summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWilliam Schwartz <wkschwartz@gmail.com>2020-12-28 16:05:18 -0600
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-12-29 12:54:08 +0100
commite64c1d8055a3e476122633da141f16b50f0c4a2d (patch)
tree6095d21b79f0f82fbe9aec730b4ec0d6eaba476e
parent98ad327864aed8df245fd19ea9d2743279e11643 (diff)
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.
-rw-r--r--django/db/migrations/loader.py14
-rw-r--r--tests/migrations/test_loader.py30
2 files changed, 39 insertions, 5 deletions
diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py
index 95a5062ec9..eb370164f3 100644
--- a/django/db/migrations/loader.py
+++ b/django/db/migrations/loader.py
@@ -88,15 +88,19 @@ class MigrationLoader:
continue
raise
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).
if not hasattr(module, '__path__'):
self.unmigrated_apps.add(app_config.label)
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)
if was_loaded:
reload(module)
diff --git a/tests/migrations/test_loader.py b/tests/migrations/test_loader.py
index 27a052ddac..d8f07851d2 100644
--- a/tests/migrations/test_loader.py
+++ b/tests/migrations/test_loader.py
@@ -1,5 +1,6 @@
import compileall
import os
+from importlib import import_module
from django.db import connection, connections
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']
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):