def test_simple_migration(self): """ Tests serializing a simple migration. """ migration = type( str("Migration"), (migrations.Migration,), { "operations": [ migrations.DeleteModel("MyModel"), migrations.AddField( "OtherModel", "field_name", models.DateTimeField(default=datetime.datetime.utcnow) ), ], "dependencies": [("testapp", "some_other_one")], }, ) writer = MigrationWriter(migration) output = writer.as_string() # It should NOT be unicode. self.assertIsInstance(output, six.binary_type, "Migration as_string returned unicode") # We don't test the output formatting - that's too fragile. # Just make sure it runs for now, and that things look alright. result = self.safe_exec(output) self.assertIn("Migration", result)
def test_migration(self): """ Tests making migrations with Django 1.7+'s migration framework """ import oscar from django.db import migrations from django.db.migrations.writer import MigrationWriter from django.utils import six from oscar.models.fields import AutoSlugField fields = { 'autoslugfield': AutoSlugField(populate_from='otherfield'), } migration = type(str("Migration"), (migrations.Migration,), { "operations": [ migrations.CreateModel("MyModel", tuple(fields.items()), {'populate_from': 'otherfield'}, (models.Model,)), ], }) writer = MigrationWriter(migration) output = writer.as_string() if isinstance(output, six.text_type): output = output.encode('utf-8') # We don't test the output formatting - that's too fragile. # Just make sure it runs for now, and that things look alright. context = { 'migrations': migrations, 'oscar': oscar, } result = self.safe_exec(output, context=context) self.assertIn("Migration", result)
def test_total_deconstruct(self): loader = MigrationLoader(None, load=True, ignore_no_migrations=True) loader.disk_migrations = {t: v for t, v in loader.disk_migrations.items() if t[0] != 'testapp'} app_labels = {"testapp"} questioner = NonInteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=True) autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) # Detect changes changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels or None, convert_apps=app_labels or None, migration_name="my_fake_migration_for_test_deconstruct", ) self.assertGreater(len(changes), 0) for app_label, app_migrations in changes.items(): for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration) migration_string = writer.as_string() self.assertNotEqual(migration_string, "")
def test_serialize_multiline_strings(self): self.assertSerializedEqual(b"foo\nbar") string, imports = MigrationWriter.serialize(b"foo\nbar") self.assertEqual(string, "b'foo\\nbar'") self.assertSerializedEqual("föo\nbár") string, imports = MigrationWriter.serialize("foo\nbar") self.assertEqual(string, "'foo\\nbar'")
def test_simple_migration(self): """ Tests serializing a simple migration. """ fields = { 'charfield': models.DateTimeField(default=datetime.datetime.utcnow), 'datetimefield': models.DateTimeField(default=datetime.datetime.utcnow), } options = { 'verbose_name': 'My model', 'verbose_name_plural': 'My models', } migration = type("Migration", (migrations.Migration,), { "operations": [ migrations.CreateModel("MyModel", tuple(fields.items()), options, (models.Model,)), migrations.CreateModel("MyModel2", tuple(fields.items()), bases=(models.Model,)), migrations.CreateModel( name="MyModel3", fields=tuple(fields.items()), options=options, bases=(models.Model,) ), migrations.DeleteModel("MyModel"), migrations.AddField("OtherModel", "datetimefield", fields["datetimefield"]), ], "dependencies": [("testapp", "some_other_one")], }) writer = MigrationWriter(migration) output = writer.as_string() # We don't test the output formatting - that's too fragile. # Just make sure it runs for now, and that things look alright. result = self.safe_exec(output) self.assertIn("Migration", result)
def write_migration_files(self, changes): """ Takes a changes dict and writes them out as migration files. """ directory_created = {} for app_label, app_migrations in changes.items(): if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label) + "\n") for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration) if self.verbosity >= 1: self.stdout.write(" %s:\n" % (self.style.MIGRATE_LABEL(writer.filename),)) for operation in migration.operations: self.stdout.write(" - %s\n" % operation.describe()) if not self.dry_run: # Write the migrations file to the disk. migrations_directory = os.path.dirname(writer.path) if not directory_created.get(app_label, False): if not os.path.isdir(migrations_directory): os.mkdir(migrations_directory) init_path = os.path.join(migrations_directory, "__init__.py") if not os.path.isfile(init_path): open(init_path, "w").close() # We just do this once per app directory_created[app_label] = True migration_string = writer.as_string() with open(writer.path, "wb") as fh: fh.write(migration_string) elif self.verbosity == 3: # Alternatively, makemigrations --dry-run --verbosity 3 # will output the migrations to stdout rather than saving # the file to the disk. self.stdout.write(self.style.MIGRATE_HEADING("Full migrations file '%s':" % writer.filename) + "\n") self.stdout.write("%s\n" % writer.as_string())
def test_17_migration(self): """ Tests making migrations with Django 1.7+'s migration framework """ fields = { 'autoslugfield': AutoSlugField(populate_from='otherfield'), } migration = type(str("Migration"), (migrations.Migration,), { "operations": [ migrations.CreateModel("MyModel", tuple(fields.items()), {'populate_from': 'otherfield'}, (models.Model,)), ], }) writer = MigrationWriter(migration) output = writer.as_string() # It should NOT be unicode. self.assertIsInstance(output, six.binary_type, "Migration as_string returned unicode") # We don't test the output formatting - that's too fragile. # Just make sure it runs for now, and that things look alright. result = self.safe_exec(output) self.assertIn("Migration", result)
def test_user_foreign_key_supports_migration(): """Tests serializing UserForeignKey in a simple migration. Since `UserForeignKey` is a ForeignKey migrations pass `to=` explicitly and we have to pop it in our __init__. """ fields = { 'charfield': UserForeignKey(), } migration = type(str('Migration'), (migrations.Migration,), { 'operations': [ migrations.CreateModel( name='MyModel', fields=tuple(fields.items()), bases=(models.Model,) ), ], }) writer = MigrationWriter(migration) output = writer.as_string() # Just make sure it runs and that things look alright. result = safe_exec(output, globals_=globals()) assert 'Migration' in result
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={"ask_merge": True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = loader.graph.forwards_plan((app_label, migration_name)) merge_migrations.append(migration) all_items_equal = lambda seq: all(item == seq[0] for item in seq[1:]) merge_migrations_generations = zip(*[m.ancestry for m in merge_migrations]) common_ancestor_count = sum( 1 for common_ancestor_generation in takewhile(all_items_equal, merge_migrations_generations) ) if not common_ancestor_count: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[common_ancestor_count:] migrations_ops = ( loader.get_migration(node_app, node_name).operations for node_app, node_name in migration.branch ) migration.merged_operations = sum(migrations_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s\n" % operation.describe()) if questioner.ask_merge(app_label): # If they still want to merge it, then write out an empty # file depending on the migrations needing merging. numbers = [MigrationAutodetector.parse_number(migration.name) for migration in merge_migrations] try: biggest_number = max([x for x in numbers if x is not None]) except ValueError: biggest_number = 1 subclass = type( "Migration", (Migration,), {"dependencies": [(app_label, migration.name) for migration in merge_migrations]}, ) new_migration = subclass("%04i_merge" % (biggest_number + 1), app_label) writer = MigrationWriter(new_migration) with open(writer.path, "wb") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path)
def test_full_serialization(self): # ensure the values passed to kwarg arguments can be serialized # the recommended 'deconstruct' testing by django docs doesn't cut it # https://docs.djangoproject.com/en/1.7/howto/custom-model-fields/#field-deconstruction # replicates https://github.com/mfogel/django-timezone-field/issues/12 for field in self.test_fields: # ensuring the following call doesn't throw an error MigrationWriter.serialize(field)
def write_migration(self, migration): loader = MigrationLoader(None, ignore_no_migrations=True) autodetector = MigrationAutodetector(loader.project_state(), ProjectState.from_apps(apps),) changes = autodetector.arrange_for_graph(changes={'share': [migration]}, graph=loader.graph,) for m in changes['share']: writer = MigrationWriter(m) with open(writer.path, 'wb') as fp: fp.write(writer.as_string())
def test_register_serializer(self): class ComplexSerializer(BaseSerializer): def serialize(self): return 'complex(%r)' % self.value, {} MigrationWriter.register_serializer(complex, ComplexSerializer) self.assertSerializedEqual(complex(1, 2)) MigrationWriter.unregister_serializer(complex) with self.assertRaisesMessage(ValueError, 'Cannot serialize: (1+2j)'): self.assertSerializedEqual(complex(1, 2))
def test_serialize(self): """ Tests various different forms of the serializer. This does not care about formatting, just that the parsed result is correct, so we always exec() the result and check that. """ # Basic values self.assertSerializedEqual(1) self.assertSerializedEqual(None) self.assertSerializedEqual(b"foobar") self.assertSerializedEqual("föobár") self.assertSerializedEqual({1: 2}) self.assertSerializedEqual(["a", 2, True, None]) self.assertSerializedEqual(set([2, 3, "eighty"])) self.assertSerializedEqual({"lalalala": ["yeah", "no", "maybe"]}) self.assertSerializedEqual(_('Hello')) # Functions with six.assertRaisesRegex(self, ValueError, 'Cannot serialize function: lambda'): self.assertSerializedEqual(lambda x: 42) self.assertSerializedEqual(models.SET_NULL) string, imports = MigrationWriter.serialize(models.SET(42)) self.assertEqual(string, 'models.SET(42)') self.serialize_round_trip(models.SET(42)) # Datetime stuff self.assertSerializedEqual(datetime.datetime.utcnow()) self.assertSerializedEqual(datetime.datetime.utcnow) self.assertSerializedEqual(datetime.datetime.today()) self.assertSerializedEqual(datetime.datetime.today) self.assertSerializedEqual(datetime.date.today()) self.assertSerializedEqual(datetime.date.today) # Classes validator = RegexValidator(message="hello") string, imports = MigrationWriter.serialize(validator) self.assertEqual(string, "django.core.validators.RegexValidator(message=%s)" % repr("hello")) self.serialize_round_trip(validator) validator = EmailValidator(message="hello") # Test with a subclass. string, imports = MigrationWriter.serialize(validator) self.assertEqual(string, "django.core.validators.EmailValidator(message=%s)" % repr("hello")) self.serialize_round_trip(validator) validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello") string, imports = MigrationWriter.serialize(validator) self.assertEqual(string, "custom.EmailValidator(message=%s)" % repr("hello")) # Django fields self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.TextField(null=True, blank=True)) # Setting references self.assertSerializedEqual(SettingsReference(settings.AUTH_USER_MODEL, "AUTH_USER_MODEL")) self.assertSerializedResultEqual( SettingsReference("someapp.model", "AUTH_USER_MODEL"), ( "settings.AUTH_USER_MODEL", set(["from django.conf import settings"]), ) )
def test_serialize_enums(self): class TextEnum(enum.Enum): A = 'a-value' B = 'value-b' class BinaryEnum(enum.Enum): A = b'a-value' B = b'value-b' class IntEnum(enum.IntEnum): A = 1 B = 2 self.assertSerializedResultEqual( TextEnum.A, ("migrations.test_writer.TextEnum('a-value')", {'import migrations.test_writer'}) ) self.assertSerializedResultEqual( BinaryEnum.A, ("migrations.test_writer.BinaryEnum(b'a-value')", {'import migrations.test_writer'}) ) self.assertSerializedResultEqual( IntEnum.B, ("migrations.test_writer.IntEnum(2)", {'import migrations.test_writer'}) ) field = models.CharField(default=TextEnum.B, choices=[(m.value, m) for m in TextEnum]) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.CharField(choices=[" "('a-value', migrations.test_writer.TextEnum('a-value')), " "('value-b', migrations.test_writer.TextEnum('value-b'))], " "default=migrations.test_writer.TextEnum('value-b'))" ) field = models.CharField(default=BinaryEnum.B, choices=[(m.value, m) for m in BinaryEnum]) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.CharField(choices=[" "(b'a-value', migrations.test_writer.BinaryEnum(b'a-value')), " "(b'value-b', migrations.test_writer.BinaryEnum(b'value-b'))], " "default=migrations.test_writer.BinaryEnum(b'value-b'))" ) field = models.IntegerField(default=IntEnum.A, choices=[(m.value, m) for m in IntEnum]) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.IntegerField(choices=[" "(1, migrations.test_writer.IntEnum(1)), " "(2, migrations.test_writer.IntEnum(2))], " "default=migrations.test_writer.IntEnum(1))" )
def test_models_import_omitted(self): """ django.db.models shouldn't be imported if unused. """ migration = type("Migration", (migrations.Migration,), { "operations": [ migrations.AlterModelOptions( name='model', options={'verbose_name': 'model', 'verbose_name_plural': 'models'}, ), ] }) writer = MigrationWriter(migration) output = writer.as_string() self.assertIn("from django.db import migrations\n", output)
def test_serialize_class_based_validators(self): """ Ticket #22943: Test serialization of class-based validators, including compiled regexes. """ validator = RegexValidator(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.RegexValidator(message='hello')") self.serialize_round_trip(validator) # Test with a compiled regex. validator = RegexValidator(regex=re.compile(r'^\w+$')) string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.RegexValidator(regex=re.compile('^\\\\w+$'))") self.serialize_round_trip(validator) # Test a string regex with flag validator = RegexValidator(r'^[0-9]+$', flags=re.S) string = MigrationWriter.serialize(validator)[0] if PY36: self.assertEqual(string, "django.core.validators.RegexValidator('^[0-9]+$', flags=re.RegexFlag(16))") else: self.assertEqual(string, "django.core.validators.RegexValidator('^[0-9]+$', flags=16)") self.serialize_round_trip(validator) # Test message and code validator = RegexValidator('^[-a-zA-Z0-9_]+$', 'Invalid', 'invalid') string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.RegexValidator('^[-a-zA-Z0-9_]+$', 'Invalid', 'invalid')") self.serialize_round_trip(validator) # Test with a subclass. validator = EmailValidator(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.EmailValidator(message='hello')") self.serialize_round_trip(validator) validator = deconstructible(path="migrations.test_writer.EmailValidator")(EmailValidator)(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "migrations.test_writer.EmailValidator(message='hello')") validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello") with self.assertRaisesMessage(ImportError, "No module named 'custom'"): MigrationWriter.serialize(validator) validator = deconstructible(path="django.core.validators.EmailValidator2")(EmailValidator)(message="hello") with self.assertRaisesMessage(ValueError, "Could not find object EmailValidator2 in django.core.validators."): MigrationWriter.serialize(validator)
def test_serialize(self): """ Tests various different forms of the serializer. This does not care about formatting, just that the parsed result is correct, so we always exec() the result and check that. """ # Basic values self.assertSerializedEqual(1) self.assertSerializedEqual(None) self.assertSerializedEqual(b"foobar") self.assertSerializedEqual("föobár") self.assertSerializedEqual({1: 2}) self.assertSerializedEqual(["a", 2, True, None]) self.assertSerializedEqual(set([2, 3, "eighty"])) self.assertSerializedEqual({"lalalala": ["yeah", "no", "maybe"]}) self.assertSerializedEqual(_('Hello')) # Functions with six.assertRaisesRegex(self, ValueError, 'Cannot serialize function: lambda'): self.assertSerializedEqual(lambda x: 42) self.assertSerializedEqual(models.SET_NULL) string, imports = MigrationWriter.serialize(models.SET(42)) self.assertEqual(string, 'models.SET(42)') self.serialize_round_trip(models.SET(42)) # Datetime stuff self.assertSerializedEqual(datetime.datetime.utcnow()) self.assertSerializedEqual(datetime.datetime.utcnow) self.assertSerializedEqual(datetime.datetime.today()) self.assertSerializedEqual(datetime.datetime.today) self.assertSerializedEqual(datetime.date.today()) self.assertSerializedEqual(datetime.date.today) # Django fields self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))
def test_register_serializer_for_migrations(self): tests = ( (DateRange(empty=True), DateRangeField), (DateTimeRange(empty=True), DateRangeField), (DateTimeTZRange(None, None, '[]'), DateTimeRangeField), (NumericRange(1, 10), IntegerRangeField), ) def assertNotSerializable(): for default, test_field in tests: with self.subTest(default=default): field = test_field(default=default) with self.assertRaisesMessage(ValueError, 'Cannot serialize: %s' % default.__class__.__name__): MigrationWriter.serialize(field) assertNotSerializable() with self.modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'}): for default, test_field in tests: with self.subTest(default=default): field = test_field(default=default) serialized_field, imports = MigrationWriter.serialize(field) self.assertEqual(imports, { 'import django.contrib.postgres.fields.ranges', 'import psycopg2.extras', }) self.assertIn( '%s.%s(default=psycopg2.extras.%r)' % ( field.__module__, field.__class__.__name__, default, ), serialized_field ) assertNotSerializable()
def test_migration_serialization(): imports = set(['import djmoney.models.fields']) if PY2: serialized = 'djmoney.models.fields.MoneyPatched(100, b\'GBP\')' else: serialized = 'djmoney.models.fields.MoneyPatched(100, \'GBP\')' assert MigrationWriter.serialize(MoneyPatched(100, 'GBP')) == (serialized, imports)
def test_serialize_functions(self): with six.assertRaisesRegex(self, ValueError, "Cannot serialize function: lambda"): self.assertSerializedEqual(lambda x: 42) self.assertSerializedEqual(models.SET_NULL) string, imports = MigrationWriter.serialize(models.SET(42)) self.assertEqual(string, "models.SET(42)") self.serialize_round_trip(models.SET(42))
def test_serialize_functions(self): with self.assertRaisesMessage(ValueError, 'Cannot serialize function: lambda'): self.assertSerializedEqual(lambda x: 42) self.assertSerializedEqual(models.SET_NULL) string, imports = MigrationWriter.serialize(models.SET(42)) self.assertEqual(string, 'models.SET(42)') self.serialize_round_trip(models.SET(42))
def write_migration_files(self, changes): """ Take a changes dict and write them out as migration files. """ directory_created = {} for app_label, app_migrations in changes.items(): if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label) + "\n") for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration) if self.verbosity >= 1: # Display a relative path if it's below the current working # directory, or an absolute path otherwise. try: migration_string = os.path.relpath(writer.path) except ValueError: migration_string = writer.path if migration_string.startswith('..'): migration_string = writer.path self.stdout.write(" %s\n" % (self.style.MIGRATE_LABEL(migration_string),)) for operation in migration.operations: self.stdout.write(" - %s\n" % operation.describe()) if not self.dry_run: # Write the migrations file to the disk. migrations_directory = os.path.dirname(writer.path) if not directory_created.get(app_label): if not os.path.isdir(migrations_directory): os.mkdir(migrations_directory) init_path = os.path.join(migrations_directory, "__init__.py") if not os.path.isfile(init_path): open(init_path, "w").close() # We just do this once per app directory_created[app_label] = True migration_string = writer.as_string() with open(writer.path, "w", encoding='utf-8') as fh: fh.write(migration_string) elif self.verbosity == 3: # Alternatively, makemigrations --dry-run --verbosity 3 # will output the migrations to stdout rather than saving # the file to the disk. self.stdout.write(self.style.MIGRATE_HEADING( "Full migrations file '%s':" % writer.filename) + "\n" ) self.stdout.write("%s\n" % writer.as_string())
def test_simple_migration(self): """ Tests serializing a simple migration. """ fields = { 'charfield': models.DateTimeField(default=datetime.datetime.utcnow), 'datetimefield': models.DateTimeField(default=datetime.datetime.utcnow), } options = { 'verbose_name': 'My model', 'verbose_name_plural': 'My models', } migration = type(str("Migration"), (migrations.Migration,), { "operations": [ migrations.CreateModel("MyModel", tuple(fields.items()), options, (models.Model,)), migrations.CreateModel("MyModel2", tuple(fields.items()), bases=(models.Model,)), migrations.CreateModel( name="MyModel3", fields=tuple(fields.items()), options=options, bases=(models.Model,) ), migrations.DeleteModel("MyModel"), migrations.AddField("OtherModel", "datetimefield", fields["datetimefield"]), ], "dependencies": [("testapp", "some_other_one")], }) writer = MigrationWriter(migration) output = writer.as_string() # It should NOT be unicode. self.assertIsInstance(output, six.binary_type, "Migration as_string returned unicode") # We don't test the output formatting - that's too fragile. # Just make sure it runs for now, and that things look alright. result = self.safe_exec(output) self.assertIn("Migration", result) # In order to preserve compatibility with Python 3.2 unicode literals # prefix shouldn't be added to strings. tokens = tokenize.generate_tokens(six.StringIO(str(output)).readline) for token_type, token_source, (srow, scol), __, line in tokens: if token_type == tokenize.STRING: self.assertFalse( token_source.startswith('u'), "Unicode literal prefix found at %d:%d: %r" % ( srow, scol, line.strip() ) )
def test_migration(self): fields = { 'field1':EnumField(TestEnum) } migration = type(str("Migration"), (migrations.Migration,), { 'operations' : [ migrations.CreateModel("Model1", tuple(fields.items()), {}, (models.Model)) ] }) writer = MigrationWriter(migration) output = writer.as_string() self.assertIsInstance(output, six.binary_type) r = {} try: exec(output, globals(), r) except Exception as e: self.fail("Could not exec {!r}: {}".format(output.strip(), e)) self.assertIn("Migration", r)
def uninstall_if_needed(setting, value, enter, **kwargs): """ Undo the effects of PostgresConfig.ready() when django.contrib.postgres is "uninstalled" by override_settings(). """ if not enter and setting == 'INSTALLED_APPS' and 'django.contrib.postgres' not in set(value): connection_created.disconnect(register_type_handlers) CharField._unregister_lookup(Unaccent) TextField._unregister_lookup(Unaccent) CharField._unregister_lookup(SearchLookup) TextField._unregister_lookup(SearchLookup) CharField._unregister_lookup(TrigramSimilar) TextField._unregister_lookup(TrigramSimilar) # Disconnect this receiver until the next time this app is installed # and ready() connects it again to prevent unnecessary processing on # each setting change. setting_changed.disconnect(uninstall_if_needed) MigrationWriter.unregister_serializer(RANGE_TYPES)
def test_custom_operation(self): migration = type("Migration", (migrations.Migration,), { "operations": [ custom_migration_operations.operations.TestOperation(), custom_migration_operations.operations.CreateModel(), migrations.CreateModel("MyModel", (), {}, (models.Model,)), custom_migration_operations.more_operations.TestOperation() ], "dependencies": [] }) writer = MigrationWriter(migration) output = writer.as_string() result = self.safe_exec(output) self.assertIn("custom_migration_operations", result) self.assertNotEqual( result['custom_migration_operations'].operations.TestOperation, result['custom_migration_operations'].more_operations.TestOperation )
def test_serialize_datetime(self): """ #23365 -- Timezone-aware datetimes should be allowed. """ # naive datetime naive_datetime = datetime.datetime(2014, 1, 1, 1, 1) self.assertEqual(MigrationWriter.serialize_datetime(naive_datetime), "datetime.datetime(2014, 1, 1, 1, 1)") # datetime with utc timezone utc_datetime = datetime.datetime(2014, 1, 1, 1, 1, tzinfo=utc) self.assertEqual(MigrationWriter.serialize_datetime(utc_datetime), "datetime.datetime(2014, 1, 1, 1, 1, tzinfo=utc)") # datetime with FixedOffset tzinfo fixed_offset_datetime = datetime.datetime(2014, 1, 1, 1, 1, tzinfo=FixedOffset(180)) self.assertEqual(MigrationWriter.serialize_datetime(fixed_offset_datetime), "datetime.datetime(2013, 12, 31, 22, 1, tzinfo=utc)")
def test_sorted_imports(self): """ #24155 - Tests ordering of imports. """ migration = type("Migration", (migrations.Migration,), { "operations": [ migrations.AddField("mymodel", "myfield", models.DateTimeField( default=datetime.datetime(2012, 1, 1, 1, 1, tzinfo=utc), )), ] }) writer = MigrationWriter(migration) output = writer.as_string() self.assertIn( "import datetime\n" "from django.db import migrations, models\n" "from django.utils.timezone import utc\n", output )
def test_migration_file_header_comments(self): """ Test comments at top of file. """ migration = type("Migration", (migrations.Migration,), { "operations": [] }) dt = datetime.datetime(2015, 7, 31, 4, 40, 0, 0, tzinfo=utc) with mock.patch('django.db.migrations.writer.now', lambda: dt): writer = MigrationWriter(migration) output = writer.as_string() self.assertTrue( output.startswith( "# Generated by Django %(version)s on 2015-07-31 04:40\n" % { 'version': get_version(), } ) )
def test_deconstruct_class_arguments(self): # Yes, it doesn't make sense to use a class as a default for a # CharField. It does make sense for custom fields though, for example # an enumfield that takes the enum class as an argument. class DeconstructibleInstances: def deconstruct(self): return ('DeconstructibleInstances', [], {}) string = MigrationWriter.serialize(models.CharField(default=DeconstructibleInstances))[0] self.assertEqual(string, "models.CharField(default=migrations.test_writer.DeconstructibleInstances)")
def test_makemigrations_with_size(self): field = ListTextField( models.CharField(max_length=5), max_length=32, size=5 ) statement, imports = MigrationWriter.serialize(field) # The order of the output max_length/size statements varies by # python version, hence a little regexp to match them assert re.compile( r"""^django_mysql\.models\.ListTextField\( models\.CharField\(max_length=5\),\ # space here ( max_length=32,\ size=5| size=5,\ max_length=32 ) \)$ """, re.VERBOSE ).match(statement)
def test_deconstruct_default_arguments(self): svf = SearchVectorField([ WeightedColumn('name', 'A'), WeightedColumn('description', 'D'), ], language=None, language_column=None, force_update=False) definition, path = MigrationWriter.serialize(svf) self.assertEqual( "tsvector_field.SearchVectorField(" "columns=[" "tsvector_field.WeightedColumn('name', 'A'), " "tsvector_field.WeightedColumn('description', 'D')]" ")", definition ) self.assertSetEqual( {'import tsvector_field'}, path )
def test_serialize_uuid(self): self.assertSerializedEqual(uuid.uuid1()) self.assertSerializedEqual(uuid.uuid4()) uuid_a = uuid.UUID('5c859437-d061-4847-b3f7-e6b78852f8c8') uuid_b = uuid.UUID('c7853ec1-2ea3-4359-b02d-b54e8f1bcee2') self.assertSerializedResultEqual( uuid_a, ("uuid.UUID('5c859437-d061-4847-b3f7-e6b78852f8c8')", {'import uuid'})) self.assertSerializedResultEqual( uuid_b, ("uuid.UUID('c7853ec1-2ea3-4359-b02d-b54e8f1bcee2')", {'import uuid'})) field = models.UUIDField(choices=((uuid_a, 'UUID A'), (uuid_b, 'UUID B')), default=uuid_a) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.UUIDField(choices=[" "(uuid.UUID('5c859437-d061-4847-b3f7-e6b78852f8c8'), 'UUID A'), " "(uuid.UUID('c7853ec1-2ea3-4359-b02d-b54e8f1bcee2'), 'UUID B')], " "default=uuid.UUID('5c859437-d061-4847-b3f7-e6b78852f8c8'))")
def test_migration_path(self): test_apps = [ 'migrations.migrations_test_apps.normal', 'migrations.migrations_test_apps.with_package_model', 'migrations.migrations_test_apps.without_init_file', ] base_dir = os.path.dirname(os.path.dirname(__file__)) for app in test_apps: with self.modify_settings(INSTALLED_APPS={'append': app}): migration = migrations.Migration('0001_initial', app.split('.')[-1]) expected_path = os.path.join( base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) writer = MigrationWriter(migration) # Silence warning on Python 2: Not importing directory # 'tests/migrations/migrations_test_apps/without_init_file/migrations': # missing __init__.py with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=ImportWarning) self.assertEqual(writer.path, expected_path)
def test_migration_path(self): _old_app_store = copy.deepcopy(cache.app_store) test_apps = [ 'migrations.migrations_test_apps.normal', 'migrations.migrations_test_apps.with_package_model', ] base_dir = os.path.dirname(os.path.dirname(__file__)) try: with override_settings(INSTALLED_APPS=test_apps): for app in test_apps: cache.load_app(app) migration = migrations.Migration('0001_initial', app.split('.')[-1]) expected_path = os.path.join( base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) writer = MigrationWriter(migration) self.assertEqual(writer.path, expected_path) finally: cache.app_store = _old_app_store
def test_register_serializer_for_migrations(self): tests = ( (DateRange(empty=True), DateRangeField), (DateTimeRange(empty=True), DateRangeField), (DateTimeTZRange(None, None, '[]'), DateTimeRangeField), (NumericRange(1, 10), IntegerRangeField), ) def assertNotSerializable(): for default, test_field in tests: with self.subTest(default=default): field = test_field(default=default) with self.assertRaisesMessage( ValueError, 'Cannot serialize: %s' % default.__class__.__name__): MigrationWriter.serialize(field) assertNotSerializable() with self.modify_settings( INSTALLED_APPS={'append': 'django.contrib.postgres'}): for default, test_field in tests: with self.subTest(default=default): field = test_field(default=default) serialized_field, imports = MigrationWriter.serialize( field) self.assertEqual( imports, { 'import django.contrib.postgres.fields.ranges', 'import psycopg2.extras', }) self.assertIn( '%s.%s(default=psycopg2.extras.%r)' % ( field.__module__, field.__class__.__name__, default, ), serialized_field) assertNotSerializable()
def test_serialize_complex_func_index(self): index = models.Index( models.Func('rating', function='ABS'), models.Case( models.When(name='special', then=models.Value('X')), default=models.Value('other'), ), models.ExpressionWrapper( models.F('pages'), output_field=models.IntegerField(), ), models.OrderBy(models.F('name').desc()), name='complex_func_index', ) string, imports = MigrationWriter.serialize(index) self.assertEqual( string, "models.Index(models.Func('rating', function='ABS'), " "models.Case(models.When(name='special', then=models.Value('X')), " "default=models.Value('other')), " "models.ExpressionWrapper(" "models.F('pages'), output_field=models.IntegerField()), " "models.OrderBy(models.OrderBy(models.F('name'), descending=True)), " "name='complex_func_index')") self.assertEqual(imports, {'from django.db import models'})
def test_serialize_class_based_validators(self): """ Ticket #22943: Test serialization of class-based validators, including compiled regexes. """ validator = RegexValidator(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.RegexValidator(message='hello')") self.serialize_round_trip(validator) # Test with a compiled regex. validator = RegexValidator(regex=re.compile(r'^\w+$', re.U)) string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.RegexValidator(regex=re.compile('^\\\\w+$', 32))") self.serialize_round_trip(validator) # Test a string regex with flag validator = RegexValidator(r'^[0-9]+$', flags=re.U) string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.RegexValidator('^[0-9]+$', flags=32)") self.serialize_round_trip(validator) # Test message and code validator = RegexValidator('^[-a-zA-Z0-9_]+$', 'Invalid', 'invalid') string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.RegexValidator('^[-a-zA-Z0-9_]+$', 'Invalid', 'invalid')") self.serialize_round_trip(validator) # Test with a subclass. validator = EmailValidator(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "django.core.validators.EmailValidator(message='hello')") self.serialize_round_trip(validator) validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual(string, "custom.EmailValidator(message='hello')")
def assertSerializedResultEqual(self, value, target): self.assertEqual(MigrationWriter.serialize(value), target)
def get_text_for_value(self, value): return ', '.join( [self.choicelist.get_text_for_value(bc.value) for bc in value]) # from lino.core.choicelists import Choice, ChoiceList, CallableChoice class ChoiceSerializer(BaseSerializer): def serialize(self): return ("rt.models.{}.{}.get_by_value('{}')".format( self.value.choicelist.app_label, self.value.choicelist.__name__, self.value.value), {'from lino.api.shell import rt'}) MigrationWriter.register_serializer(Choice, ChoiceSerializer) class ChoiceListSerializer(BaseSerializer): def serialize(self): return ("rt.models.{}.{}".format(self.value.choicelist.app_label, self.value.choicelist.__name__), {'from lino.api.shell import rt'}) MigrationWriter.register_serializer(ChoiceList, ChoiceListSerializer) class CallableChoiceSerializer(BaseSerializer): def serialize(self): choice = self.value()
def handle(self, **options): self.verbosity = options['verbosity'] self.interactive = options['interactive'] app_label = options['app_label'] start_migration_name = options['start_migration_name'] migration_name = options['migration_name'] no_optimize = options['no_optimize'] squashed_name = options['squashed_name'] include_header = options['include_header'] # Validate app_label. try: apps.get_app_config(app_label) except LookupError as err: raise CommandError(str(err)) # Load the current graph state, check the BLOG and migration they asked for exists loader = MigrationLoader(connections[DEFAULT_DB_ALIAS]) if app_label not in loader.migrated_apps: raise CommandError( "App '%s' does not have migrations (so squashmigrations on " "it makes no sense)" % app_label) migration = self.find_migration(loader, app_label, migration_name) # Work out the list of predecessor migrations migrations_to_squash = [ loader.get_migration(al, mn) for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name)) if al == migration.app_label ] if start_migration_name: start_migration = self.find_migration(loader, app_label, start_migration_name) start = loader.get_migration(start_migration.app_label, start_migration.name) try: start_index = migrations_to_squash.index(start) migrations_to_squash = migrations_to_squash[start_index:] except ValueError: raise CommandError( "The migration '%s' cannot be found. Maybe it comes after " "the migration '%s'?\n" "Have a look at:\n" " python manage.py showmigrations %s\n" "to debug this issue." % (start_migration, migration, app_label)) # Tell them what we're doing and optionally ask if we should proceed if self.verbosity > 0 or self.interactive: self.stdout.write( self.style.MIGRATE_HEADING( "Will squash the following migrations:")) for migration in migrations_to_squash: self.stdout.write(" - %s" % migration.name) if self.interactive: answer = None while not answer or answer not in "yn": answer = input("Do you wish to proceed? [yN] ") if not answer: answer = "n" break else: answer = answer[0].lower() if answer != "y": return # Load the operations from all those migrations and concat together, # along with collecting external dependencies and detecting # double-squashing operations = [] dependencies = set() # We need to take all dependencies from the first migration in the list # as it may be 0002 depending on 0001 first_migration = True for smigration in migrations_to_squash: if smigration.replaces: raise CommandError( "You cannot squash squashed migrations! Please transition " "it to a normal migration first: " "https://docs.djangoproject.com/en/%s/topics/migrations/#squashing-migrations" % get_docs_version()) operations.extend(smigration.operations) for dependency in smigration.dependencies: if isinstance(dependency, SwappableTuple): if settings.AUTH_USER_MODEL == dependency.setting: dependencies.add(("__setting__", "AUTH_USER_MODEL")) else: dependencies.add(dependency) elif dependency[0] != smigration.app_label or first_migration: dependencies.add(dependency) first_migration = False if no_optimize: if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING("(Skipping optimization.)")) new_operations = operations else: if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Optimizing...")) optimizer = MigrationOptimizer() new_operations = optimizer.optimize(operations, migration.app_label) if self.verbosity > 0: if len(new_operations) == len(operations): self.stdout.write(" No optimizations possible.") else: self.stdout.write( " Optimized from %s operations to %s operations." % (len(operations), len(new_operations))) # Work out the value of replaces (any squashed ones we're re-squashing) # need to feed their replaces into ours replaces = [] for migration in migrations_to_squash: if migration.replaces: replaces.extend(migration.replaces) else: replaces.append((migration.app_label, migration.name)) # Make a new migration with those operations subclass = type( "Migration", (migrations.Migration, ), { "dependencies": dependencies, "operations": new_operations, "replaces": replaces, }) if start_migration_name: if squashed_name: # Use the name from --squashed-name. prefix, _ = start_migration.name.split('_', 1) name = '%s_%s' % (prefix, squashed_name) else: # Generate a name. name = '%s_squashed_%s' % (start_migration.name, migration.name) new_migration = subclass(name, app_label) else: name = '0001_%s' % (squashed_name or 'squashed_%s' % migration.name) new_migration = subclass(name, app_label) new_migration.initial = True # Write out the new migration file writer = MigrationWriter(new_migration, include_header) with open(writer.path, "w", encoding='utf-8') as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING( 'Created new squashed migration %s' % writer.path) + '\n' ' You should commit this migration but leave the old ones in place;\n' ' the new migration will be used for new installs. Once you are sure\n' ' all instances of the codebase have applied the migrations you squashed,\n' ' you can delete them.') if writer.needs_manual_porting: self.stdout.write( self.style.MIGRATE_HEADING('Manual porting required') + '\n' ' Your migrations contained functions that must be manually copied over,\n' ' as we could not safely copy their implementation.\n' ' See the comment at the top of the squashed migration for details.' )
def test_serialize_range(self): string, imports = MigrationWriter.serialize(range(1, 5)) self.assertEqual(string, "range(1, 5)") self.assertEqual(imports, set())
from django.core.files.storage import FileSystemStorage from django.db.migrations.serializer import BaseSerializer from django.db.migrations.writer import MigrationWriter class FileSystemStorageSerializer(BaseSerializer): def serialize(self): return self.value.location, { 'from django.core.files.storage import FileSystemStorage' } MigrationWriter.register_serializer(FileSystemStorage, FileSystemStorageSerializer)
def test_serialize_builtins(self): string, imports = MigrationWriter.serialize(range) self.assertEqual(string, 'range') self.assertEqual(imports, set())
def test_makemigrations(self): field = DynamicField(spec={'a': 'beta'}) statement, imports = MigrationWriter.serialize(field) # 'spec' should not appear since that would trigger needless ALTERs assert statement == "django_mysql.models.DynamicField()"
def test_black_and_isort(self): writer = MigrationWriter(self.get_migration()) output = writer.as_string() expected = textwrap.dedent(""" import datetime from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("testapp", "some_other_one"), ] operations = [ migrations.CreateModel( name="MyModel", fields=[ ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), ( "datetimefield", models.DateTimeField(default=datetime.datetime.utcnow), ), ], options={ "verbose_name": "My model", "verbose_name_plural": "My models", }, ), migrations.CreateModel( name="MyModel2", fields=[ ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), ( "datetimefield", models.DateTimeField(default=datetime.datetime.utcnow), ), ], ), migrations.CreateModel( name="MyModel3", fields=[ ("charfield", models.DateTimeField(default=datetime.datetime.utcnow)), ( "datetimefield", models.DateTimeField(default=datetime.datetime.utcnow), ), ], options={ "verbose_name": "My model", "verbose_name_plural": "My models", }, ), migrations.DeleteModel( name="MyModel", ), migrations.AddField( model_name="OtherModel", name="datetimefield", field=models.DateTimeField(default=datetime.datetime.utcnow), ), ] """ # noqa ) self.assertInOutput(expected, output)
def test_serialize(self): """ Tests various different forms of the serializer. This does not care about formatting, just that the parsed result is correct, so we always exec() the result and check that. """ # Basic values self.assertSerializedEqual(1) self.assertSerializedEqual(None) self.assertSerializedEqual(b"foobar") string, imports = MigrationWriter.serialize(b"foobar") self.assertEqual(string, "b'foobar'") self.assertSerializedEqual("föobár") string, imports = MigrationWriter.serialize("foobar") self.assertEqual(string, "'foobar'") self.assertSerializedEqual({1: 2}) self.assertSerializedEqual(["a", 2, True, None]) self.assertSerializedEqual(set([2, 3, "eighty"])) self.assertSerializedEqual({"lalalala": ["yeah", "no", "maybe"]}) self.assertSerializedEqual(_('Hello')) # Functions with six.assertRaisesRegex(self, ValueError, 'Cannot serialize function: lambda'): self.assertSerializedEqual(lambda x: 42) self.assertSerializedEqual(models.SET_NULL) string, imports = MigrationWriter.serialize(models.SET(42)) self.assertEqual(string, 'models.SET(42)') self.serialize_round_trip(models.SET(42)) # Datetime stuff self.assertSerializedEqual(datetime.datetime.utcnow()) self.assertSerializedEqual(datetime.datetime.utcnow) self.assertSerializedEqual(datetime.datetime.today()) self.assertSerializedEqual(datetime.datetime.today) self.assertSerializedEqual(datetime.date.today()) self.assertSerializedEqual(datetime.date.today) with self.assertRaises(ValueError): self.assertSerializedEqual( datetime.datetime(2012, 1, 1, 1, 1, tzinfo=get_default_timezone())) safe_date = datetime_safe.date(2014, 3, 31) string, imports = MigrationWriter.serialize(safe_date) self.assertEqual(string, repr(datetime.date(2014, 3, 31))) self.assertEqual(imports, {'import datetime'}) safe_datetime = datetime_safe.datetime(2014, 3, 31, 16, 4, 31) string, imports = MigrationWriter.serialize(safe_datetime) self.assertEqual(string, repr(datetime.datetime(2014, 3, 31, 16, 4, 31))) self.assertEqual(imports, {'import datetime'}) # Classes validator = RegexValidator(message="hello") string, imports = MigrationWriter.serialize(validator) self.assertEqual( string, "django.core.validators.RegexValidator(message='hello')") self.serialize_round_trip(validator) validator = EmailValidator(message="hello") # Test with a subclass. string, imports = MigrationWriter.serialize(validator) self.assertEqual( string, "django.core.validators.EmailValidator(message='hello')") self.serialize_round_trip(validator) validator = deconstructible( path="custom.EmailValidator")(EmailValidator)(message="hello") string, imports = MigrationWriter.serialize(validator) self.assertEqual(string, "custom.EmailValidator(message='hello')") # Django fields self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.TextField(null=True, blank=True)) # Setting references self.assertSerializedEqual( SettingsReference(settings.AUTH_USER_MODEL, "AUTH_USER_MODEL")) self.assertSerializedResultEqual( SettingsReference("someapp.model", "AUTH_USER_MODEL"), ( "settings.AUTH_USER_MODEL", set(["from django.conf import settings"]), )) self.assertSerializedResultEqual(((x, x * x) for x in range(3)), ( "((0, 0), (1, 1), (2, 4))", set(), ))
def test_register_non_serializer(self): with self.assertRaisesMessage( ValueError, "'TestModel1' must inherit from 'BaseSerializer'." ): MigrationWriter.register_serializer(complex, TestModel1)
def test_serialize_type_model(self): self.assertSerializedEqual(models.Model) self.assertSerializedResultEqual( MigrationWriter.serialize(models.Model), ("('models.Model', {'from django.db import models'})", set()), )
def test_migration_serialization(): serialized = "djmoney.money.Money(100, 'GBP')" assert MigrationWriter.serialize(Money( 100, "GBP")) == (serialized, {"import djmoney.money"})
def test_serialize_class_based_validators(self): """ Ticket #22943: Test serialization of class-based validators, including compiled regexes. """ validator = RegexValidator(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual( string, "django.core.validators.RegexValidator(message='hello')") self.serialize_round_trip(validator) # Test with a compiled regex. validator = RegexValidator(regex=re.compile(r'^\w+$')) string = MigrationWriter.serialize(validator)[0] self.assertEqual( string, "django.core.validators.RegexValidator(regex=re.compile('^\\\\w+$'))" ) self.serialize_round_trip(validator) # Test a string regex with flag validator = RegexValidator(r'^[0-9]+$', flags=re.S) string = MigrationWriter.serialize(validator)[0] if PY36: self.assertEqual( string, "django.core.validators.RegexValidator('^[0-9]+$', flags=re.RegexFlag(16))" ) else: self.assertEqual( string, "django.core.validators.RegexValidator('^[0-9]+$', flags=16)") self.serialize_round_trip(validator) # Test message and code validator = RegexValidator('^[-a-zA-Z0-9_]+$', 'Invalid', 'invalid') string = MigrationWriter.serialize(validator)[0] self.assertEqual( string, "django.core.validators.RegexValidator('^[-a-zA-Z0-9_]+$', 'Invalid', 'invalid')" ) self.serialize_round_trip(validator) # Test with a subclass. validator = EmailValidator(message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual( string, "django.core.validators.EmailValidator(message='hello')") self.serialize_round_trip(validator) validator = deconstructible( path="migrations.test_writer.EmailValidator")(EmailValidator)( message="hello") string = MigrationWriter.serialize(validator)[0] self.assertEqual( string, "migrations.test_writer.EmailValidator(message='hello')") validator = deconstructible( path="custom.EmailValidator")(EmailValidator)(message="hello") with self.assertRaisesMessage(ImportError, "No module named 'custom'"): MigrationWriter.serialize(validator) validator = deconstructible( path="django.core.validators.EmailValidator2")(EmailValidator)( message="hello") with self.assertRaisesMessage( ValueError, "Could not find object EmailValidator2 in django.core.validators." ): MigrationWriter.serialize(validator)
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={"ask_merge": True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = [ mig for mig in loader.graph.forwards_plan((app_label, migration_name)) if mig[0] == migration.app_label ] merge_migrations.append(migration) def all_items_equal(seq): return all(item == seq[0] for item in seq[1:]) merge_migrations_generations = zip(*(m.ancestry for m in merge_migrations)) common_ancestor_count = sum( 1 for common_ancestor_generation in takewhile( all_items_equal, merge_migrations_generations)) if not common_ancestor_count: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[common_ancestor_count:] migrations_ops = (loader.get_migration(node_app, node_name).operations for node_app, node_name in migration.branch) migration.merged_operations = sum(migrations_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write( self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s" % operation.describe()) if questioner.ask_merge(app_label): # If they still want to merge it, then write out an empty # file depending on the migrations needing merging. numbers = [ MigrationAutodetector.parse_number(migration.name) for migration in merge_migrations ] try: biggest_number = max(x for x in numbers if x is not None) except ValueError: biggest_number = 1 subclass = type( "Migration", (Migration, ), { "dependencies": [(app_label, migration.name) for migration in merge_migrations], }, ) migration_name = "%04i_%s" % ( biggest_number + 1, self.migration_name or ("merge_%s" % get_migration_name_timestamp()), ) new_migration = subclass(migration_name, app_label) writer = MigrationWriter(new_migration, self.include_header) if not self.dry_run: # Write the merge migrations file to the disk with open(writer.path, "w", encoding="utf-8") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path) elif self.verbosity == 3: # Alternatively, makemigrations --merge --dry-run --verbosity 3 # will output the merge migrations to stdout rather than saving # the file to the disk. self.stdout.write( self.style.MIGRATE_HEADING( "Full merge migrations file '%s':" % writer.filename)) self.stdout.write(writer.as_string())
def handle(self, **options): self.verbosity = options.get('verbosity') self.interactive = options.get('interactive') app_label, migration_name = options['app_label'], options[ 'migration_name'] # Load the current graph state, check the app and migration they asked for exists executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS]) if app_label not in executor.loader.migrated_apps: raise CommandError( "App '%s' does not have migrations (so squashmigrations on it makes no sense)" % app_label) try: migration = executor.loader.get_migration_by_prefix( app_label, migration_name) except AmbiguityError: raise CommandError( "More than one migration matches '%s' in app '%s'. Please be more specific." % (migration_name, app_label)) except KeyError: raise CommandError( "Cannot find a migration matching '%s' from app '%s'." % (migration_name, app_label)) # Work out the list of predecessor migrations migrations_to_squash = [ executor.loader.get_migration(al, mn) for al, mn in executor.loader.graph.forwards_plan(( migration.app_label, migration.name)) if al == migration.app_label ] # Tell them what we're doing and optionally ask if we should proceed if self.verbosity > 0 or self.interactive: self.stdout.write( self.style.MIGRATE_HEADING( "Will squash the following migrations:")) for migration in migrations_to_squash: self.stdout.write(" - %s" % migration.name) if self.interactive: answer = None while not answer or answer not in "yn": answer = six.moves.input("Do you wish to proceed? [yN] ") if not answer: answer = "n" break else: answer = answer[0].lower() if answer != "y": return # Load the operations from all those migrations and concat together, # along with collecting external dependencies and detecting # double-squashing operations = [] dependencies = set() for smigration in migrations_to_squash: if smigration.replaces: raise CommandError( "You cannot squash squashed migrations! Please transition it to a normal migration first: https://docs.djangoproject.com/en/1.7/topics/migrations/#squashing-migrations" ) operations.extend(smigration.operations) for dependency in smigration.dependencies: if isinstance(dependency, SwappableTuple): if settings.AUTH_USER_MODEL == dependency.setting: dependencies.add(("__setting__", "AUTH_USER_MODEL")) else: dependencies.add(dependency) elif dependency[0] != smigration.app_label: dependencies.add(dependency) if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Optimizing...")) optimizer = MigrationOptimizer() new_operations = optimizer.optimize(operations, migration.app_label) if self.verbosity > 0: if len(new_operations) == len(operations): self.stdout.write(" No optimizations possible.") else: self.stdout.write( " Optimized from %s operations to %s operations." % (len(operations), len(new_operations))) # Work out the value of replaces (any squashed ones we're re-squashing) # need to feed their replaces into ours replaces = [] for migration in migrations_to_squash: if migration.replaces: replaces.extend(migration.replaces) else: replaces.append((migration.app_label, migration.name)) # Make a new migration with those operations subclass = type( "Migration", (migrations.Migration, ), { "dependencies": dependencies, "operations": new_operations, "replaces": replaces, }) new_migration = subclass("0001_squashed_%s" % migration.name, app_label) # Write out the new migration file writer = MigrationWriter(new_migration) with open(writer.path, "wb") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING( "Created new squashed migration %s" % writer.path)) self.stdout.write( " You should commit this migration but leave the old ones in place;" ) self.stdout.write( " the new migration will be used for new installs. Once you are sure" ) self.stdout.write( " all instances of the codebase have applied the migrations you squashed," ) self.stdout.write(" you can delete them.") if writer.needs_manual_porting: self.stdout.write( self.style.MIGRATE_HEADING("Manual porting required")) self.stdout.write( " Your migrations contained functions that must be manually copied over," ) self.stdout.write( " as we could not safely copy their implementation.") self.stdout.write( " See the comment at the top of the squashed migration for details." )
def handle(self, app_label=None, migration_name=None, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') if app_label is None or migration_name is None: self.stderr.write(self.usage_str) sys.exit(1) # Load the current graph state, check the app and migration they asked for exists executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS]) if app_label not in executor.loader.migrated_apps: raise CommandError( "App '%s' does not have migrations (so squashmigrations on it makes no sense)" % app_label) try: migration = executor.loader.get_migration_by_prefix( app_label, migration_name) except AmbiguityError: raise CommandError( "More than one migration matches '%s' in app '%s'. Please be more specific." % (migration_name, app_label)) except KeyError: raise CommandError( "Cannot find a migration matching '%s' from app '%s'." % (migration_name, app_label)) # Work out the list of predecessor migrations migrations_to_squash = [ executor.loader.get_migration(al, mn) for al, mn in executor.loader.graph.forwards_plan(( migration.app_label, migration.name)) if al == migration.app_label ] # Tell them what we're doing and optionally ask if we should proceed if self.verbosity > 0 or self.interactive: self.stdout.write( self.style.MIGRATE_HEADING( "Will squash the following migrations:")) for migration in migrations_to_squash: self.stdout.write(" - %s" % migration.name) if self.interactive: answer = None while not answer or answer not in "yn": answer = six.moves.input("Do you wish to proceed? [yN] ") if not answer: answer = "n" break else: answer = answer[0].lower() if answer != "y": return # Load the operations from all those migrations and concat together operations = [] for smigration in migrations_to_squash: operations.extend(smigration.operations) if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Optimizing...")) optimizer = MigrationOptimizer() new_operations = optimizer.optimize(operations, migration.app_label) if self.verbosity > 0: if len(new_operations) == len(operations): self.stdout.write(" No optimizations possible.") else: self.stdout.write( " Optimized from %s operations to %s operations." % (len(operations), len(new_operations))) # Work out the value of replaces (any squashed ones we're re-squashing) # need to feed their replaces into ours replaces = [] for migration in migrations_to_squash: if migration.replaces: replaces.extend(migration.replaces) else: replaces.append((migration.app_label, migration.name)) # Make a new migration with those operations subclass = type( "Migration", (migrations.Migration, ), { "dependencies": [], "operations": new_operations, "replaces": replaces, }) new_migration = subclass("0001_squashed_%s" % migration.name, app_label) # Write out the new migration file writer = MigrationWriter(new_migration) with open(writer.path, "wb") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING( "Created new squashed migration %s" % writer.path)) self.stdout.write( " You should commit this migration but leave the old ones in place;" ) self.stdout.write( " the new migration will be used for new installs. Once you are sure" ) self.stdout.write( " all instances of the codebase have applied the migrations you squashed," ) self.stdout.write(" you can delete them.")
def write_to_last_migration_files(self, changes): loader = MigrationLoader(connections[DEFAULT_DB_ALIAS]) new_changes = {} update_previous_migration_paths = {} for app_label, app_migrations in changes.items(): # Find last migration. leaf_migration_nodes = loader.graph.leaf_nodes(app=app_label) if len(leaf_migration_nodes) == 0: raise CommandError( f"App {app_label} has no migration, cannot update last migration." ) leaf_migration_node = leaf_migration_nodes[0] # Multiple leaf nodes have already been checked earlier in command. leaf_migration = loader.graph.nodes[leaf_migration_node] # Updated migration cannot be a squash migration, a dependency of # another migration, and cannot be already applied. if leaf_migration.replaces: raise CommandError( f"Cannot update squash migration '{leaf_migration}'." ) if leaf_migration_node in loader.applied_migrations: raise CommandError( f"Cannot update applied migration '{leaf_migration}'." ) depending_migrations = [ migration for migration in loader.disk_migrations.values() if leaf_migration_node in migration.dependencies ] if depending_migrations: formatted_migrations = ", ".join( [f"'{migration}'" for migration in depending_migrations] ) raise CommandError( f"Cannot update migration '{leaf_migration}' that migrations " f"{formatted_migrations} depend on." ) # Build new migration. for migration in app_migrations: leaf_migration.operations.extend(migration.operations) for dependency in migration.dependencies: if isinstance(dependency, SwappableTuple): if settings.AUTH_USER_MODEL == dependency.setting: leaf_migration.dependencies.append( ("__setting__", "AUTH_USER_MODEL") ) else: leaf_migration.dependencies.append(dependency) elif dependency[0] != migration.app_label: leaf_migration.dependencies.append(dependency) # Optimize migration. optimizer = MigrationOptimizer() leaf_migration.operations = optimizer.optimize( leaf_migration.operations, app_label ) # Update name. previous_migration_path = MigrationWriter(leaf_migration).path suggested_name = ( leaf_migration.name[:4] + "_" + leaf_migration.suggest_name() ) if leaf_migration.name == suggested_name: new_name = leaf_migration.name + "_updated" else: new_name = suggested_name leaf_migration.name = new_name # Register overridden migration. new_changes[app_label] = [leaf_migration] update_previous_migration_paths[app_label] = previous_migration_path self.write_migration_files(new_changes, update_previous_migration_paths)
def test_serialize_enums(self): self.assertSerializedResultEqual( TextEnum.A, ("migrations.test_writer.TextEnum['A']", {"import migrations.test_writer"}), ) self.assertSerializedResultEqual( TextTranslatedEnum.A, ( "migrations.test_writer.TextTranslatedEnum['A']", {"import migrations.test_writer"}, ), ) self.assertSerializedResultEqual( BinaryEnum.A, ( "migrations.test_writer.BinaryEnum['A']", {"import migrations.test_writer"}, ), ) self.assertSerializedResultEqual( IntEnum.B, ("migrations.test_writer.IntEnum['B']", {"import migrations.test_writer"}), ) self.assertSerializedResultEqual( self.NestedEnum.A, ( "migrations.test_writer.WriterTests.NestedEnum['A']", {"import migrations.test_writer"}, ), ) self.assertSerializedEqual(self.NestedEnum.A) field = models.CharField( default=TextEnum.B, choices=[(m.value, m) for m in TextEnum] ) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.CharField(choices=[" "('a-value', migrations.test_writer.TextEnum['A']), " "('value-b', migrations.test_writer.TextEnum['B'])], " "default=migrations.test_writer.TextEnum['B'])", ) field = models.CharField( default=TextTranslatedEnum.A, choices=[(m.value, m) for m in TextTranslatedEnum], ) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.CharField(choices=[" "('a-value', migrations.test_writer.TextTranslatedEnum['A']), " "('value-b', migrations.test_writer.TextTranslatedEnum['B'])], " "default=migrations.test_writer.TextTranslatedEnum['A'])", ) field = models.CharField( default=BinaryEnum.B, choices=[(m.value, m) for m in BinaryEnum] ) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.CharField(choices=[" "(b'a-value', migrations.test_writer.BinaryEnum['A']), " "(b'value-b', migrations.test_writer.BinaryEnum['B'])], " "default=migrations.test_writer.BinaryEnum['B'])", ) field = models.IntegerField( default=IntEnum.A, choices=[(m.value, m) for m in IntEnum] ) string = MigrationWriter.serialize(field)[0] self.assertEqual( string, "models.IntegerField(choices=[" "(1, migrations.test_writer.IntEnum['A']), " "(2, migrations.test_writer.IntEnum['B'])], " "default=migrations.test_writer.IntEnum['A'])", )
def write_migration_files(self, changes, update_previous_migration_paths=None): """ Take a changes dict and write them out as migration files. """ directory_created = {} for app_label, app_migrations in changes.items(): if self.verbosity >= 1: self.log(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label)) for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration, self.include_header) if self.verbosity >= 1: # Display a relative path if it's below the current working # directory, or an absolute path otherwise. migration_string = self.get_relative_path(writer.path) self.log(" %s\n" % self.style.MIGRATE_LABEL(migration_string)) for operation in migration.operations: self.log(" - %s" % operation.describe()) if self.scriptable: self.stdout.write(migration_string) if not self.dry_run: # Write the migrations file to the disk. migrations_directory = os.path.dirname(writer.path) if not directory_created.get(app_label): os.makedirs(migrations_directory, exist_ok=True) init_path = os.path.join(migrations_directory, "__init__.py") if not os.path.isfile(init_path): open(init_path, "w").close() # We just do this once per app directory_created[app_label] = True migration_string = writer.as_string() with open(writer.path, "w", encoding="utf-8") as fh: fh.write(migration_string) self.written_files.append(writer.path) if update_previous_migration_paths: prev_path = update_previous_migration_paths[app_label] rel_prev_path = self.get_relative_path(prev_path) if writer.needs_manual_porting: migration_path = self.get_relative_path(writer.path) self.log( self.style.WARNING( f"Updated migration {migration_path} requires " f"manual porting.\n" f"Previous migration {rel_prev_path} was kept and " f"must be deleted after porting functions manually." ) ) else: os.remove(prev_path) self.log(f"Deleted {rel_prev_path}") elif self.verbosity == 3: # Alternatively, makemigrations --dry-run --verbosity 3 # will log the migrations rather than saving the file to # the disk. self.log( self.style.MIGRATE_HEADING( "Full migrations file '%s':" % writer.filename ) ) self.log(writer.as_string()) run_formatters(self.written_files)
"false").lower() == "true") # Rendre certains warnings silencieux SILENCED_SYSTEM_CHECKS = [ # On a remplacé django.contrib.auth.context_processors.auth par un équivalent, agir.authentication.context_processors.auth "admin.E402", # social_django utilise encore des champs postgres JSON (au lieu du nouveau JSONField de Django) "fields.W904", ] # Django ne sait pas par défaut sérializer les types intervalle de psycopg2 # Cela fait du coup planter `makemigrate` quand il tente de séralizer les valeurs # par défaut des champs de dates pour les modèles de mandats (dans l'appli élus). # Il faut enregistrer manuellement le serializer correspondant auprès du générateur # de migrations. MigrationWriter.register_serializer(DateRange, RangeSerializer) # Django < 3.1 not compatible with GDAL 3 if os.environ.get("GDAL_LIBRARY_PATH"): GDAL_LIBRARY_PATH = os.environ.get("GDAL_LIBRARY_PATH") ENABLE_API = os.environ.get("ENABLE_API", "n").lower() in YES_VALUES or DEBUG ENABLE_ADMIN = os.environ.get("ENABLE_ADMIN", "n").lower() in YES_VALUES or DEBUG ENABLE_FRONT = os.environ.get("ENABLE_FRONT", "n").lower() in YES_VALUES or DEBUG # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEST_RUNNER = "agir.api.test_runner.TestRunner"
def serialize_round_trip(self, value): string, imports = MigrationWriter.serialize(value) return self.safe_exec( "%s\ntest_value_result = %s" % ("\n".join(imports), string), value)['test_value_result']
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner() for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = loader.graph.forwards_plan( (app_label, migration_name)) merge_migrations.append(migration) common_ancestor = None for level in zip(*[m.ancestry for m in merge_migrations]): if reduce(operator.eq, level): common_ancestor = level[0] else: break if common_ancestor is None: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[( migration.ancestry.index(common_ancestor) + 1):] migration.merged_operations = [] for node_app, node_name in migration.branch: migration.merged_operations.extend( loader.get_migration(node_app, node_name).operations) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write( self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s\n" % operation.describe()) if questioner.ask_merge(app_label): # If they still want to merge it, then write out an empty # file depending on the migrations needing merging. numbers = [ MigrationAutodetector.parse_number(migration.name) for migration in merge_migrations ] try: biggest_number = max([x for x in numbers if x is not None]) except ValueError: biggest_number = 1 subclass = type( "Migration", (migrations.Migration, ), { "dependencies": [(app_label, migration.name) for migration in merge_migrations], }) new_migration = subclass("%04i_merge" % (biggest_number + 1), app_label) writer = MigrationWriter(new_migration) with open(writer.path, "wb") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path)