From 27a3eee72170f1eb994a213db985f42c6cf5f994 Mon Sep 17 00:00:00 2001 From: Amir Karimi Date: Sat, 16 Sep 2023 05:41:22 +0330 Subject: [PATCH] Fixed #31700 -- Made makemigrations command display meaningful symbols for each operation. --- django/contrib/postgres/operations.py | 13 +++- .../management/commands/makemigrations.py | 4 +- django/db/migrations/operations/base.py | 20 ++++++ django/db/migrations/operations/fields.py | 10 ++- django/db/migrations/operations/models.py | 17 ++++- django/db/migrations/operations/special.py | 5 +- docs/intro/tutorial02.txt | 4 +- docs/ref/contrib/gis/tutorial.txt | 2 +- docs/ref/migration-operations.txt | 42 ++++++++++- docs/releases/5.1.txt | 9 ++- docs/topics/migrations.txt | 2 +- tests/migrations/test_commands.py | 12 ++-- tests/migrations/test_operations.py | 72 +++++++++++++++++++ tests/postgres_tests/test_operations.py | 21 ++++++ 14 files changed, 214 insertions(+), 19 deletions(-) diff --git a/django/contrib/postgres/operations.py b/django/contrib/postgres/operations.py index 5ac396bedf3..1ee5fbc2e2c 100644 --- a/django/contrib/postgres/operations.py +++ b/django/contrib/postgres/operations.py @@ -5,12 +5,13 @@ from django.contrib.postgres.signals import ( ) from django.db import NotSupportedError, router from django.db.migrations import AddConstraint, AddIndex, RemoveIndex -from django.db.migrations.operations.base import Operation +from django.db.migrations.operations.base import Operation, OperationCategory from django.db.models.constraints import CheckConstraint class CreateExtension(Operation): reversible = True + category = OperationCategory.ADDITION def __init__(self, name): self.name = name @@ -120,6 +121,7 @@ class AddIndexConcurrently(NotInTransactionMixin, AddIndex): """Create an index using PostgreSQL's CREATE INDEX CONCURRENTLY syntax.""" atomic = False + category = OperationCategory.ADDITION def describe(self): return "Concurrently create index %s on field(s) %s of model %s" % ( @@ -145,6 +147,7 @@ class RemoveIndexConcurrently(NotInTransactionMixin, RemoveIndex): """Remove an index using PostgreSQL's DROP INDEX CONCURRENTLY syntax.""" atomic = False + category = OperationCategory.REMOVAL def describe(self): return "Concurrently remove index %s from %s" % (self.name, self.model_name) @@ -213,6 +216,8 @@ class CollationOperation(Operation): class CreateCollation(CollationOperation): """Create a collation.""" + category = OperationCategory.ADDITION + def database_forwards(self, app_label, schema_editor, from_state, to_state): if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate( schema_editor.connection.alias, app_label @@ -236,6 +241,8 @@ class CreateCollation(CollationOperation): class RemoveCollation(CollationOperation): """Remove a collation.""" + category = OperationCategory.REMOVAL + def database_forwards(self, app_label, schema_editor, from_state, to_state): if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate( schema_editor.connection.alias, app_label @@ -262,6 +269,8 @@ class AddConstraintNotValid(AddConstraint): NOT VALID syntax. """ + category = OperationCategory.ADDITION + def __init__(self, model_name, constraint): if not isinstance(constraint, CheckConstraint): raise TypeError( @@ -293,6 +302,8 @@ class AddConstraintNotValid(AddConstraint): class ValidateConstraint(Operation): """Validate a table NOT VALID constraint.""" + category = OperationCategory.ALTERATION + def __init__(self, model_name, name): self.model_name = model_name self.name = name diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 22498af3c07..a4e4d520e61 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -348,7 +348,7 @@ class Command(BaseCommand): 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()) + self.log(" %s" % operation.formatted_description()) if self.scriptable: self.stdout.write(migration_string) if not self.dry_run: @@ -456,7 +456,7 @@ class Command(BaseCommand): for migration in merge_migrations: self.log(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: - self.log(" - %s" % operation.describe()) + self.log(" %s" % operation.formatted_description()) 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. diff --git a/django/db/migrations/operations/base.py b/django/db/migrations/operations/base.py index 7d4dff25974..3bd9546bd72 100644 --- a/django/db/migrations/operations/base.py +++ b/django/db/migrations/operations/base.py @@ -1,6 +1,17 @@ +import enum + from django.db import router +class OperationCategory(str, enum.Enum): + ADDITION = "+" + REMOVAL = "-" + ALTERATION = "~" + PYTHON = "p" + SQL = "s" + MIXED = "?" + + class Operation: """ Base class for migration operations. @@ -33,6 +44,8 @@ class Operation: serialization_expand_args = [] + category = None + def __new__(cls, *args, **kwargs): # We capture the arguments to make returning them trivial self = object.__new__(cls) @@ -85,6 +98,13 @@ class Operation: """ return "%s: %s" % (self.__class__.__name__, self._constructor_args) + def formatted_description(self): + """Output a description prefixed by a category symbol.""" + description = self.describe() + if self.category is None: + return f"{OperationCategory.MIXED.value} {description}" + return f"{self.category.value} {description}" + @property def migration_name_fragment(self): """ diff --git a/django/db/migrations/operations/fields.py b/django/db/migrations/operations/fields.py index fc5640bea99..34b441a2472 100644 --- a/django/db/migrations/operations/fields.py +++ b/django/db/migrations/operations/fields.py @@ -2,7 +2,7 @@ from django.db.migrations.utils import field_references from django.db.models import NOT_PROVIDED from django.utils.functional import cached_property -from .base import Operation +from .base import Operation, OperationCategory class FieldOperation(Operation): @@ -75,6 +75,8 @@ class FieldOperation(Operation): class AddField(FieldOperation): """Add a field to a model.""" + category = OperationCategory.ADDITION + def __init__(self, model_name, name, field, preserve_default=True): self.preserve_default = preserve_default super().__init__(model_name, name, field) @@ -154,6 +156,8 @@ class AddField(FieldOperation): class RemoveField(FieldOperation): """Remove a field from a model.""" + category = OperationCategory.REMOVAL + def deconstruct(self): kwargs = { "model_name": self.model_name, @@ -201,6 +205,8 @@ class AlterField(FieldOperation): new field. """ + category = OperationCategory.ALTERATION + def __init__(self, model_name, name, field, preserve_default=True): self.preserve_default = preserve_default super().__init__(model_name, name, field) @@ -270,6 +276,8 @@ class AlterField(FieldOperation): class RenameField(FieldOperation): """Rename a field on the model. Might affect db_column too.""" + category = OperationCategory.ALTERATION + def __init__(self, model_name, old_name, new_name): self.old_name = old_name self.new_name = new_name diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index df153da7e5a..b24a8f6557b 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.db.migrations.operations.base import Operation +from django.db.migrations.operations.base import Operation, OperationCategory from django.db.migrations.state import ModelState from django.db.migrations.utils import field_references, resolve_relation from django.db.models.options import normalize_together @@ -41,6 +41,7 @@ class ModelOperation(Operation): class CreateModel(ModelOperation): """Create a model's table.""" + category = OperationCategory.ADDITION serialization_expand_args = ["fields", "options", "managers"] def __init__(self, name, fields, options=None, bases=None, managers=None): @@ -347,6 +348,8 @@ class CreateModel(ModelOperation): class DeleteModel(ModelOperation): """Drop a model's table.""" + category = OperationCategory.REMOVAL + def deconstruct(self): kwargs = { "name": self.name, @@ -382,6 +385,8 @@ class DeleteModel(ModelOperation): class RenameModel(ModelOperation): """Rename a model.""" + category = OperationCategory.ALTERATION + def __init__(self, old_name, new_name): self.old_name = old_name self.new_name = new_name @@ -499,6 +504,8 @@ class RenameModel(ModelOperation): class ModelOptionOperation(ModelOperation): + category = OperationCategory.ALTERATION + def reduce(self, operation, app_label): if ( isinstance(operation, (self.__class__, DeleteModel)) @@ -849,6 +856,8 @@ class IndexOperation(Operation): class AddIndex(IndexOperation): """Add an index on a model.""" + category = OperationCategory.ADDITION + def __init__(self, model_name, index): self.model_name = model_name if not index.name: @@ -911,6 +920,8 @@ class AddIndex(IndexOperation): class RemoveIndex(IndexOperation): """Remove an index from a model.""" + category = OperationCategory.REMOVAL + def __init__(self, model_name, name): self.model_name = model_name self.name = name @@ -954,6 +965,8 @@ class RemoveIndex(IndexOperation): class RenameIndex(IndexOperation): """Rename an index.""" + category = OperationCategory.ALTERATION + def __init__(self, model_name, new_name, old_name=None, old_fields=None): if not old_name and not old_fields: raise ValueError( @@ -1104,6 +1117,7 @@ class RenameIndex(IndexOperation): class AddConstraint(IndexOperation): + category = OperationCategory.ADDITION option_name = "constraints" def __init__(self, model_name, constraint): @@ -1154,6 +1168,7 @@ class AddConstraint(IndexOperation): class RemoveConstraint(IndexOperation): + category = OperationCategory.REMOVAL option_name = "constraints" def __init__(self, model_name, name): diff --git a/django/db/migrations/operations/special.py b/django/db/migrations/operations/special.py index 94a6ec72de9..196f24fcd67 100644 --- a/django/db/migrations/operations/special.py +++ b/django/db/migrations/operations/special.py @@ -1,6 +1,6 @@ from django.db import router -from .base import Operation +from .base import Operation, OperationCategory class SeparateDatabaseAndState(Operation): @@ -11,6 +11,7 @@ class SeparateDatabaseAndState(Operation): that affect the state or not the database, or so on. """ + category = OperationCategory.MIXED serialization_expand_args = ["database_operations", "state_operations"] def __init__(self, database_operations=None, state_operations=None): @@ -68,6 +69,7 @@ class RunSQL(Operation): by this SQL change, in case it's custom column/table creation/deletion. """ + category = OperationCategory.SQL noop = "" def __init__( @@ -138,6 +140,7 @@ class RunPython(Operation): Run Python code in a context suitable for doing versioned ORM operations. """ + category = OperationCategory.PYTHON reduces_to_sql = False def __init__( diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 6ba70ddc1c1..d558a3eb1dc 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -241,8 +241,8 @@ You should see something similar to the following: Migrations for 'polls': polls/migrations/0001_initial.py - - Create model Question - - Create model Choice + + Create model Question + + Create model Choice By running ``makemigrations``, you're telling Django that you've made some changes to your models (in this case, you've made new ones) and that diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index eb62df56a86..53c961561cd 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -241,7 +241,7 @@ create a database migration: $ python manage.py makemigrations Migrations for 'world': world/migrations/0001_initial.py: - - Create model WorldBorder + + Create model WorldBorder Let's look at the SQL that will generate the table for the ``WorldBorder`` model: diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index e8d86308516..3c39d27873b 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -475,6 +475,42 @@ operations. For an example using ``SeparateDatabaseAndState``, see :ref:`changing-a-manytomanyfield-to-use-a-through-model`. +Operation category +================== + +.. versionadded:: 5.1 + +.. currentmodule:: django.db.migrations.operations.base + +.. class:: OperationCategory + + Categories of migration operation used by the :djadmin:`makemigrations` + command to display meaningful symbols. + + .. attribute:: ADDITION + + *Symbol*: ``+`` + + .. attribute:: REMOVAL + + *Symbol*: ``-`` + + .. attribute:: ALTERATION + + *Symbol*: ``~`` + + .. attribute:: PYTHON + + *Symbol*: ``p`` + + .. attribute:: SQL + + *Symbol*: ``s`` + + .. attribute:: MIXED + + *Symbol*: ``?`` + .. _writing-your-own-migration-operation: Writing your own @@ -495,6 +531,10 @@ structure of an ``Operation`` looks like this:: # If this is False, Django will refuse to reverse past this operation. reversible = False + # This categorizes the operation. The corresponding symbol will be + # display by the makemigrations command. + category = OperationCategory.ADDITION + def __init__(self, arg1, arg2): # Operations are usually instantiated with arguments in migration # files. Store the values of them on self for later use. @@ -516,7 +556,7 @@ structure of an ``Operation`` looks like this:: pass def describe(self): - # This is used to describe what the operation does in console output. + # This is used to describe what the operation does. return "Custom Operation" @property diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 1f7d26d7b82..f8393303be2 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -178,12 +178,17 @@ Logging Management Commands ~~~~~~~~~~~~~~~~~~~ -* ... +* :djadmin:`makemigrations` command now displays meaningful symbols for each + operation to highlight :class:`operation categories + `. Migrations ~~~~~~~~~~ -* ... +* The new ``Operation.category`` attribute allows specifying an + :class:`operation category + ` used by the + :djadmin:`makemigrations` to display a meaningful symbol for the operation. Models ~~~~~~ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index 9e339852b67..5fb5019c1ec 100644 --- a/docs/topics/migrations.txt +++ b/docs/topics/migrations.txt @@ -118,7 +118,7 @@ field and remove a model - and then run :djadmin:`makemigrations`: $ python manage.py makemigrations Migrations for 'books': books/migrations/0003_auto.py: - - Alter field author on book + ~ Alter field author on book Your models will be scanned and compared to the versions currently contained in your migration files, and then a new set of migrations diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index a9c1cdf8938..263b25ab614 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -2141,7 +2141,7 @@ class MakeMigrationsTests(MigrationTestBase): ) # Normal --dry-run output - self.assertIn("- Add field silly_char to sillymodel", out.getvalue()) + self.assertIn("+ Add field silly_char to sillymodel", out.getvalue()) # Additional output caused by verbosity 3 # The complete migrations file that would be written @@ -2171,7 +2171,7 @@ class MakeMigrationsTests(MigrationTestBase): ) initial_file = os.path.join(migration_dir, "0001_initial.py") self.assertEqual(out.getvalue(), f"{initial_file}\n") - self.assertIn(" - Create model ModelWithCustomBase\n", err.getvalue()) + self.assertIn(" + Create model ModelWithCustomBase\n", err.getvalue()) @mock.patch("builtins.input", return_value="Y") def test_makemigrations_scriptable_merge(self, mock_input): @@ -2216,7 +2216,7 @@ class MakeMigrationsTests(MigrationTestBase): self.assertTrue(os.path.exists(initial_file)) # Command output indicates the migration is created. - self.assertIn(" - Create model SillyModel", out.getvalue()) + self.assertIn(" + Create model SillyModel", out.getvalue()) @override_settings(MIGRATION_MODULES={"migrations": "some.nonexistent.path"}) def test_makemigrations_migrations_modules_nonexistent_toplevel_package(self): @@ -2321,12 +2321,12 @@ class MakeMigrationsTests(MigrationTestBase): out.getvalue().lower(), "merging conflicting_app_with_dependencies\n" " branch 0002_conflicting_second\n" - " - create model something\n" + " + create model something\n" " branch 0002_second\n" " - delete model tribble\n" " - remove field silly_field from author\n" - " - add field rating to author\n" - " - create model book\n" + " + add field rating to author\n" + " + create model book\n" "\n" "merging will only work if the operations printed above do not " "conflict\n" diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 2d9d3a38f05..52e43d20f90 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -4,6 +4,7 @@ from decimal import Decimal from django.core.exceptions import FieldDoesNotExist from django.db import IntegrityError, connection, migrations, models, transaction from django.db.migrations.migration import Migration +from django.db.migrations.operations.base import Operation from django.db.migrations.operations.fields import FieldOperation from django.db.migrations.state import ModelState, ProjectState from django.db.models import F @@ -47,6 +48,7 @@ class OperationTests(OperationTestBase): ], ) self.assertEqual(operation.describe(), "Create model Pony") + self.assertEqual(operation.formatted_description(), "+ Create model Pony") self.assertEqual(operation.migration_name_fragment, "pony") # Test the state alteration project_state = ProjectState() @@ -710,6 +712,7 @@ class OperationTests(OperationTestBase): # Test the state alteration operation = migrations.DeleteModel("Pony") self.assertEqual(operation.describe(), "Delete model Pony") + self.assertEqual(operation.formatted_description(), "- Delete model Pony") self.assertEqual(operation.migration_name_fragment, "delete_pony") new_state = project_state.clone() operation.state_forwards("test_dlmo", new_state) @@ -790,6 +793,9 @@ class OperationTests(OperationTestBase): # Test the state alteration operation = migrations.RenameModel("Pony", "Horse") self.assertEqual(operation.describe(), "Rename model Pony to Horse") + self.assertEqual( + operation.formatted_description(), "~ Rename model Pony to Horse" + ) self.assertEqual(operation.migration_name_fragment, "rename_pony_horse") # Test initial state and database self.assertIn(("test_rnmo", "pony"), project_state.models) @@ -1350,6 +1356,9 @@ class OperationTests(OperationTestBase): models.FloatField(null=True, default=5), ) self.assertEqual(operation.describe(), "Add field height to Pony") + self.assertEqual( + operation.formatted_description(), "+ Add field height to Pony" + ) self.assertEqual(operation.migration_name_fragment, "pony_height") project_state, new_state = self.make_test_state("test_adfl", operation) self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 6) @@ -1906,6 +1915,9 @@ class OperationTests(OperationTestBase): # Test the state alteration operation = migrations.RemoveField("Pony", "pink") self.assertEqual(operation.describe(), "Remove field pink from Pony") + self.assertEqual( + operation.formatted_description(), "- Remove field pink from Pony" + ) self.assertEqual(operation.migration_name_fragment, "remove_pony_pink") new_state = project_state.clone() operation.state_forwards("test_rmfl", new_state) @@ -1952,6 +1964,10 @@ class OperationTests(OperationTestBase): self.assertEqual( operation.describe(), "Rename table for Pony to test_almota_pony_2" ) + self.assertEqual( + operation.formatted_description(), + "~ Rename table for Pony to test_almota_pony_2", + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_table") new_state = project_state.clone() operation.state_forwards("test_almota", new_state) @@ -2093,6 +2109,9 @@ class OperationTests(OperationTestBase): "Pony", "pink", models.IntegerField(null=True) ) self.assertEqual(operation.describe(), "Alter field pink on Pony") + self.assertEqual( + operation.formatted_description(), "~ Alter field pink on Pony" + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_pink") new_state = project_state.clone() operation.state_forwards("test_alfl", new_state) @@ -2403,6 +2422,9 @@ class OperationTests(OperationTestBase): # Add table comment. operation = migrations.AlterModelTableComment("Pony", "Custom pony comment") self.assertEqual(operation.describe(), "Alter Pony table comment") + self.assertEqual( + operation.formatted_description(), "~ Alter Pony table comment" + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_table_comment") new_state = project_state.clone() operation.state_forwards(app_label, new_state) @@ -3073,6 +3095,9 @@ class OperationTests(OperationTestBase): project_state = self.set_up_test_model("test_rnfl") operation = migrations.RenameField("Pony", "pink", "blue") self.assertEqual(operation.describe(), "Rename field pink on Pony to blue") + self.assertEqual( + operation.formatted_description(), "~ Rename field pink on Pony to blue" + ) self.assertEqual(operation.migration_name_fragment, "rename_pink_pony_blue") new_state = project_state.clone() operation.state_forwards("test_rnfl", new_state) @@ -3326,6 +3351,10 @@ class OperationTests(OperationTestBase): self.assertEqual( operation.describe(), "Alter unique_together for Pony (1 constraint(s))" ) + self.assertEqual( + operation.formatted_description(), + "~ Alter unique_together for Pony (1 constraint(s))", + ) self.assertEqual( operation.migration_name_fragment, "alter_pony_unique_together", @@ -3478,6 +3507,10 @@ class OperationTests(OperationTestBase): operation.describe(), "Create index test_adin_pony_pink_idx on field(s) pink of model Pony", ) + self.assertEqual( + operation.formatted_description(), + "+ Create index test_adin_pony_pink_idx on field(s) pink of model Pony", + ) self.assertEqual( operation.migration_name_fragment, "pony_test_adin_pony_pink_idx", @@ -3511,6 +3544,9 @@ class OperationTests(OperationTestBase): self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) operation = migrations.RemoveIndex("Pony", "pony_test_idx") self.assertEqual(operation.describe(), "Remove index pony_test_idx from Pony") + self.assertEqual( + operation.formatted_description(), "- Remove index pony_test_idx from Pony" + ) self.assertEqual( operation.migration_name_fragment, "remove_pony_pony_test_idx", @@ -3565,6 +3601,10 @@ class OperationTests(OperationTestBase): operation.describe(), "Rename index pony_pink_idx on Pony to new_pony_test_idx", ) + self.assertEqual( + operation.formatted_description(), + "~ Rename index pony_pink_idx on Pony to new_pony_test_idx", + ) self.assertEqual( operation.migration_name_fragment, "rename_pony_pink_idx_new_pony_test_idx", @@ -3807,6 +3847,10 @@ class OperationTests(OperationTestBase): self.assertEqual( operation.describe(), "Alter index_together for Pony (0 constraint(s))" ) + self.assertEqual( + operation.formatted_description(), + "~ Alter index_together for Pony (0 constraint(s))", + ) def test_add_constraint(self): project_state = self.set_up_test_model("test_addconstraint") @@ -3819,6 +3863,10 @@ class OperationTests(OperationTestBase): gt_operation.describe(), "Create constraint test_add_constraint_pony_pink_gt_2 on model Pony", ) + self.assertEqual( + gt_operation.formatted_description(), + "+ Create constraint test_add_constraint_pony_pink_gt_2 on model Pony", + ) self.assertEqual( gt_operation.migration_name_fragment, "pony_test_add_constraint_pony_pink_gt_2", @@ -4024,6 +4072,10 @@ class OperationTests(OperationTestBase): gt_operation.describe(), "Remove constraint test_remove_constraint_pony_pink_gt_2 from model Pony", ) + self.assertEqual( + gt_operation.formatted_description(), + "- Remove constraint test_remove_constraint_pony_pink_gt_2 from model Pony", + ) self.assertEqual( gt_operation.migration_name_fragment, "remove_pony_test_remove_constraint_pony_pink_gt_2", @@ -4564,6 +4616,9 @@ class OperationTests(OperationTestBase): "Pony", {"permissions": [("can_groom", "Can groom")]} ) self.assertEqual(operation.describe(), "Change Meta options on Pony") + self.assertEqual( + operation.formatted_description(), "~ Change Meta options on Pony" + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_options") new_state = project_state.clone() operation.state_forwards("test_almoop", new_state) @@ -4630,6 +4685,10 @@ class OperationTests(OperationTestBase): self.assertEqual( operation.describe(), "Set order_with_respect_to on Rider to pony" ) + self.assertEqual( + operation.formatted_description(), + "~ Set order_with_respect_to on Rider to pony", + ) self.assertEqual( operation.migration_name_fragment, "alter_rider_order_with_respect_to", @@ -4705,6 +4764,7 @@ class OperationTests(OperationTestBase): ], ) self.assertEqual(operation.describe(), "Change managers on Pony") + self.assertEqual(operation.formatted_description(), "~ Change managers on Pony") self.assertEqual(operation.migration_name_fragment, "alter_pony_managers") managers = project_state.models["test_almoma", "pony"].managers self.assertEqual(managers, []) @@ -4840,6 +4900,7 @@ class OperationTests(OperationTestBase): ], ) self.assertEqual(operation.describe(), "Raw SQL operation") + self.assertEqual(operation.formatted_description(), "s Raw SQL operation") # Test the state alteration new_state = project_state.clone() operation.state_forwards("test_runsql", new_state) @@ -5034,6 +5095,7 @@ class OperationTests(OperationTestBase): inner_method, reverse_code=inner_method_reverse ) self.assertEqual(operation.describe(), "Raw Python operation") + self.assertEqual(operation.formatted_description(), "p Raw Python operation") # Test the state alteration does nothing new_state = project_state.clone() operation.state_forwards("test_runpython", new_state) @@ -5565,6 +5627,10 @@ class OperationTests(OperationTestBase): self.assertEqual( operation.describe(), "Custom state/database change combination" ) + self.assertEqual( + operation.formatted_description(), + "? Custom state/database change combination", + ) # Test the state alteration new_state = project_state.clone() operation.state_forwards("test_separatedatabaseandstate", new_state) @@ -6073,3 +6139,9 @@ class FieldOperationTests(SimpleTestCase): self.assertIs( operation.references_field("Through", "second", "migrations"), True ) + + +class BaseOperationTests(SimpleTestCase): + def test_formatted_description_no_category(self): + operation = Operation() + self.assertEqual(operation.formatted_description(), "? Operation: ((), {})") diff --git a/tests/postgres_tests/test_operations.py b/tests/postgres_tests/test_operations.py index f395198533c..ff344e3cb0c 100644 --- a/tests/postgres_tests/test_operations.py +++ b/tests/postgres_tests/test_operations.py @@ -59,6 +59,10 @@ class AddIndexConcurrentlyTests(OperationTestBase): operation.describe(), "Concurrently create index pony_pink_idx on field(s) pink of model Pony", ) + self.assertEqual( + operation.formatted_description(), + "+ Concurrently create index pony_pink_idx on field(s) pink of model Pony", + ) operation.state_forwards(self.app_label, new_state) self.assertEqual( len(new_state.models[self.app_label, "pony"].options["indexes"]), 1 @@ -154,6 +158,10 @@ class RemoveIndexConcurrentlyTests(OperationTestBase): operation.describe(), "Concurrently remove index pony_pink_idx from Pony", ) + self.assertEqual( + operation.formatted_description(), + "- Concurrently remove index pony_pink_idx from Pony", + ) operation.state_forwards(self.app_label, new_state) self.assertEqual( len(new_state.models[self.app_label, "pony"].options["indexes"]), 0 @@ -190,6 +198,9 @@ class CreateExtensionTests(PostgreSQLTestCase): @override_settings(DATABASE_ROUTERS=[NoMigrationRouter()]) def test_no_allow_migrate(self): operation = CreateExtension("tablefunc") + self.assertEqual( + operation.formatted_description(), "+ Creates extension tablefunc" + ) project_state = ProjectState() new_state = project_state.clone() # Don't create an extension. @@ -287,6 +298,7 @@ class CreateCollationTests(PostgreSQLTestCase): operation = CreateCollation("C_test", locale="C") self.assertEqual(operation.migration_name_fragment, "create_collation_c_test") self.assertEqual(operation.describe(), "Create collation C_test") + self.assertEqual(operation.formatted_description(), "+ Create collation C_test") project_state = ProjectState() new_state = project_state.clone() # Create a collation. @@ -418,6 +430,7 @@ class RemoveCollationTests(PostgreSQLTestCase): operation = RemoveCollation("C_test", locale="C") self.assertEqual(operation.migration_name_fragment, "remove_collation_c_test") self.assertEqual(operation.describe(), "Remove collation C_test") + self.assertEqual(operation.formatted_description(), "- Remove collation C_test") project_state = ProjectState() new_state = project_state.clone() # Remove a collation. @@ -470,6 +483,10 @@ class AddConstraintNotValidTests(OperationTestBase): operation.describe(), f"Create not valid constraint {constraint_name} on model Pony", ) + self.assertEqual( + operation.formatted_description(), + f"+ Create not valid constraint {constraint_name} on model Pony", + ) self.assertEqual( operation.migration_name_fragment, f"pony_{constraint_name}_not_valid", @@ -530,6 +547,10 @@ class ValidateConstraintTests(OperationTestBase): operation.describe(), f"Validate constraint {constraint_name} on model Pony", ) + self.assertEqual( + operation.formatted_description(), + f"~ Validate constraint {constraint_name} on model Pony", + ) self.assertEqual( operation.migration_name_fragment, f"pony_validate_{constraint_name}",