From 2ad994f9a02648678d5760a429a65cf6d14518c5 Mon Sep 17 00:00:00 2001 From: "utkarsh.arya@zomato.com" Date: Sat, 15 Nov 2025 22:54:56 +0000 Subject: [PATCH] Allow migrations in namespace packages without __init__.py Remove obsolete __file__ check to support Python 3 implicit namespace packages as valid migration modules. This enables migrations to work in directories lacking __init__.py files. Refs #29091. --- django/db/migrations/loader.py | 5 ---- tests/migrations/test_loader.py | 30 +++++++++++++++++-- .../0001_initial.py | 16 ++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 tests/migrations/test_migrations_namespace_package/0001_initial.py diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index 8c308621d2..d075fcc5df 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -84,11 +84,6 @@ 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) diff --git a/tests/migrations/test_loader.py b/tests/migrations/test_loader.py index 0f0a590e7c..fca83d7c11 100644 --- a/tests/migrations/test_loader.py +++ b/tests/migrations/test_loader.py @@ -190,12 +190,38 @@ class LoaderTests(TestCase): ) def test_load_empty_dir(self): + """ + A namespace package (directory without __init__.py) can be a valid + migrations module even when it has no migration files. It's treated + the same as a regular package with no migrations. + """ with override_settings(MIGRATION_MODULES={"migrations": "migrations.faulty_migrations.namespace"}): loader = MigrationLoader(connection) self.assertIn( - "migrations", loader.unmigrated_apps, - "App missing __init__.py in migrations module not in unmigrated apps." + "migrations", loader.migrated_apps, + "Namespace package should be in migrated apps even without migrations." ) + # Ensure no migrations were actually loaded + migration_count = len([ + key for key in loader.disk_migrations if key[0] == "migrations" + ]) + self.assertEqual(migration_count, 0) + + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_namespace_package"}) + def test_namespace_package_with_migrations(self): + """ + Namespace packages (directories without __init__.py) can contain + migration files and should be loaded correctly. + """ + loader = MigrationLoader(connection) + self.assertIn( + "migrations", loader.migrated_apps, + "Namespace package with migrations should be in migrated apps." + ) + self.assertIn( + ("migrations", "0001_initial"), loader.disk_migrations, + "Migration from namespace package should be loaded." + ) @override_settings( INSTALLED_APPS=['migrations.migrations_test_apps.migrated_app'], diff --git a/tests/migrations/test_migrations_namespace_package/0001_initial.py b/tests/migrations/test_migrations_namespace_package/0001_initial.py new file mode 100644 index 0000000000..da6882c683 --- /dev/null +++ b/tests/migrations/test_migrations_namespace_package/0001_initial.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + operations = [ + migrations.CreateModel( + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=255)), + ], + ), + ]