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:
parent
98ad327864
commit
e64c1d8055
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue