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 import sys
from importlib import import_module, reload from importlib import import_module, reload
@ -97,17 +97,20 @@ class MigrationLoader:
if was_loaded: if was_loaded:
reload(module) reload(module)
self.migrated_apps.add(app_config.label) self.migrated_apps.add(app_config.label)
directory = os.path.dirname(module.__file__) migration_names = {name for _, name, is_pkg in pkgutil.iter_modules(module.__path__) if not is_pkg}
# Scan for .py files # Load migrations
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
for migration_name in migration_names: 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"): if not hasattr(migration_module, "Migration"):
raise BadMigrationError( raise BadMigrationError(
"Migration %s in app %s has no Migration class" % (migration_name, app_config.label) "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. * Added support for serialization of ``functools.partialmethod`` objects.
* To support frozen environments, migrations may be loaded from ``.pyc`` files.
Models Models
~~~~~~ ~~~~~~
@ -366,6 +368,9 @@ Miscellaneous
with such passwords from requesting a password reset. Audit your code to 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. 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: .. _deprecated-features-2.1:
Features deprecated in 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 import connection, connections
from django.db.migrations.exceptions import ( from django.db.migrations.exceptions import (
AmbiguityError, InconsistentMigrationHistory, NodeNotFoundError, AmbiguityError, InconsistentMigrationHistory, NodeNotFoundError,
@ -6,6 +9,8 @@ from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
from django.test import TestCase, modify_settings, override_settings from django.test import TestCase, modify_settings, override_settings
from .test_base import MigrationTestBase
class RecorderTests(TestCase): class RecorderTests(TestCase):
""" """
@ -494,3 +499,32 @@ class LoaderTests(TestCase):
('app1', '4_auto'), ('app1', '4_auto'),
} }
self.assertEqual(plan, expected_plan) 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)