Skip to content

Commit 7d272e8

Browse files
committed
Render enum fields as concrete Field instances in migrations
The migration writer rendered IntEnumField/CharEnumField using their public factory-function names. Those factories are typed to return the enum type (so model attributes resolve to the enum), not a Field, so the generated migration failed mypy with 'incompatible type "tuple[str, ...]"' in CreateModel and 'incompatible type' in AddField. Emit the concrete IntEnumFieldInstance/CharEnumFieldInstance classes instead; these are real Field subclasses, so the generated code type-checks while producing an identical field at runtime. Export the instance classes from tortoise.fields so the 'from tortoise import fields' alias resolves them. Fixes #2155.
1 parent 8c12adc commit 7d272e8

4 files changed

Lines changed: 61 additions & 7 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Added
2020

2121
Fixed
2222
^^^^^
23+
- ``makemigrations`` now renders ``IntEnumField``/``CharEnumField`` as their concrete ``IntEnumFieldInstance``/``CharEnumFieldInstance`` classes, so generated migrations type-check under ``mypy`` instead of failing with ``incompatible type "tuple[str, ...]"``. (#2155)
2324
- ``MigrationRecorder`` now uses parameterized queries; fixes MariaDB/MySQL rejecting ISO-8601 ``applied_at`` values. (#2132)
2425
- ``db_default`` on ``ForeignKeyField``/``OneToOneField`` now propagates to the underlying ``<fk>_id`` column, so ``CREATE TABLE`` emits the ``DEFAULT`` clause for FK columns. (#2199)
2526
- ``MigrationRecorder`` no longer emits tortoise's own ``pk`` field ``DeprecationWarning`` when applying migrations; it now builds its bookkeeping model with ``primary_key=True``. (#2203)

tests/migrations/test_writer.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from tortoise.migrations.constraints import UniqueConstraint
1313
from tortoise.migrations.operations import (
1414
AddConstraint,
15+
AddField,
1516
AddIndex,
1617
AlterField,
1718
CreateModel,
@@ -485,7 +486,14 @@ class Migration(migrations.Migration):
485486

486487

487488
def test_writer_handles_enum_fields(tmp_path: Path, monkeypatch) -> None:
488-
"""Test that IntEnumField and CharEnumField are rendered correctly (not as FieldInstance)."""
489+
"""Test that IntEnumField and CharEnumField render as their concrete Field instances.
490+
491+
The public ``IntEnumField``/``CharEnumField`` names are factory functions typed to
492+
return the enum type (so model attributes resolve to the enum), not a ``Field``.
493+
Rendering them into a migration produced code that failed ``mypy`` (see #2155), so
494+
the writer emits the concrete ``*FieldInstance`` classes, which are real ``Field``
495+
subclasses and therefore type-check inside migration operations.
496+
"""
489497
operations = [
490498
CreateModel(
491499
name="Entity",
@@ -496,8 +504,6 @@ def test_writer_handles_enum_fields(tmp_path: Path, monkeypatch) -> None:
496504
],
497505
),
498506
]
499-
# The migration should use fields.IntEnumField and fields.CharEnumField
500-
# NOT fields.IntEnumFieldInstance or fields.CharEnumFieldInstance
501507
expected = textwrap.dedent(
502508
"""\
503509
from tortoise import migrations
@@ -511,8 +517,8 @@ class Migration(migrations.Migration):
511517
name='Entity',
512518
fields=[
513519
('id', fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
514-
('status', fields.IntEnumField(default=Status.ACTIVE, description='ACTIVE: 1\\nINACTIVE: 2', enum_type=Status, generated=False)),
515-
('role', fields.CharEnumField(description='ADMIN: admin\\nUSER: user', enum_type=Role, max_length=5)),
520+
('status', fields.IntEnumFieldInstance(default=Status.ACTIVE, description='ACTIVE: 1\\nINACTIVE: 2', enum_type=Status, generated=False)),
521+
('role', fields.CharEnumFieldInstance(description='ADMIN: admin\\nUSER: user', enum_type=Role, max_length=5)),
516522
],
517523
),
518524
]
@@ -521,6 +527,42 @@ class Migration(migrations.Migration):
521527
_write_migration(tmp_path, monkeypatch, "0010_enum_fields", operations, expected)
522528

523529

530+
def test_writer_enum_field_render_is_field_instance(tmp_path: Path, monkeypatch) -> None:
531+
"""Regression test for #2155.
532+
533+
``AddField`` (and ``CreateModel``) previously rendered enum fields via the public
534+
``fields.CharEnumField``/``fields.IntEnumField`` factory functions, whose declared
535+
return type is the enum type rather than a ``Field``. The generated migration then
536+
failed ``mypy`` with ``incompatible type "tuple[str, ExampleEnum]"``. The writer now
537+
emits the concrete ``*FieldInstance`` classes so the generated code type-checks.
538+
"""
539+
operations = [
540+
AddField(
541+
model_name="Entity",
542+
name="role",
543+
field=fields.CharEnumField(Role), # type: ignore[arg-type]
544+
),
545+
]
546+
expected = textwrap.dedent(
547+
"""\
548+
from tortoise import migrations
549+
from tortoise.migrations import operations as ops
550+
from tests.migrations.test_writer import Role
551+
from tortoise import fields
552+
553+
class Migration(migrations.Migration):
554+
operations = [
555+
ops.AddField(
556+
model_name='Entity',
557+
name='role',
558+
field=fields.CharEnumFieldInstance(description='ADMIN: admin\\nUSER: user', enum_type=Role, max_length=5),
559+
),
560+
]
561+
"""
562+
)
563+
_write_migration(tmp_path, monkeypatch, "0011_add_enum_field", operations, expected)
564+
565+
524566
def _runpython_forward(apps, schema_editor) -> None:
525567
_ = (apps, schema_editor)
526568

tortoise/fields/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
BinaryField,
1313
BooleanField,
1414
CharEnumField,
15+
CharEnumFieldInstance,
1516
CharField,
1617
DateField,
1718
DatetimeField,
1819
DecimalField,
1920
FloatField,
2021
IntEnumField,
22+
IntEnumFieldInstance,
2123
IntField,
2224
JSONField,
2325
SmallIntField,
@@ -56,13 +58,15 @@
5658
"BinaryField",
5759
"BooleanField",
5860
"CharEnumField",
61+
"CharEnumFieldInstance",
5962
"CharField",
6063
"DateField",
6164
"DatetimeField",
6265
"TimeField",
6366
"DecimalField",
6467
"FloatField",
6568
"IntEnumField",
69+
"IntEnumFieldInstance",
6670
"IntField",
6771
"JSONField",
6872
"SmallIntField",

tortoise/migrations/writer.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,15 @@ def render_value(value: Any, imports: ImportManager) -> str:
222222
def _render_call(path: str, args: list[Any], kwargs: dict[str, Any], imports: ImportManager) -> str:
223223
if path.startswith("tortoise.fields."):
224224
class_name = path.rsplit(".", 1)[1]
225-
# Render FieldInstance classes as their public Field name
226-
if class_name.endswith("FieldInstance"):
225+
# Render FieldInstance classes as their public Field name. The enum fields are
226+
# excluded: their public ``IntEnumField``/``CharEnumField`` names are factory
227+
# functions typed to return the enum type (not a ``Field``), so rendering them
228+
# produces migrations that fail ``mypy`` (see #2155). The concrete
229+
# ``*EnumFieldInstance`` classes are real ``Field`` subclasses that type-check.
230+
if class_name.endswith("FieldInstance") and class_name not in (
231+
"IntEnumFieldInstance",
232+
"CharEnumFieldInstance",
233+
):
227234
class_name = class_name.replace("FieldInstance", "Field")
228235
# For relational fields, move model_name to first positional arg
229236
if path.startswith("tortoise.fields.relational.") and "model_name" in kwargs:

0 commit comments

Comments
 (0)