From f2611d6a5dc2947c54cd631ebf461895fe74c15e Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Mon, 11 May 2026 10:40:42 +0200 Subject: [PATCH] chore: drop dedupe post-processing via datamodel-codegen reuse_model Enabling the built-in `reuse_model` option suppresses the duplicate `class Type(StrEnum)` that the `deduplicate_error_type_enum` step existed to remove, so the post-processing step and its tests are no longer needed. --- pyproject.toml | 3 +- scripts/postprocess_generated_models.py | 28 ---- src/apify_client/_models.py | 31 ++-- .../unit/test_postprocess_generated_models.py | 149 +----------------- uv.lock | 8 +- 5 files changed, 18 insertions(+), 201 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a1939ba..03306698 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dev = [ # See https://github.com/apify/apify-client-python/pull/582/ for more details. # We explicitly constrain black>=24.3.0 to override the transitive dependency. "black>=24.3.0", - "datamodel-code-generator[http,ruff]<1.0.0", + "datamodel-code-generator[http,ruff]>=0.57.0,<1.0.0", "dycw-pytest-only<3.0.0", "griffe<3.0.0", "poethepoet<1.0.0", @@ -229,6 +229,7 @@ use_annotated = true wrap_string_literal = true snake_case_field = true use_subclass_enum = true +reuse_model = true extra_fields = "allow" allow_population_by_field_name = true aliases = "datamodel_codegen_aliases.json" diff --git a/scripts/postprocess_generated_models.py b/scripts/postprocess_generated_models.py index fdabfe81..f9146a5e 100644 --- a/scripts/postprocess_generated_models.py +++ b/scripts/postprocess_generated_models.py @@ -3,7 +3,6 @@ Applied to `_models.py`: - Fix discriminator field names that use camelCase instead of snake_case (known issue with discriminators on schemas referenced from array items). -- Deduplicate the inlined `Type(StrEnum)` that comes from ErrorResponse.yaml; rewire to `ErrorType`. - Rewrite every `class X(StrEnum)` as `X = Literal[...]` so downstream code can pass plain strings (and reuse the named alias in resource-client signatures) instead of enum members. - Move the resulting `X = Literal[...]` definitions into `_literals.py`, leaving @@ -97,32 +96,6 @@ def fix_discriminators(content: str) -> str: return content -def deduplicate_error_type_enum(content: str) -> str: - """Remove the duplicate `Type` enum and rewire references to `ErrorType`. - - The `type` property on `ErrorResponse` discriminator subtypes (`RunFailedErrorDetail` etc.) - re-emits the value list of the named `ErrorType` enum as a separate `class Type(StrEnum)` — - upstream issue https://github.com/koxudaxi/datamodel-code-generator/issues/3104. - """ - tree = ast.parse(content) - type_node = next( - (n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Type' and 'StrEnum' in _base_names(n)), - None, - ) - if type_node is None: - return content - - assert type_node.end_lineno is not None # noqa: S101 - lines = content.split('\n') - del lines[type_node.lineno - 1 : type_node.end_lineno] - content = '\n'.join(lines) - - # Lookbehinds are deliberately narrow: matching bare `\bType\b` would also rewrite `Type` in - # docstrings (`Content-Type`, `Type of event`), which broke an earlier version. - content = re.sub(r'(?<=: )Type\b|(?<=\| )Type\b|(?<=\[)Type\b', 'ErrorType', content) - return _collapse_blank_lines(content) - - def convert_enums_to_literals(content: str) -> str: """Rewrite every `class X(StrEnum): ...` into an `X = Literal[...]` alias. @@ -425,7 +398,6 @@ def postprocess_models(models_path: Path, literals_path: Path) -> list[Path]: """ original = models_path.read_text() fixed = fix_discriminators(original) - fixed = deduplicate_error_type_enum(fixed) fixed = convert_enums_to_literals(fixed) fixed = add_docs_group_decorators(fixed, 'Models') models_content, literals_content = split_literals_to_file(fixed) diff --git a/src/apify_client/_models.py b/src/apify_client/_models.py index a8b4ee69..a8a29fa5 100644 --- a/src/apify_client/_models.py +++ b/src/apify_client/_models.py @@ -841,15 +841,6 @@ class DecodeAndVerifyData(BaseModel): is_verified_user: Annotated[bool, Field(alias='isVerifiedUser', examples=[False])] -@docs_group('Models') -class DecodeAndVerifyRequest(BaseModel): - model_config = ConfigDict( - extra='allow', - populate_by_name=True, - ) - encoded: Annotated[str, Field(examples=['eyJwYXlsb2FkIjoiLi4uIiwic2lnbmF0dXJlIjoiLi4uIn0='])] - - @docs_group('Models') class DecodeAndVerifyResponse(BaseModel): model_config = ConfigDict( @@ -961,6 +952,11 @@ class EncodeAndSignData(BaseModel): encoded: Annotated[str, Field(examples=['eyJwYXlsb2FkIjoiLi4uIiwic2lnbmF0dXJlIjoiLi4uIn0='])] +@docs_group('Models') +class DecodeAndVerifyRequest(EncodeAndSignData): + pass + + @docs_group('Models') class EncodeAndSignResponse(BaseModel): model_config = ConfigDict( @@ -3230,13 +3226,8 @@ class UpdateRunRequest(BaseModel): @docs_group('Models') -class UpdateStoreRequest(BaseModel): - model_config = ConfigDict( - extra='allow', - populate_by_name=True, - ) - name: str | None = None - general_access: Annotated[GeneralAccess | None, Field(alias='generalAccess')] = None +class UpdateStoreRequest(UpdateDatasetRequest): + pass @docs_group('Models') @@ -3448,12 +3439,8 @@ class WebhookDispatch(BaseModel): @docs_group('Models') -class WebhookDispatchResponse(BaseModel): - model_config = ConfigDict( - extra='allow', - populate_by_name=True, - ) - data: WebhookDispatch +class WebhookDispatchResponse(TestWebhookResponse): + pass @docs_group('Models') diff --git a/tests/unit/test_postprocess_generated_models.py b/tests/unit/test_postprocess_generated_models.py index 0c9b7e08..bd024db7 100644 --- a/tests/unit/test_postprocess_generated_models.py +++ b/tests/unit/test_postprocess_generated_models.py @@ -5,7 +5,6 @@ from scripts.postprocess_generated_models import ( add_docs_group_decorators, convert_enums_to_literals, - deduplicate_error_type_enum, fix_discriminators, split_literals_to_file, ) @@ -49,143 +48,6 @@ def test_fix_discriminators_does_not_touch_unrelated() -> None: assert result == content -# -- deduplicate_error_type_enum ---------------------------------------------- - - -def test_deduplicate_error_type_enum_removes_duplicate() -> None: - """The duplicate `Type(StrEnum)` is dropped while the original `ErrorType` is preserved.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - SOME_ERROR = 'some-error' - - class Type(StrEnum): - SOME_ERROR = 'some-error' - OTHER = 'other' - - class ErrorResponse(BaseModel): - error_type: Type - """) - result = deduplicate_error_type_enum(content) - assert 'class Type(StrEnum)' not in result - assert 'class ErrorType(StrEnum)' in result - - -def test_deduplicate_error_type_enum_rewires_colon_annotation() -> None: - """A `: Type` annotation is rewired to `: ErrorType` after the duplicate is dropped.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - SOME_ERROR = 'some-error' - - class Type(StrEnum): - SOME_ERROR = 'some-error' - - class ErrorResponse(BaseModel): - error_type: Type - """) - result = deduplicate_error_type_enum(content) - assert 'error_type: ErrorType' in result - - -def test_deduplicate_error_type_enum_rewires_union_annotation() -> None: - """A `| Type` arm in a union annotation is rewired to `| ErrorType`.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - X = 'x' - - class Type(StrEnum): - X = 'x' - - class Foo(BaseModel): - field: str | Type - """) - result = deduplicate_error_type_enum(content) - assert '| ErrorType' in result - assert '| Type' not in result - - -def test_deduplicate_error_type_enum_rewires_bracket_annotation() -> None: - """A `[Type]` subscript inside a generic annotation is rewired to `[ErrorType]`.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - X = 'x' - - class Type(StrEnum): - X = 'x' - - class Foo(BaseModel): - field: list[Type] - """) - result = deduplicate_error_type_enum(content) - assert 'list[ErrorType]' in result - assert 'list[Type]' not in result - - -def test_deduplicate_error_type_enum_collapses_extra_blank_lines() -> None: - """Removing the duplicate enum collapses any resulting run of 4+ blank lines.""" - content = "\nclass Type(StrEnum):\n X = 'x'\n\n\n\n\nclass Next(BaseModel):\n pass\n" - result = deduplicate_error_type_enum(content) - assert '\n\n\n\n' not in result - - -def test_deduplicate_error_type_enum_no_change_when_no_duplicate() -> None: - """Source without a `Type(StrEnum)` duplicate passes through unchanged.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - SOME_ERROR = 'some-error' - - class Foo(BaseModel): - field: ErrorType - """) - result = deduplicate_error_type_enum(content) - assert result == content - - -def test_deduplicate_error_type_enum_does_not_touch_type_in_class_names() -> None: - """`Type` inside other class names (e.g. `ContentType`) is not rewritten.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - X = 'x' - - class Type(StrEnum): - X = 'x' - - class ContentType(BaseModel): - value: str - """) - result = deduplicate_error_type_enum(content) - assert 'class ContentType(BaseModel)' in result - - -def test_deduplicate_error_type_enum_handles_type_as_last_class() -> None: - """The `Type` enum is removed even when it's the last top-level definition.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - X = 'x' - - class Foo(BaseModel): - field: Type - - class Type(StrEnum): - X = 'x' - """) - result = deduplicate_error_type_enum(content) - assert 'class Type(StrEnum)' not in result - assert 'field: ErrorType' in result - - -def test_deduplicate_error_type_enum_skips_non_strenum_type() -> None: - """A `Type` class that is not a `StrEnum` (e.g. a Pydantic model) is left in place.""" - content = textwrap.dedent("""\ - class ErrorType(StrEnum): - X = 'x' - - class Type(BaseModel): - value: str - """) - result = deduplicate_error_type_enum(content) - assert 'class Type(BaseModel)' in result - - # -- add_docs_group_decorators ------------------------------------------------ @@ -417,7 +279,7 @@ class Status(StrEnum): def test_full_pipeline() -> None: - """All steps composed: discriminator fix, `Type` dedup, enum-to-literal, docs decorators.""" + """All steps composed: discriminator fix, enum-to-literal, docs decorators.""" content = textwrap.dedent("""\ from enum import StrEnum from typing import Literal @@ -430,17 +292,13 @@ class Zebra(BaseModel): class ErrorType(StrEnum): SOME_ERROR = 'some-error' - class Type(StrEnum): - SOME_ERROR = 'some-error' - class ErrorResponse(BaseModel): - error_type: Type + error_type: ErrorType class Alpha(BaseModel): name: str """) result = fix_discriminators(content) - result = deduplicate_error_type_enum(result) result = convert_enums_to_literals(result) result = add_docs_group_decorators(result, 'Models') @@ -448,8 +306,7 @@ class Alpha(BaseModel): assert "discriminator='pricing_model'" in result assert "discriminator='pricingModel'" not in result - # Duplicate Type enum removed and references rewired, then the remaining enum converted. - assert 'class Type(StrEnum)' not in result + # The enum is converted to a Literal alias. assert 'class ErrorType(StrEnum)' not in result assert 'ErrorType = Literal[' in result assert 'error_type: ErrorType' in result diff --git a/uv.lock b/uv.lock index 8e00f349..756a531e 100644 --- a/uv.lock +++ b/uv.lock @@ -82,7 +82,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=24.3.0" }, - { name = "datamodel-code-generator", extras = ["http", "ruff"], specifier = "<1.0.0" }, + { name = "datamodel-code-generator", extras = ["http", "ruff"], specifier = ">=0.57.0,<1.0.0" }, { name = "dycw-pytest-only", specifier = "<3.0.0" }, { name = "griffe", specifier = "<3.0.0" }, { name = "poethepoet", specifier = "<1.0.0" }, @@ -421,7 +421,7 @@ wheels = [ [[package]] name = "datamodel-code-generator" -version = "0.56.1" +version = "0.57.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -433,9 +433,9 @@ dependencies = [ { name = "pydantic" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/9c/5379be8daf9ff2fbbb9a7efcf68c037b089e5b76f360c4f9ca730916e2ee/datamodel_code_generator-0.56.1.tar.gz", hash = "sha256:697abd90cc4eb2c65f130be79a83a24746c3f2d0e15e6eb9dbf17b96784449be", size = 840372, upload-time = "2026-04-16T17:09:53.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/44/87d5980f813a1e323c5d726b3ac5fec8c915ce8a77fcdceaf9c00457dbae/datamodel_code_generator-0.57.0.tar.gz", hash = "sha256:0eda778ea06eaa476e542a5f1fe1d14cc3bbf686edb33a0ad6151c7d19089906", size = 932941, upload-time = "2026-05-07T16:21:55.819Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/cd/4a4a13f55069d88d8405aa325874b04c1b1dbc830f0d66c0e56cbd16aa12/datamodel_code_generator-0.56.1-py3-none-any.whl", hash = "sha256:cb2adc18a301f1a2cfcd56672037218eeaff0573669861c2b371eefe86753874", size = 256845, upload-time = "2026-04-16T17:09:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/4fb9a44bb4a305b860c5a5b1866dcccfac3b76f5f170a9e68fc7733e16d2/datamodel_code_generator-0.57.0-py3-none-any.whl", hash = "sha256:d26bf5defe5154493d0aa5a822b7725332b9e9dd2abccc2f8856052286aa83b5", size = 259343, upload-time = "2026-05-07T16:21:53.823Z" }, ] [package.optional-dependencies]