Fixed #32302 -- Allowed migrations to be loaded from regular packages with no __file__ attribute.

The migrations loader prevents the use of PEP-420 namespace packages
for holding apps' migrations modules. Previously the loader tested for
this only by checking that app.migrations.__file__ is present. This
prevented migrations' being found in frozen Python environments that
don't set __file__ on any modules. Now the loader *additionally* checks
whether app.migrations.__path__ is a list because namespace packages
use a different type for __path__. Namespace packages continue to be
forbidden, and, in fact, users of normal Python environments should
experience no change whatsoever.
This commit is contained in:
William Schwartz 2020-12-28 16:05:18 -06:00 committed by Mariusz Felisiak
parent 98ad327864
commit e64c1d8055
2 changed files with 39 additions and 5 deletions

View File

@ -88,15 +88,19 @@ class MigrationLoader:
continue continue
raise raise
else: else:
# Empty directories are namespaces.
# getattr() needed on PY36 and older (replace w/attribute access).
if getattr(module, '__file__', None) is None:
self.unmigrated_apps.add(app_config.label)
continue
# Module is not a package (e.g. migrations.py). # Module is not a package (e.g. migrations.py).
if not hasattr(module, '__path__'): if not hasattr(module, '__path__'):
self.unmigrated_apps.add(app_config.label) self.unmigrated_apps.add(app_config.label)
continue continue
# Empty directories are namespaces. Namespace packages have no
# __file__ and don't use a list for __path__. See
# https://docs.python.org/3/reference/import.html#namespace-packages
if (
getattr(module, '__file__', None) is None and
not isinstance(module.__path__, list)
):
self.unmigrated_apps.add(app_config.label)
continue
# Force a reload if it's already loaded (tests need this) # Force a reload if it's already loaded (tests need this)
if was_loaded: if was_loaded:
reload(module) reload(module)

View File

@ -1,5 +1,6 @@
import compileall import compileall
import os import os
from importlib import import_module
from django.db import connection, connections from django.db import connection, connections
from django.db.migrations.exceptions import ( from django.db.migrations.exceptions import (
@ -512,6 +513,35 @@ class LoaderTests(TestCase):
migrations = [name for app, name in loader.disk_migrations if app == 'migrations'] migrations = [name for app, name in loader.disk_migrations if app == 'migrations']
self.assertEqual(migrations, []) self.assertEqual(migrations, [])
@override_settings(MIGRATION_MODULES={'migrations': 'migrations.test_migrations'})
def test_loading_package_without__file__(self):
"""
To support frozen environments, MigrationLoader loads migrations from
regular packages with no __file__ attribute.
"""
test_module = import_module('migrations.test_migrations')
loader = MigrationLoader(connection)
# __file__ == __spec__.origin or the latter is None and former is
# undefined.
module_file = test_module.__file__
module_origin = test_module.__spec__.origin
module_has_location = test_module.__spec__.has_location
try:
del test_module.__file__
test_module.__spec__.origin = None
test_module.__spec__.has_location = False
loader.load_disk()
migrations = [
name
for app, name in loader.disk_migrations
if app == 'migrations'
]
self.assertCountEqual(migrations, ['0001_initial', '0002_second'])
finally:
test_module.__file__ = module_file
test_module.__spec__.origin = module_origin
test_module.__spec__.has_location = module_has_location
class PycLoaderTests(MigrationTestBase): class PycLoaderTests(MigrationTestBase):