Fixed #23406 -- Allowed migrations to be loaded from .pyc files.

This commit is contained in:
Dan Watson 2018-03-15 15:32:56 -04:00 committed by Tim Graham
parent ee7f51c66d
commit 29150d5da8
5 changed files with 53 additions and 11 deletions

View File

@ -1,4 +1,4 @@
import os
import pkgutil
import sys
from importlib import import_module, reload
@ -97,17 +97,20 @@ class MigrationLoader:
if was_loaded:
reload(module)
self.migrated_apps.add(app_config.label)
directory = os.path.dirname(module.__file__)
# Scan for .py files
migration_names = set()
for name in os.listdir(directory):
if name.endswith(".py"):
import_name = name.rsplit(".", 1)[0]
if import_name[0] not in "_.~":
migration_names.add(import_name)
# Load them
migration_names = {name for _, name, is_pkg in pkgutil.iter_modules(module.__path__) if not is_pkg}
# Load migrations
for migration_name in migration_names:
migration_module = import_module("%s.%s" % (module_name, migration_name))
migration_path = '%s.%s' % (module_name, migration_name)
try:
migration_module = import_module(migration_path)
except ImportError as e:
if 'bad magic number' in str(e):
raise ImportError(
"Couldn't import %r as it appears to be a stale "
".pyc file." % migration_path
) from e
else:
raise
if not hasattr(migration_module, "Migration"):
raise BadMigrationError(
"Migration %s in app %s has no Migration class" % (migration_name, app_config.label)

View File

@ -194,6 +194,8 @@ Migrations
* Added support for serialization of ``functools.partialmethod`` objects.
* To support frozen environments, migrations may be loaded from ``.pyc`` files.
Models
~~~~~~
@ -366,6 +368,9 @@ Miscellaneous
with such passwords from requesting a password reset. Audit your code to
confirm that your usage of these APIs don't rely on the old behavior.
* Since migrations are now loaded from ``.pyc`` files, you might need to delete
them if you're working in a mixed Python 2 and Python 3 environment.
.. _deprecated-features-2.1:
Features deprecated in 2.1

View File

@ -1,3 +1,6 @@
import compileall
import os
from django.db import connection, connections
from django.db.migrations.exceptions import (
AmbiguityError, InconsistentMigrationHistory, NodeNotFoundError,
@ -6,6 +9,8 @@ from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder
from django.test import TestCase, modify_settings, override_settings
from .test_base import MigrationTestBase
class RecorderTests(TestCase):
"""
@ -494,3 +499,32 @@ class LoaderTests(TestCase):
('app1', '4_auto'),
}
self.assertEqual(plan, expected_plan)
class PycLoaderTests(MigrationTestBase):
def test_valid(self):
"""
To support frozen environments, MigrationLoader loads .pyc migrations.
"""
with self.temporary_migration_module(module='migrations.test_migrations') as migration_dir:
# Compile .py files to .pyc files and delete .py files.
compileall.compile_dir(migration_dir, force=True, quiet=1, legacy=True)
for name in os.listdir(migration_dir):
if name.endswith('.py'):
os.remove(os.path.join(migration_dir, name))
loader = MigrationLoader(connection)
self.assertIn(('migrations', '0001_initial'), loader.disk_migrations)
def test_invalid(self):
"""
MigrationLoader reraises ImportErrors caused by "bad magic number" pyc
files with a more helpful message.
"""
with self.temporary_migration_module(module='migrations.test_migrations_bad_pyc'):
msg = (
"Couldn't import '\w+.migrations.0001_initial' as it appears "
"to be a stale .pyc file."
)
with self.assertRaisesRegex(ImportError, msg):
MigrationLoader(connection)