From 7e5fa7fb79279e194201ff86f181986121dce363 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sun, 14 Dec 2025 17:53:19 +0000 Subject: [PATCH] Fix handling of ClassVar fields in msgspec rendering to avoid conflicts --- src/datamodel_code_generator/model/msgspec.py | 5 ++--- .../model/template/msgspec.jinja2 | 17 +++++++++------ src/datamodel_code_generator/parser/base.py | 6 +++++- .../discriminator_literals_msgspec.py | 10 +++------ ...riminator_literals_msgspec_keyword_only.py | 10 +++------ ...rals_msgspec_keyword_only_omit_defaults.py | 10 +++------ ...minator_with_external_reference_msgspec.py | 21 +++++-------------- .../inner_folder/artificial_folder/type_1.py | 8 ++----- .../inner_folder/schema.py | 8 +++---- .../inner_folder/type_2.py | 5 +---- .../subfolder/type_5.py | 8 ++----- .../type_4.py | 8 ++----- .../discriminator_with_meta_msgspec.py | 6 ++---- .../discriminator_with_type_string_msgspec.py | 4 +--- ...r_with_type_string_msgspec_no_annotated.py | 4 +--- 15 files changed, 46 insertions(+), 84 deletions(-) diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index ec24621d3..911b8b04c 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -22,7 +22,6 @@ from datamodel_code_generator.model import DataModel, DataModelFieldBase from datamodel_code_generator.model.base import UNDEFINED from datamodel_code_generator.model.imports import ( - IMPORT_CLASSVAR, IMPORT_MSGSPEC_CONVERT, IMPORT_MSGSPEC_FIELD, IMPORT_MSGSPEC_META, @@ -82,6 +81,8 @@ def import_extender(cls: type[DataModelFieldBaseT]) -> type[DataModelFieldBaseT] @wraps(original_imports.fget) # pyright: ignore[reportArgumentType] def new_imports(self: DataModelFieldBaseT) -> tuple[Import, ...]: + if self.extras.get("is_classvar"): + return () extra_imports = [] field = self.field # TODO: Improve field detection @@ -91,8 +92,6 @@ def new_imports(self: DataModelFieldBaseT) -> tuple[Import, ...]: extra_imports.append(IMPORT_MSGSPEC_CONVERT) if isinstance(self, DataModelField) and self.needs_meta_import: extra_imports.append(IMPORT_MSGSPEC_META) - if self.extras.get("is_classvar"): - extra_imports.append(IMPORT_CLASSVAR) if not self.required and not self.nullable: extra_imports.append(IMPORT_MSGSPEC_UNSETTYPE) if not self.data_type.use_union_operator: diff --git a/src/datamodel_code_generator/model/template/msgspec.jinja2 b/src/datamodel_code_generator/model/template/msgspec.jinja2 index f4d8e57aa..99afc2478 100644 --- a/src/datamodel_code_generator/model/template/msgspec.jinja2 +++ b/src/datamodel_code_generator/model/template/msgspec.jinja2 @@ -13,13 +13,15 @@ class {{ class_name }}: {{ description | indent(4) }} """ {%- endif %} -{%- if not fields and not description %} - pass -{%- endif %} +{%- set ns = namespace(has_rendered_field=false) -%} {%- for field in fields -%} - {%- if not field.annotated and field.field %} + {%- if field.extras.get('is_classvar') %} + {#- Skip fields with is_classvar=True - they are managed by msgspec tag_field -#} + {%- elif not field.annotated and field.field %} + {%- set ns.has_rendered_field = true %} {{ field.name }}: {{ field.type_hint }} = {{ field.field }} {%- else %} + {%- set ns.has_rendered_field = true %} {%- if field.annotated and not field.field %} {{ field.name }}: {{ field.annotated }} {%- elif field.annotated and field.field %} @@ -34,17 +36,20 @@ class {{ class_name }}: - {%- if field.docstring %} + {%- if not field.extras.get('is_classvar') and field.docstring %} """ {{ field.docstring | indent(4) }} """ {%- if field.use_inline_field_description and not loop.last %} {% endif %} - {%- elif field.inline_field_docstring %} + {%- elif not field.extras.get('is_classvar') and field.inline_field_docstring %} {{ field.inline_field_docstring }} {%- if not loop.last %} {% endif %} {%- endif %} {%- endfor -%} +{%- if not ns.has_rendered_field and not description %} + pass +{%- endif -%} diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 45ef24fd9..177e28e53 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -2028,6 +2028,8 @@ def walk_data_type(data_type: DataType) -> None: for import_ in model.imports: add(import_.alias or import_.import_.split(".")[-1]) for field in model.fields: + if field.extras.get("is_classvar"): + continue add(field.name) add(field.alias) walk_data_type(field.data_type) @@ -2489,7 +2491,9 @@ class Processed(NamedTuple): ) ] for from_, import_ in unused_imports: - processed_model.imports.remove(Import(from_=from_, import_=import_)) + import_obj = Import(from_=from_, import_=import_) + while processed_model.imports.counter.get((from_, import_), 0) > 0: + processed_model.imports.remove(import_obj) for module, mod_key, models, init, imports, scoped_model_resolver in processed_models: # noqa: B007 # process after removing unused models diff --git a/tests/data/expected/main/jsonschema/discriminator_literals_msgspec.py b/tests/data/expected/main/jsonschema/discriminator_literals_msgspec.py index 22911ec62..aad67ebfe 100644 --- a/tests/data/expected/main/jsonschema/discriminator_literals_msgspec.py +++ b/tests/data/expected/main/jsonschema/discriminator_literals_msgspec.py @@ -4,21 +4,17 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union +from typing import Annotated, Union from msgspec import Meta, Struct, UnsetType class Type1(Struct, tag_field='type_', tag='a'): - type_: ClassVar[Annotated[Union[Literal['a'], UnsetType], Meta(title='Type ')]] = ( - 'a' - ) + pass class Type2(Struct, tag_field='type_', tag='b'): - type_: ClassVar[Annotated[Union[Literal['b'], UnsetType], Meta(title='Type ')]] = ( - 'b' - ) + pass class UnrelatedType(Struct): diff --git a/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only.py b/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only.py index aef0446a7..75a058e21 100644 --- a/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only.py +++ b/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only.py @@ -4,21 +4,17 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union +from typing import Annotated, Union from msgspec import Meta, Struct, UnsetType class Type1(Struct, kw_only=True, tag_field='type_', tag='a'): - type_: ClassVar[Annotated[Union[Literal['a'], UnsetType], Meta(title='Type ')]] = ( - 'a' - ) + pass class Type2(Struct, kw_only=True, tag_field='type_', tag='b'): - type_: ClassVar[Annotated[Union[Literal['b'], UnsetType], Meta(title='Type ')]] = ( - 'b' - ) + pass class UnrelatedType(Struct, kw_only=True): diff --git a/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only_omit_defaults.py b/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only_omit_defaults.py index 37165d7f7..92db16d2a 100644 --- a/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only_omit_defaults.py +++ b/tests/data/expected/main/jsonschema/discriminator_literals_msgspec_keyword_only_omit_defaults.py @@ -4,21 +4,17 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union +from typing import Annotated, Union from msgspec import Meta, Struct, UnsetType class Type1(Struct, omit_defaults=True, kw_only=True, tag_field='type_', tag='a'): - type_: ClassVar[Annotated[Union[Literal['a'], UnsetType], Meta(title='Type ')]] = ( - 'a' - ) + pass class Type2(Struct, omit_defaults=True, kw_only=True, tag_field='type_', tag='b'): - type_: ClassVar[Annotated[Union[Literal['b'], UnsetType], Meta(title='Type ')]] = ( - 'b' - ) + pass class UnrelatedType(Struct, omit_defaults=True, kw_only=True): diff --git a/tests/data/expected/main/jsonschema/discriminator_with_external_reference_msgspec.py b/tests/data/expected/main/jsonschema/discriminator_with_external_reference_msgspec.py index 6001aaf75..061f1ec0e 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_external_reference_msgspec.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_external_reference_msgspec.py @@ -4,42 +4,31 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union +from typing import Annotated, Union from msgspec import UNSET, Meta, Struct, UnsetType class Type1(Struct, tag_field='type_', tag='a'): - type_: ClassVar[Annotated[Union[Literal['a'], UnsetType], Meta(title='Type ')]] = ( - 'a' - ) + pass class Type2(Struct, tag_field='type_', tag='b'): - type_: ClassVar[Annotated[Union[Literal['b'], UnsetType], Meta(title='Type ')]] = ( - 'b' - ) ref_type: Union[ Annotated[Type1, Meta(description='A referenced type.')], UnsetType ] = UNSET class Type4(Struct, tag_field='type_', tag='d'): - type_: ClassVar[Annotated[Union[Literal['d'], UnsetType], Meta(title='Type ')]] = ( - 'd' - ) + pass class Type5(Struct, tag_field='type_', tag='e'): - type_: ClassVar[Annotated[Union[Literal['e'], UnsetType], Meta(title='Type ')]] = ( - 'e' - ) + pass class Type3(Struct, tag_field='type_', tag='c'): - type_: ClassVar[Annotated[Union[Literal['c'], UnsetType], Meta(title='Type ')]] = ( - 'c' - ) + pass class Response(Struct): diff --git a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/artificial_folder/type_1.py b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/artificial_folder/type_1.py index 1c34c199d..2ee47257f 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/artificial_folder/type_1.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/artificial_folder/type_1.py @@ -4,12 +4,8 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union - -from msgspec import Meta, Struct, UnsetType +from msgspec import Struct class Type1(Struct, tag_field='type_', tag='a'): - type_: ClassVar[Annotated[Union[Literal['a'], UnsetType], Meta(title='Type ')]] = ( - 'a' - ) + pass diff --git a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/schema.py b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/schema.py index 0b5d2c9db..d2575a789 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/schema.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/schema.py @@ -4,9 +4,9 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union +from typing import Annotated, Union -from msgspec import Meta, Struct, UnsetType +from msgspec import Meta, Struct from .. import type_4 from ..subfolder import type_5 @@ -15,9 +15,7 @@ class Type3(Struct, tag_field='type_', tag='c'): - type_: ClassVar[Annotated[Union[Literal['c'], UnsetType], Meta(title='Type ')]] = ( - 'c' - ) + pass class Response(Struct): diff --git a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/type_2.py b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/type_2.py index 4db246659..d947d3633 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/type_2.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/inner_folder/type_2.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union +from typing import Annotated, Union from msgspec import UNSET, Meta, Struct, UnsetType @@ -12,9 +12,6 @@ class Type2(Struct, tag_field='type_', tag='b'): - type_: ClassVar[Annotated[Union[Literal['b'], UnsetType], Meta(title='Type ')]] = ( - 'b' - ) ref_type: Union[ Annotated[type_1.Type1, Meta(description='A referenced type.')], UnsetType ] = UNSET diff --git a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/subfolder/type_5.py b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/subfolder/type_5.py index 2fda007d8..be9aa1936 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/subfolder/type_5.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/subfolder/type_5.py @@ -4,12 +4,8 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union - -from msgspec import Meta, Struct, UnsetType +from msgspec import Struct class Type5(Struct, tag_field='type_', tag='e'): - type_: ClassVar[Annotated[Union[Literal['e'], UnsetType], Meta(title='Type ')]] = ( - 'e' - ) + pass diff --git a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/type_4.py b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/type_4.py index 389e4f75b..29ecaaaf5 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/type_4.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_external_references_folder_msgspec/type_4.py @@ -4,12 +4,8 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union - -from msgspec import Meta, Struct, UnsetType +from msgspec import Struct class Type4(Struct, tag_field='type_', tag='d'): - type_: ClassVar[Annotated[Union[Literal['d'], UnsetType], Meta(title='Type ')]] = ( - 'd' - ) + pass diff --git a/tests/data/expected/main/jsonschema/discriminator_with_meta_msgspec.py b/tests/data/expected/main/jsonschema/discriminator_with_meta_msgspec.py index eaa176c33..6e1a2b4d4 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_meta_msgspec.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_meta_msgspec.py @@ -4,18 +4,16 @@ from __future__ import annotations -from typing import Annotated, ClassVar, Literal, Union +from typing import Union -from msgspec import UNSET, Meta, Struct, UnsetType +from msgspec import UNSET, Struct, UnsetType class SystemMessage(Struct, tag_field='role', tag='system'): - role: ClassVar[Annotated[Literal['system'], Meta(title='Message Role')]] content: str class UserMessage(Struct, tag_field='role', tag='user'): - role: ClassVar[Annotated[Literal['user'], Meta(title='Message Role')]] content: str diff --git a/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec.py b/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec.py index 3a3ca3710..a356b7fda 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec.py @@ -4,18 +4,16 @@ from __future__ import annotations -from typing import ClassVar, Literal, Union +from typing import Union from msgspec import UNSET, Struct, UnsetType class SystemMessage(Struct, tag_field='role', tag='system'): - role: ClassVar[Literal['system']] content: str class UserMessage(Struct, tag_field='role', tag='user'): - role: ClassVar[Literal['user']] content: str diff --git a/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec_no_annotated.py b/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec_no_annotated.py index 3a3ca3710..a356b7fda 100644 --- a/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec_no_annotated.py +++ b/tests/data/expected/main/jsonschema/discriminator_with_type_string_msgspec_no_annotated.py @@ -4,18 +4,16 @@ from __future__ import annotations -from typing import ClassVar, Literal, Union +from typing import Union from msgspec import UNSET, Struct, UnsetType class SystemMessage(Struct, tag_field='role', tag='system'): - role: ClassVar[Literal['system']] content: str class UserMessage(Struct, tag_field='role', tag='user'): - role: ClassVar[Literal['user']] content: str