diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7bbbe6d0b..ee7e13d2a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,13 @@ Changelog 1.1 === +1.1.5 +----- + +Fixed +^^^^^ +- ``makemigrations`` no longer crashes with ``AttributeError: 'tuple' object has no attribute 'deconstruct'`` when generating a fresh ``CreateModel`` migration for models using tuple-style ``Meta.indexes`` (e.g. ``indexes = [("field_a", "field_b")]``). Tuple entries are now normalised to ``Index`` objects before rendering. + 1.1.4 ----- diff --git a/tests/contrib/test_decorator.py b/tests/contrib/test_decorator.py index e8f03e51e..1ef0966a8 100644 --- a/tests/contrib/test_decorator.py +++ b/tests/contrib/test_decorator.py @@ -18,7 +18,7 @@ async def test_basic_example_script(db) -> None: r = subprocess.run( # nosec [sys.executable, "examples/basic.py"], capture_output=True, text=True, env=env ) - assert not r.stderr, f"Script had errors: {r.stderr}" + assert r.returncode == 0, f"Script failed (rc={r.returncode}): {r.stderr}" output = r.stdout s = "[{'id': 1, 'name': 'Updated name'}, {'id': 2, 'name': 'Test 2'}]" assert s in output diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 12de3ff21..248d679a8 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -187,6 +187,36 @@ class Migration(migrations.Migration): _write_migration(tmp_path, monkeypatch, "0003_options", operations, expected) +def test_writer_handles_tuple_indexes_in_options(tmp_path: Path, monkeypatch) -> None: + """Tuple-style indexes in options should be normalised to Index objects without crashing.""" + operations = [ + CreateModel( + name="Token", + fields=[ + ("id", fields.IntField(primary_key=True)), + ("user_id", fields.IntField()), + ("revoked_at", fields.DatetimeField(null=True)), + ], + options={ + "indexes": [ + ("user_id", "revoked_at"), + ], + }, + ), + ] + module_path = _prepare_migration_package(tmp_path, "app") + monkeypatch.syspath_prepend(str(tmp_path)) + writer = MigrationWriter( + "0001_initial", + "app", + operations, + migrations_module=module_path, + ) + content = writer.as_string() + assert "Index(fields=['user_id', 'revoked_at'])" in content + assert "from tortoise.indexes import Index" in content + + def test_writer_renders_fk_field(tmp_path: Path, monkeypatch) -> None: operations = [ CreateModel( diff --git a/tests/test_relations.py b/tests/test_relations.py index 6afe270ec..b1ab786ab 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -599,7 +599,7 @@ async def test_reverse_relation_create_fk_errors_for_unsaved_instance(db): async def test_recursive(db) -> None: file = "examples/relations_recursive.py" r = subprocess.run([sys.executable, file], capture_output=True, text=True) # nosec - assert not r.stderr, f"Script had errors: {r.stderr}" + assert r.returncode == 0, f"Script failed (rc={r.returncode}): {r.stderr}" output = r.stdout s = "2.1. Second H2 (to: ) (from: 2.2. Third H2, Loose, 1.1. First H2)" assert s in output diff --git a/tortoise/__init__.py b/tortoise/__init__.py index 6cb6bc7dd..845920f32 100644 --- a/tortoise/__init__.py +++ b/tortoise/__init__.py @@ -585,7 +585,7 @@ async def main() -> None: portal.call(main) -__version__ = "1.1.4" +__version__ = "1.1.5" __all__ = [ "BackwardFKRelation", diff --git a/tortoise/migrations/writer.py b/tortoise/migrations/writer.py index 1a7f4724e..d99332a9f 100644 --- a/tortoise/migrations/writer.py +++ b/tortoise/migrations/writer.py @@ -16,6 +16,7 @@ from pypika_tortoise.context import DEFAULT_SQL_CONTEXT +from tortoise.indexes import Index from tortoise.migrations.constraints import CheckConstraint, UniqueConstraint from tortoise.migrations.operations import ( AddConstraint, @@ -455,8 +456,11 @@ def _render_model_options(self, options: dict[str, Any], imports: ImportManager) rendered: dict[str, str] = {} for key, value in options.items(): if key == "indexes": + normalized = [ + item if isinstance(item, Index) else Index(fields=tuple(item)) for item in value + ] rendered[key] = ( - "[" + ", ".join(self._render_index(item, imports) for item in value) + "]" + "[" + ", ".join(self._render_index(item, imports) for item in normalized) + "]" ) continue if key == "constraints":