From ef6f61c17918de15c3f0c510515168655186c4f3 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 30 Dec 2025 08:59:42 +0000 Subject: [PATCH 1/7] Add config parameter to Parser classes --- src/datamodel_code_generator/config.py | 15 ++ src/datamodel_code_generator/parser/base.py | 198 +++++++++++++++++- .../parser/graphql.py | 10 + .../parser/jsonschema.py | 4 + .../parser/openapi.py | 10 + .../test_public_api_signature_baseline.py | 134 +++++++++++- 6 files changed, 369 insertions(+), 2 deletions(-) diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index b8bc22402..02e9987b1 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -314,6 +314,21 @@ class Config: target_pydantic_version: TargetPydanticVersion | None = None +class OpenAPIParserConfig(ParserConfig): + """Configuration model for OpenAPIParser.__init__().""" + + openapi_scopes: list[OpenAPIScope] | None = None + include_path_parameters: bool = False + use_status_code_in_response_name: bool = False + + +class GraphQLParserConfig(ParserConfig): + """Configuration model for GraphQLParser.__init__().""" + + data_model_scalar_type: type[DataModel] | None = None + data_model_union_type: type[DataModel] | None = None + + class ParseConfig(BaseModel): """Configuration model for Parser.parse().""" diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 3583b3c0f..505a198d3 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -82,6 +82,7 @@ from collections.abc import Iterable, Iterator, Mapping, Sequence from datamodel_code_generator import DataclassArguments + from datamodel_code_generator.config import ParserConfig @runtime_checkable @@ -686,10 +687,11 @@ class Parser(ABC): parse_raw() to handle specific schema formats. """ - def __init__( # noqa: PLR0912, PLR0913, PLR0915 + def __init__( # noqa: PLR0912, PLR0913, PLR0914, PLR0915 self, source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, + config: ParserConfig | None = None, data_model_type: type[DataModel] = pydantic_model.BaseModel, data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, @@ -802,6 +804,200 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915 target_pydantic_version: TargetPydanticVersion | None = None, ) -> None: """Initialize the Parser with configuration options.""" + if config is not None: + data_model_type = config.data_model_type if data_model_type is pydantic_model.BaseModel else data_model_type + data_model_root_type = ( + config.data_model_root_type + if data_model_root_type is pydantic_model.CustomRootType + else data_model_root_type + ) + data_type_manager_type = ( + config.data_type_manager_type + if data_type_manager_type is pydantic_model.DataTypeManager + else data_type_manager_type + ) + data_model_field_type = ( + config.data_model_field_type + if data_model_field_type is pydantic_model.DataModelField + else data_model_field_type + ) + base_class = config.base_class if base_class is None else base_class + base_class_map = config.base_class_map if base_class_map is None else base_class_map + additional_imports = config.additional_imports if additional_imports is None else additional_imports + class_decorators = config.class_decorators if class_decorators is None else class_decorators + custom_template_dir = config.custom_template_dir if custom_template_dir is None else custom_template_dir + if extra_template_data is None and config.extra_template_data is not None: + extra_template_data = defaultdict(dict, config.extra_template_data) + target_python_version = ( + config.target_python_version if target_python_version == PythonVersionMin else target_python_version + ) + dump_resolve_reference_action = ( + config.dump_resolve_reference_action + if dump_resolve_reference_action is None + else dump_resolve_reference_action + ) + validation = validation or config.validation + field_constraints = field_constraints or config.field_constraints + snake_case_field = snake_case_field or config.snake_case_field + strip_default_none = strip_default_none or config.strip_default_none + aliases = config.aliases if aliases is None else aliases + allow_population_by_field_name = allow_population_by_field_name or config.allow_population_by_field_name + apply_default_values_for_required_fields = ( + apply_default_values_for_required_fields or config.apply_default_values_for_required_fields + ) + allow_extra_fields = allow_extra_fields or config.allow_extra_fields + extra_fields = config.extra_fields if extra_fields is None else extra_fields + use_generic_base_class = use_generic_base_class or config.use_generic_base_class + force_optional_for_required_fields = ( + force_optional_for_required_fields or config.force_optional_for_required_fields + ) + class_name = config.class_name if class_name is None else class_name + use_standard_collections = use_standard_collections or config.use_standard_collections + base_path = config.base_path if base_path is None else base_path + use_schema_description = use_schema_description or config.use_schema_description + use_field_description = use_field_description or config.use_field_description + use_field_description_example = use_field_description_example or config.use_field_description_example + use_attribute_docstrings = use_attribute_docstrings or config.use_attribute_docstrings + use_inline_field_description = use_inline_field_description or config.use_inline_field_description + use_default_kwarg = use_default_kwarg or config.use_default_kwarg + reuse_model = reuse_model or config.reuse_model + reuse_scope = config.reuse_scope if reuse_scope is None else reuse_scope + shared_module_name = ( + config.shared_module_name if shared_module_name == DEFAULT_SHARED_MODULE_NAME else shared_module_name + ) + encoding = config.encoding if encoding == "utf-8" else encoding + enum_field_as_literal = ( + config.enum_field_as_literal if enum_field_as_literal is None else enum_field_as_literal + ) + enum_field_as_literal_map = ( + config.enum_field_as_literal_map if enum_field_as_literal_map is None else enum_field_as_literal_map + ) + ignore_enum_constraints = ignore_enum_constraints or config.ignore_enum_constraints + set_default_enum_member = set_default_enum_member or config.set_default_enum_member + use_subclass_enum = use_subclass_enum or config.use_subclass_enum + use_specialized_enum = config.use_specialized_enum if use_specialized_enum else False + strict_nullable = strict_nullable or config.strict_nullable + use_generic_container_types = use_generic_container_types or config.use_generic_container_types + enable_faux_immutability = enable_faux_immutability or config.enable_faux_immutability + remote_text_cache = config.remote_text_cache if remote_text_cache is None else remote_text_cache + disable_appending_item_suffix = disable_appending_item_suffix or config.disable_appending_item_suffix + strict_types = config.strict_types if strict_types is None else strict_types + empty_enum_field_name = ( + config.empty_enum_field_name if empty_enum_field_name is None else empty_enum_field_name + ) + custom_class_name_generator = ( + config.custom_class_name_generator + if custom_class_name_generator is title_to_class_name + else custom_class_name_generator + ) + field_extra_keys = config.field_extra_keys if field_extra_keys is None else field_extra_keys + field_include_all_keys = field_include_all_keys or config.field_include_all_keys + field_extra_keys_without_x_prefix = ( + config.field_extra_keys_without_x_prefix + if field_extra_keys_without_x_prefix is None + else field_extra_keys_without_x_prefix + ) + model_extra_keys = config.model_extra_keys if model_extra_keys is None else model_extra_keys + model_extra_keys_without_x_prefix = ( + config.model_extra_keys_without_x_prefix + if model_extra_keys_without_x_prefix is None + else model_extra_keys_without_x_prefix + ) + wrap_string_literal = config.wrap_string_literal if wrap_string_literal is None else wrap_string_literal + use_title_as_name = use_title_as_name or config.use_title_as_name + use_operation_id_as_name = use_operation_id_as_name or config.use_operation_id_as_name + use_unique_items_as_set = use_unique_items_as_set or config.use_unique_items_as_set + use_tuple_for_fixed_items = use_tuple_for_fixed_items or config.use_tuple_for_fixed_items + allof_merge_mode = ( + config.allof_merge_mode if allof_merge_mode == AllOfMergeMode.Constraints else allof_merge_mode + ) + http_headers = config.http_headers if http_headers is None else http_headers + http_ignore_tls = http_ignore_tls or config.http_ignore_tls + http_timeout = config.http_timeout if http_timeout is None else http_timeout + use_annotated = use_annotated or config.use_annotated + use_serialize_as_any = use_serialize_as_any or config.use_serialize_as_any + use_non_positive_negative_number_constrained_types = ( + use_non_positive_negative_number_constrained_types + or config.use_non_positive_negative_number_constrained_types + ) + use_decimal_for_multiple_of = use_decimal_for_multiple_of or config.use_decimal_for_multiple_of + original_field_name_delimiter = ( + config.original_field_name_delimiter + if original_field_name_delimiter is None + else original_field_name_delimiter + ) + use_double_quotes = use_double_quotes or config.use_double_quotes + use_union_operator = use_union_operator or config.use_union_operator + allow_responses_without_content = allow_responses_without_content or config.allow_responses_without_content + collapse_root_models = collapse_root_models or config.collapse_root_models + collapse_root_models_name_strategy = ( + config.collapse_root_models_name_strategy + if collapse_root_models_name_strategy is None + else collapse_root_models_name_strategy + ) + collapse_reuse_models = collapse_reuse_models or config.collapse_reuse_models + skip_root_model = skip_root_model or config.skip_root_model + use_type_alias = use_type_alias or config.use_type_alias + special_field_name_prefix = ( + config.special_field_name_prefix if special_field_name_prefix is None else special_field_name_prefix + ) + remove_special_field_name_prefix = ( + remove_special_field_name_prefix or config.remove_special_field_name_prefix + ) + capitalise_enum_members = capitalise_enum_members or config.capitalise_enum_members + keep_model_order = keep_model_order or config.keep_model_order + use_one_literal_as_default = use_one_literal_as_default or config.use_one_literal_as_default + use_enum_values_in_discriminator = ( + use_enum_values_in_discriminator or config.use_enum_values_in_discriminator + ) + known_third_party = config.known_third_party if known_third_party is None else known_third_party + custom_formatters = config.custom_formatters if custom_formatters is None else custom_formatters + custom_formatters_kwargs = ( + config.custom_formatters_kwargs if custom_formatters_kwargs is None else custom_formatters_kwargs + ) + use_pendulum = use_pendulum or config.use_pendulum + use_standard_primitive_types = use_standard_primitive_types or config.use_standard_primitive_types + http_query_parameters = ( + config.http_query_parameters if http_query_parameters is None else http_query_parameters + ) + treat_dot_as_module = config.treat_dot_as_module if treat_dot_as_module is None else treat_dot_as_module + use_exact_imports = use_exact_imports or config.use_exact_imports + default_field_extras = config.default_field_extras if default_field_extras is None else default_field_extras + target_datetime_class = ( + config.target_datetime_class if target_datetime_class is None else target_datetime_class + ) + target_date_class = config.target_date_class if target_date_class is None else target_date_class + keyword_only = keyword_only or config.keyword_only + frozen_dataclasses = frozen_dataclasses or config.frozen_dataclasses + no_alias = no_alias or config.no_alias + use_frozen_field = use_frozen_field or config.use_frozen_field + use_default_factory_for_optional_nested_models = ( + use_default_factory_for_optional_nested_models or config.use_default_factory_for_optional_nested_models + ) + formatters = config.formatters if formatters == DEFAULT_FORMATTERS else formatters + defer_formatting = defer_formatting or config.defer_formatting + parent_scoped_naming = parent_scoped_naming or config.parent_scoped_naming + naming_strategy = config.naming_strategy if naming_strategy is None else naming_strategy + duplicate_name_suffix = ( + config.duplicate_name_suffix if duplicate_name_suffix is None else duplicate_name_suffix + ) + dataclass_arguments = config.dataclass_arguments if dataclass_arguments is None else dataclass_arguments + type_mappings = config.type_mappings if type_mappings is None else type_mappings + type_overrides = config.type_overrides if type_overrides is None else type_overrides + read_only_write_only_model_type = ( + config.read_only_write_only_model_type + if read_only_write_only_model_type is None + else read_only_write_only_model_type + ) + field_type_collision_strategy = ( + config.field_type_collision_strategy + if field_type_collision_strategy is None + else field_type_collision_strategy + ) + target_pydantic_version = ( + config.target_pydantic_version if target_pydantic_version is None else target_pydantic_version + ) + self.keyword_only = keyword_only self.target_pydantic_version = target_pydantic_version self.frozen_dataclasses = frozen_dataclasses diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index b583399e6..cb23535be 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -57,6 +57,8 @@ from collections import defaultdict from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence + from datamodel_code_generator.config import GraphQLParserConfig + # graphql-core >=3.2.7 removed TypeResolvers in favor of TypeFields.kind. # Normalize to a single callable for resolving type kinds. try: # graphql-core < 3.2.7 @@ -103,6 +105,7 @@ def __init__( # noqa: PLR0913 self, source: str | Path | ParseResult, *, + config: GraphQLParserConfig | None = None, data_model_type: type[DataModel] = pydantic_model.BaseModel, data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, data_model_scalar_type: type[DataModel] = DataTypeScalar, @@ -217,8 +220,15 @@ def __init__( # noqa: PLR0913 target_pydantic_version: TargetPydanticVersion | None = None, ) -> None: """Initialize the GraphQL parser with configuration options.""" + if config is not None: + if config.data_model_scalar_type is not None and data_model_scalar_type is DataTypeScalar: + data_model_scalar_type = config.data_model_scalar_type + if config.data_model_union_type is not None and data_model_union_type is DataTypeUnion: + data_model_union_type = config.data_model_union_type + super().__init__( source=source, + config=config, data_model_type=data_model_type, data_model_root_type=data_model_root_type, data_type_manager_type=data_type_manager_type, diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 4764905f0..1b7cf4641 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -98,6 +98,8 @@ if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence + from datamodel_code_generator.config import ParserConfig + def unescape_json_pointer_segment(segment: str) -> str: """Unescape JSON pointer segment by converting escape sequences and percent-encoding.""" @@ -635,6 +637,7 @@ def __init__( # noqa: PLR0913 self, source: str | Path | list[Path] | ParseResult, *, + config: ParserConfig | None = None, data_model_type: type[DataModel] = pydantic_model.BaseModel, data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, @@ -750,6 +753,7 @@ def __init__( # noqa: PLR0913 target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime super().__init__( source=source, + config=config, data_model_type=data_model_type, data_model_root_type=data_model_root_type, data_type_manager_type=data_type_manager_type, diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index fae1af0a5..5b07e3610 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -58,6 +58,7 @@ from collections.abc import Callable, Iterable, Mapping, Sequence from urllib.parse import ParseResult + from datamodel_code_generator.config import OpenAPIParserConfig from datamodel_code_generator.parser import DefaultPutDict @@ -185,6 +186,7 @@ def __init__( # noqa: PLR0913 self, source: str | Path | list[Path] | ParseResult, *, + config: OpenAPIParserConfig | None = None, data_model_type: type[DataModel] = pydantic_model.BaseModel, data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, @@ -300,9 +302,17 @@ def __init__( # noqa: PLR0913 target_pydantic_version: TargetPydanticVersion | None = None, ) -> None: """Initialize the OpenAPI parser with extensive configuration options.""" + if config is not None: + openapi_scopes = config.openapi_scopes if openapi_scopes is None else openapi_scopes + include_path_parameters = include_path_parameters or config.include_path_parameters + use_status_code_in_response_name = ( + use_status_code_in_response_name or config.use_status_code_in_response_name + ) + target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime super().__init__( source=source, + config=config, data_model_type=data_model_type, data_model_root_type=data_model_root_type, data_type_manager_type=data_type_manager_type, diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 7042596bd..49347e081 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -41,7 +41,7 @@ from pathlib import Path from urllib.parse import ParseResult - from datamodel_code_generator.config import GenerateConfig + from datamodel_code_generator.config import GenerateConfig, ParserConfig from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion from datamodel_code_generator.model.dataclass import DataclassArguments from datamodel_code_generator.model.pydantic import DataTypeManager @@ -183,6 +183,7 @@ def __init__( self, source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, + config: ParserConfig | None = None, data_model_type: type[DataModel] = BaseModel, data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, @@ -603,3 +604,134 @@ def test_generate_with_config_produces_same_result_as_kwargs(tmp_path: Path) -> kwargs_content = output_kwargs.read_text(encoding="utf-8") config_content = output_config.read_text(encoding="utf-8") assert kwargs_content == config_content, "Output differs between kwargs and config" + + +@PYDANTIC_V2_SKIP +def test_jsonschema_parser_with_config() -> None: + """Test that JsonSchemaParser works correctly with ParserConfig.""" + from collections import defaultdict + + from datamodel_code_generator.config import ParserConfig + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + ParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + + extra_data: defaultdict[str, dict[str, Any]] = defaultdict(dict) + extra_data["TestModel"]["custom_key"] = "custom_value" + + config = ParserConfig( + validation=True, + snake_case_field=True, + use_schema_description=True, + reuse_model=True, + extra_template_data=extra_data, + ) + + parser = JsonSchemaParser( + source='{"type": "object", "properties": {"testField": {"type": "string"}}}', + config=config, + ) + + assert parser.validation is True + assert parser.snake_case_field is True + assert parser.use_schema_description is True + assert parser.reuse_model is True + assert parser.extra_template_data is not None + assert parser.extra_template_data["TestModel"]["custom_key"] == "custom_value" + + +@PYDANTIC_V2_SKIP +def test_openapi_parser_with_config() -> None: + """Test that OpenAPIParser works correctly with OpenAPIParserConfig.""" + from datamodel_code_generator.config import OpenAPIParserConfig + from datamodel_code_generator.enums import OpenAPIScope + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.parser.openapi import OpenAPIParser + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + OpenAPIParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + + config = OpenAPIParserConfig( + validation=True, + snake_case_field=True, + openapi_scopes=[OpenAPIScope.Schemas, OpenAPIScope.Paths], + include_path_parameters=True, + use_status_code_in_response_name=True, + ) + + openapi_spec = """{ + "openapi": "3.0.0", + "info": {"title": "Test", "version": "1.0.0"}, + "paths": {}, + "components": {"schemas": {"Test": {"type": "object"}}} + }""" + + parser = OpenAPIParser( + source=openapi_spec, + config=config, + ) + + assert parser.validation is True + assert parser.snake_case_field is True + assert parser.open_api_scopes == [OpenAPIScope.Schemas, OpenAPIScope.Paths] + assert parser.include_path_parameters is True + assert parser.use_status_code_in_response_name is True + + +@PYDANTIC_V2_SKIP +def test_graphql_parser_with_config() -> None: + """Test that GraphQLParser works correctly with GraphQLParserConfig.""" + from datamodel_code_generator.config import GraphQLParserConfig + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.model.scalar import DataTypeScalar + from datamodel_code_generator.model.union import DataTypeUnion + from datamodel_code_generator.parser.graphql import GraphQLParser + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + GraphQLParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + + config = GraphQLParserConfig( + validation=True, + snake_case_field=True, + data_model_scalar_type=DataTypeScalar, + data_model_union_type=DataTypeUnion, + ) + + graphql_schema = """ + type Query { + test: String + } + """ + + parser = GraphQLParser( + source=graphql_schema, + config=config, + ) + + assert parser.validation is True + assert parser.snake_case_field is True + assert parser.data_model_scalar_type is DataTypeScalar + assert parser.data_model_union_type is DataTypeUnion From 8f4aff3171b31fdc2961cc766401ce794b2e0a45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 30 Dec 2025 13:00:40 +0000 Subject: [PATCH 2/7] docs: update CLI reference documentation and prompt data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- docs/cli-reference/model-customization.md | 6 +++++- docs/cli-reference/template-customization.md | 12 +++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/cli-reference/model-customization.md b/docs/cli-reference/model-customization.md index 1e4f78399..f1931e154 100644 --- a/docs/cli-reference/model-customization.md +++ b/docs/cli-reference/model-customization.md @@ -1368,11 +1368,15 @@ model types (Pydantic v1/v2, dataclass, TypedDict, msgspec) handle const fields. from __future__ import annotations + from typing import Literal + from pydantic import BaseModel, Field class Api(BaseModel): - version: str = Field('v1', const=True, description='The version of this API') + version: Literal['v1'] = Field( + 'v1', const=True, description='The version of this API' + ) ``` === "Pydantic v2" diff --git a/docs/cli-reference/template-customization.md b/docs/cli-reference/template-customization.md index ef781e2d6..fd54c2140 100644 --- a/docs/cli-reference/template-customization.md +++ b/docs/cli-reference/template-customization.md @@ -2553,11 +2553,15 @@ helps maintain consistency with codebases that prefer double-quote formatting. from __future__ import annotations + from typing import Literal + from pydantic import BaseModel, Field, confloat class MapState1(BaseModel): - map_view_mode: str = Field("MODE_2D", alias="mapViewMode", const=True) + map_view_mode: Literal["MODE_2D"] = Field( + "MODE_2D", alias="mapViewMode", const=True + ) class MapState2(BaseModel): @@ -2567,8 +2571,10 @@ helps maintain consistency with codebases that prefer double-quote formatting. bearing: Bearing | None = None pitch: Pitch drag_rotate: DragRotate | None = Field(None, alias="dragRotate") - map_split_mode: str = Field("SWIPE_COMPARE", alias="mapSplitMode", const=True) - is_split: bool = Field(True, alias="isSplit", const=True) + map_split_mode: Literal["SWIPE_COMPARE"] = Field( + "SWIPE_COMPARE", alias="mapSplitMode", const=True + ) + is_split: Literal[True] = Field(True, alias="isSplit", const=True) class MapState3(BaseModel): From 2896b196d83a5b7068bd8c89446aa35dc4bb0c96 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 30 Dec 2025 13:59:32 +0000 Subject: [PATCH 3/7] Simplify Parser __init__ signatures with config, **options pattern --- src/datamodel_code_generator/parser/base.py | 452 ++++++------------ .../parser/graphql.py | 274 +---------- .../parser/jsonschema.py | 249 +--------- .../parser/openapi.py | 278 +---------- .../test_public_api_signature_baseline.py | 121 +---- 5 files changed, 189 insertions(+), 1185 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 505a198d3..7a672c1bd 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -31,17 +31,13 @@ Error, FieldTypeCollisionStrategy, ModuleSplitMode, - NamingStrategy, ReadOnlyWriteOnlyModelType, ReuseScope, - TargetPydanticVersion, YamlValue, ) from datamodel_code_generator.format import ( DEFAULT_FORMATTERS, CodeFormatter, - DateClassType, - DatetimeClassType, Formatter, PythonVersion, PythonVersionMin, @@ -75,16 +71,30 @@ from datamodel_code_generator.parser._graph import stable_toposort from datamodel_code_generator.parser._scc import find_circular_sccs, strongly_connected_components from datamodel_code_generator.reference import ModelResolver, ModelType, Reference -from datamodel_code_generator.types import DataType, DataTypeManager, StrictTypes +from datamodel_code_generator.types import DataType, DataTypeManager from datamodel_code_generator.util import camel_to_snake, model_copy, model_dump if TYPE_CHECKING: - from collections.abc import Iterable, Iterator, Mapping, Sequence + from collections.abc import Iterable, Iterator, Sequence - from datamodel_code_generator import DataclassArguments from datamodel_code_generator.config import ParserConfig +def _get_option( + options: dict[str, Any], + key: str, + config: ParserConfig | None, + *, + default: Any, +) -> Any: + """Get option value with precedence: options > config > default.""" + if key in options: + return options[key] + if config is not None: + return getattr(config, key, default) + return default + + @runtime_checkable class HashableComparable(Hashable, Protocol): """Protocol for types that are both hashable and support comparison.""" @@ -687,316 +697,136 @@ class Parser(ABC): parse_raw() to handle specific schema formats. """ - def __init__( # noqa: PLR0912, PLR0913, PLR0914, PLR0915 + def __init__( # noqa: PLR0912, PLR0914, PLR0915 self, source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, config: ParserConfig | None = None, - data_model_type: type[DataModel] = pydantic_model.BaseModel, - data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, - data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, - data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField, - base_class: str | None = None, - base_class_map: dict[str, str] | None = None, - additional_imports: list[str] | None = None, - class_decorators: list[str] | None = None, - custom_template_dir: Path | None = None, - extra_template_data: defaultdict[str, dict[str, Any]] | None = None, - target_python_version: PythonVersion = PythonVersionMin, - dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = None, - validation: bool = False, - field_constraints: bool = False, - snake_case_field: bool = False, - strip_default_none: bool = False, - aliases: Mapping[str, str] | None = None, - allow_population_by_field_name: bool = False, - apply_default_values_for_required_fields: bool = False, - allow_extra_fields: bool = False, - extra_fields: str | None = None, - use_generic_base_class: bool = False, - force_optional_for_required_fields: bool = False, - class_name: str | None = None, - use_standard_collections: bool = False, - base_path: Path | None = None, - use_schema_description: bool = False, - use_field_description: bool = False, - use_field_description_example: bool = False, - use_attribute_docstrings: bool = False, - use_inline_field_description: bool = False, - use_default_kwarg: bool = False, - reuse_model: bool = False, - reuse_scope: ReuseScope | None = None, - shared_module_name: str = DEFAULT_SHARED_MODULE_NAME, - encoding: str = "utf-8", - enum_field_as_literal: LiteralType | None = None, - enum_field_as_literal_map: dict[str, str] | None = None, - ignore_enum_constraints: bool = False, - set_default_enum_member: bool = False, - use_subclass_enum: bool = False, - use_specialized_enum: bool = True, - strict_nullable: bool = False, - use_generic_container_types: bool = False, - enable_faux_immutability: bool = False, - remote_text_cache: DefaultPutDict[str, str] | None = None, - disable_appending_item_suffix: bool = False, - strict_types: Sequence[StrictTypes] | None = None, - empty_enum_field_name: str | None = None, - custom_class_name_generator: Callable[[str], str] | None = title_to_class_name, - field_extra_keys: set[str] | None = None, - field_include_all_keys: bool = False, - field_extra_keys_without_x_prefix: set[str] | None = None, - model_extra_keys: set[str] | None = None, - model_extra_keys_without_x_prefix: set[str] | None = None, - wrap_string_literal: bool | None = None, - use_title_as_name: bool = False, - use_operation_id_as_name: bool = False, - use_unique_items_as_set: bool = False, - use_tuple_for_fixed_items: bool = False, - allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints, - http_headers: Sequence[tuple[str, str]] | None = None, - http_ignore_tls: bool = False, - http_timeout: float | None = None, - use_annotated: bool = False, - use_serialize_as_any: bool = False, - use_non_positive_negative_number_constrained_types: bool = False, - use_decimal_for_multiple_of: bool = False, - original_field_name_delimiter: str | None = None, - use_double_quotes: bool = False, - use_union_operator: bool = False, - allow_responses_without_content: bool = False, - collapse_root_models: bool = False, - collapse_root_models_name_strategy: CollapseRootModelsNameStrategy | None = None, - collapse_reuse_models: bool = False, - skip_root_model: bool = False, - use_type_alias: bool = False, - special_field_name_prefix: str | None = None, - remove_special_field_name_prefix: bool = False, - capitalise_enum_members: bool = False, - keep_model_order: bool = False, - use_one_literal_as_default: bool = False, - use_enum_values_in_discriminator: bool = False, - known_third_party: list[str] | None = None, - custom_formatters: list[str] | None = None, - custom_formatters_kwargs: dict[str, Any] | None = None, - use_pendulum: bool = False, - use_standard_primitive_types: bool = False, - http_query_parameters: Sequence[tuple[str, str]] | None = None, - treat_dot_as_module: bool | None = None, - use_exact_imports: bool = False, - default_field_extras: dict[str, Any] | None = None, - target_datetime_class: DatetimeClassType | None = None, - target_date_class: DateClassType | None = None, - keyword_only: bool = False, - frozen_dataclasses: bool = False, - no_alias: bool = False, - use_frozen_field: bool = False, - use_default_factory_for_optional_nested_models: bool = False, - formatters: list[Formatter] = DEFAULT_FORMATTERS, - defer_formatting: bool = False, - parent_scoped_naming: bool = False, - naming_strategy: NamingStrategy | None = None, - duplicate_name_suffix: dict[str, str] | None = None, - dataclass_arguments: DataclassArguments | None = None, - type_mappings: list[str] | None = None, - type_overrides: dict[str, str] | None = None, - read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None, - field_type_collision_strategy: FieldTypeCollisionStrategy | None = None, - target_pydantic_version: TargetPydanticVersion | None = None, + **options: Any, ) -> None: """Initialize the Parser with configuration options.""" - if config is not None: - data_model_type = config.data_model_type if data_model_type is pydantic_model.BaseModel else data_model_type - data_model_root_type = ( - config.data_model_root_type - if data_model_root_type is pydantic_model.CustomRootType - else data_model_root_type - ) - data_type_manager_type = ( - config.data_type_manager_type - if data_type_manager_type is pydantic_model.DataTypeManager - else data_type_manager_type - ) - data_model_field_type = ( - config.data_model_field_type - if data_model_field_type is pydantic_model.DataModelField - else data_model_field_type - ) - base_class = config.base_class if base_class is None else base_class - base_class_map = config.base_class_map if base_class_map is None else base_class_map - additional_imports = config.additional_imports if additional_imports is None else additional_imports - class_decorators = config.class_decorators if class_decorators is None else class_decorators - custom_template_dir = config.custom_template_dir if custom_template_dir is None else custom_template_dir - if extra_template_data is None and config.extra_template_data is not None: - extra_template_data = defaultdict(dict, config.extra_template_data) - target_python_version = ( - config.target_python_version if target_python_version == PythonVersionMin else target_python_version - ) - dump_resolve_reference_action = ( - config.dump_resolve_reference_action - if dump_resolve_reference_action is None - else dump_resolve_reference_action - ) - validation = validation or config.validation - field_constraints = field_constraints or config.field_constraints - snake_case_field = snake_case_field or config.snake_case_field - strip_default_none = strip_default_none or config.strip_default_none - aliases = config.aliases if aliases is None else aliases - allow_population_by_field_name = allow_population_by_field_name or config.allow_population_by_field_name - apply_default_values_for_required_fields = ( - apply_default_values_for_required_fields or config.apply_default_values_for_required_fields - ) - allow_extra_fields = allow_extra_fields or config.allow_extra_fields - extra_fields = config.extra_fields if extra_fields is None else extra_fields - use_generic_base_class = use_generic_base_class or config.use_generic_base_class - force_optional_for_required_fields = ( - force_optional_for_required_fields or config.force_optional_for_required_fields - ) - class_name = config.class_name if class_name is None else class_name - use_standard_collections = use_standard_collections or config.use_standard_collections - base_path = config.base_path if base_path is None else base_path - use_schema_description = use_schema_description or config.use_schema_description - use_field_description = use_field_description or config.use_field_description - use_field_description_example = use_field_description_example or config.use_field_description_example - use_attribute_docstrings = use_attribute_docstrings or config.use_attribute_docstrings - use_inline_field_description = use_inline_field_description or config.use_inline_field_description - use_default_kwarg = use_default_kwarg or config.use_default_kwarg - reuse_model = reuse_model or config.reuse_model - reuse_scope = config.reuse_scope if reuse_scope is None else reuse_scope - shared_module_name = ( - config.shared_module_name if shared_module_name == DEFAULT_SHARED_MODULE_NAME else shared_module_name - ) - encoding = config.encoding if encoding == "utf-8" else encoding - enum_field_as_literal = ( - config.enum_field_as_literal if enum_field_as_literal is None else enum_field_as_literal - ) - enum_field_as_literal_map = ( - config.enum_field_as_literal_map if enum_field_as_literal_map is None else enum_field_as_literal_map - ) - ignore_enum_constraints = ignore_enum_constraints or config.ignore_enum_constraints - set_default_enum_member = set_default_enum_member or config.set_default_enum_member - use_subclass_enum = use_subclass_enum or config.use_subclass_enum - use_specialized_enum = config.use_specialized_enum if use_specialized_enum else False - strict_nullable = strict_nullable or config.strict_nullable - use_generic_container_types = use_generic_container_types or config.use_generic_container_types - enable_faux_immutability = enable_faux_immutability or config.enable_faux_immutability - remote_text_cache = config.remote_text_cache if remote_text_cache is None else remote_text_cache - disable_appending_item_suffix = disable_appending_item_suffix or config.disable_appending_item_suffix - strict_types = config.strict_types if strict_types is None else strict_types - empty_enum_field_name = ( - config.empty_enum_field_name if empty_enum_field_name is None else empty_enum_field_name - ) - custom_class_name_generator = ( - config.custom_class_name_generator - if custom_class_name_generator is title_to_class_name - else custom_class_name_generator - ) - field_extra_keys = config.field_extra_keys if field_extra_keys is None else field_extra_keys - field_include_all_keys = field_include_all_keys or config.field_include_all_keys - field_extra_keys_without_x_prefix = ( - config.field_extra_keys_without_x_prefix - if field_extra_keys_without_x_prefix is None - else field_extra_keys_without_x_prefix - ) - model_extra_keys = config.model_extra_keys if model_extra_keys is None else model_extra_keys - model_extra_keys_without_x_prefix = ( - config.model_extra_keys_without_x_prefix - if model_extra_keys_without_x_prefix is None - else model_extra_keys_without_x_prefix - ) - wrap_string_literal = config.wrap_string_literal if wrap_string_literal is None else wrap_string_literal - use_title_as_name = use_title_as_name or config.use_title_as_name - use_operation_id_as_name = use_operation_id_as_name or config.use_operation_id_as_name - use_unique_items_as_set = use_unique_items_as_set or config.use_unique_items_as_set - use_tuple_for_fixed_items = use_tuple_for_fixed_items or config.use_tuple_for_fixed_items - allof_merge_mode = ( - config.allof_merge_mode if allof_merge_mode == AllOfMergeMode.Constraints else allof_merge_mode - ) - http_headers = config.http_headers if http_headers is None else http_headers - http_ignore_tls = http_ignore_tls or config.http_ignore_tls - http_timeout = config.http_timeout if http_timeout is None else http_timeout - use_annotated = use_annotated or config.use_annotated - use_serialize_as_any = use_serialize_as_any or config.use_serialize_as_any - use_non_positive_negative_number_constrained_types = ( - use_non_positive_negative_number_constrained_types - or config.use_non_positive_negative_number_constrained_types - ) - use_decimal_for_multiple_of = use_decimal_for_multiple_of or config.use_decimal_for_multiple_of - original_field_name_delimiter = ( - config.original_field_name_delimiter - if original_field_name_delimiter is None - else original_field_name_delimiter - ) - use_double_quotes = use_double_quotes or config.use_double_quotes - use_union_operator = use_union_operator or config.use_union_operator - allow_responses_without_content = allow_responses_without_content or config.allow_responses_without_content - collapse_root_models = collapse_root_models or config.collapse_root_models - collapse_root_models_name_strategy = ( - config.collapse_root_models_name_strategy - if collapse_root_models_name_strategy is None - else collapse_root_models_name_strategy - ) - collapse_reuse_models = collapse_reuse_models or config.collapse_reuse_models - skip_root_model = skip_root_model or config.skip_root_model - use_type_alias = use_type_alias or config.use_type_alias - special_field_name_prefix = ( - config.special_field_name_prefix if special_field_name_prefix is None else special_field_name_prefix - ) - remove_special_field_name_prefix = ( - remove_special_field_name_prefix or config.remove_special_field_name_prefix - ) - capitalise_enum_members = capitalise_enum_members or config.capitalise_enum_members - keep_model_order = keep_model_order or config.keep_model_order - use_one_literal_as_default = use_one_literal_as_default or config.use_one_literal_as_default - use_enum_values_in_discriminator = ( - use_enum_values_in_discriminator or config.use_enum_values_in_discriminator - ) - known_third_party = config.known_third_party if known_third_party is None else known_third_party - custom_formatters = config.custom_formatters if custom_formatters is None else custom_formatters - custom_formatters_kwargs = ( - config.custom_formatters_kwargs if custom_formatters_kwargs is None else custom_formatters_kwargs - ) - use_pendulum = use_pendulum or config.use_pendulum - use_standard_primitive_types = use_standard_primitive_types or config.use_standard_primitive_types - http_query_parameters = ( - config.http_query_parameters if http_query_parameters is None else http_query_parameters - ) - treat_dot_as_module = config.treat_dot_as_module if treat_dot_as_module is None else treat_dot_as_module - use_exact_imports = use_exact_imports or config.use_exact_imports - default_field_extras = config.default_field_extras if default_field_extras is None else default_field_extras - target_datetime_class = ( - config.target_datetime_class if target_datetime_class is None else target_datetime_class - ) - target_date_class = config.target_date_class if target_date_class is None else target_date_class - keyword_only = keyword_only or config.keyword_only - frozen_dataclasses = frozen_dataclasses or config.frozen_dataclasses - no_alias = no_alias or config.no_alias - use_frozen_field = use_frozen_field or config.use_frozen_field - use_default_factory_for_optional_nested_models = ( - use_default_factory_for_optional_nested_models or config.use_default_factory_for_optional_nested_models - ) - formatters = config.formatters if formatters == DEFAULT_FORMATTERS else formatters - defer_formatting = defer_formatting or config.defer_formatting - parent_scoped_naming = parent_scoped_naming or config.parent_scoped_naming - naming_strategy = config.naming_strategy if naming_strategy is None else naming_strategy - duplicate_name_suffix = ( - config.duplicate_name_suffix if duplicate_name_suffix is None else duplicate_name_suffix - ) - dataclass_arguments = config.dataclass_arguments if dataclass_arguments is None else dataclass_arguments - type_mappings = config.type_mappings if type_mappings is None else type_mappings - type_overrides = config.type_overrides if type_overrides is None else type_overrides - read_only_write_only_model_type = ( - config.read_only_write_only_model_type - if read_only_write_only_model_type is None - else read_only_write_only_model_type - ) - field_type_collision_strategy = ( - config.field_type_collision_strategy - if field_type_collision_strategy is None - else field_type_collision_strategy - ) - target_pydantic_version = ( - config.target_pydantic_version if target_pydantic_version is None else target_pydantic_version - ) + + def opt(key: str, *, default: Any) -> Any: + return _get_option(options, key, config, default=default) + + data_model_type = opt("data_model_type", default=pydantic_model.BaseModel) + data_model_root_type = opt("data_model_root_type", default=pydantic_model.CustomRootType) + data_type_manager_type = opt("data_type_manager_type", default=pydantic_model.DataTypeManager) + data_model_field_type = opt("data_model_field_type", default=pydantic_model.DataModelField) + base_class = opt("base_class", default=None) + base_class_map = opt("base_class_map", default=None) + additional_imports = opt("additional_imports", default=None) + class_decorators = opt("class_decorators", default=None) + custom_template_dir = opt("custom_template_dir", default=None) + extra_template_data = opt("extra_template_data", default=None) + target_python_version = opt("target_python_version", default=PythonVersionMin) + dump_resolve_reference_action = opt("dump_resolve_reference_action", default=None) + validation = opt("validation", default=False) + field_constraints = opt("field_constraints", default=False) + snake_case_field = opt("snake_case_field", default=False) + strip_default_none = opt("strip_default_none", default=False) + aliases = opt("aliases", default=None) + allow_population_by_field_name = opt("allow_population_by_field_name", default=False) + apply_default_values_for_required_fields = opt("apply_default_values_for_required_fields", default=False) + allow_extra_fields = opt("allow_extra_fields", default=False) + extra_fields = opt("extra_fields", default=None) + use_generic_base_class = opt("use_generic_base_class", default=False) + force_optional_for_required_fields = opt("force_optional_for_required_fields", default=False) + class_name = opt("class_name", default=None) + use_standard_collections = opt("use_standard_collections", default=False) + base_path = opt("base_path", default=None) + use_schema_description = opt("use_schema_description", default=False) + use_field_description = opt("use_field_description", default=False) + use_field_description_example = opt("use_field_description_example", default=False) + use_attribute_docstrings = opt("use_attribute_docstrings", default=False) + use_inline_field_description = opt("use_inline_field_description", default=False) + use_default_kwarg = opt("use_default_kwarg", default=False) + reuse_model = opt("reuse_model", default=False) + reuse_scope = opt("reuse_scope", default=None) + shared_module_name = opt("shared_module_name", default=DEFAULT_SHARED_MODULE_NAME) + encoding = opt("encoding", default="utf-8") + enum_field_as_literal = opt("enum_field_as_literal", default=None) + enum_field_as_literal_map = opt("enum_field_as_literal_map", default=None) + ignore_enum_constraints = opt("ignore_enum_constraints", default=False) + set_default_enum_member = opt("set_default_enum_member", default=False) + use_subclass_enum = opt("use_subclass_enum", default=False) + use_specialized_enum = opt("use_specialized_enum", default=True) + strict_nullable = opt("strict_nullable", default=False) + use_generic_container_types = opt("use_generic_container_types", default=False) + enable_faux_immutability = opt("enable_faux_immutability", default=False) + remote_text_cache = opt("remote_text_cache", default=None) + disable_appending_item_suffix = opt("disable_appending_item_suffix", default=False) + strict_types = opt("strict_types", default=None) + empty_enum_field_name = opt("empty_enum_field_name", default=None) + custom_class_name_generator = opt("custom_class_name_generator", default=None) + field_extra_keys = opt("field_extra_keys", default=None) + field_include_all_keys = opt("field_include_all_keys", default=False) + field_extra_keys_without_x_prefix = opt("field_extra_keys_without_x_prefix", default=None) + model_extra_keys = opt("model_extra_keys", default=None) + model_extra_keys_without_x_prefix = opt("model_extra_keys_without_x_prefix", default=None) + wrap_string_literal = opt("wrap_string_literal", default=None) + use_title_as_name = opt("use_title_as_name", default=False) + use_operation_id_as_name = opt("use_operation_id_as_name", default=False) + use_unique_items_as_set = opt("use_unique_items_as_set", default=False) + use_tuple_for_fixed_items = opt("use_tuple_for_fixed_items", default=False) + allof_merge_mode = opt("allof_merge_mode", default=AllOfMergeMode.Constraints) + http_headers = opt("http_headers", default=None) + http_ignore_tls = opt("http_ignore_tls", default=False) + http_timeout = opt("http_timeout", default=None) + use_annotated = opt("use_annotated", default=False) + use_serialize_as_any = opt("use_serialize_as_any", default=False) + use_non_positive_negative_number_constrained_types = opt( + "use_non_positive_negative_number_constrained_types", default=False + ) + use_decimal_for_multiple_of = opt("use_decimal_for_multiple_of", default=False) + original_field_name_delimiter = opt("original_field_name_delimiter", default=None) + use_double_quotes = opt("use_double_quotes", default=False) + use_union_operator = opt("use_union_operator", default=False) + allow_responses_without_content = opt("allow_responses_without_content", default=False) + collapse_root_models = opt("collapse_root_models", default=False) + collapse_root_models_name_strategy = opt("collapse_root_models_name_strategy", default=None) + collapse_reuse_models = opt("collapse_reuse_models", default=False) + skip_root_model = opt("skip_root_model", default=False) + use_type_alias = opt("use_type_alias", default=False) + special_field_name_prefix = opt("special_field_name_prefix", default=None) + remove_special_field_name_prefix = opt("remove_special_field_name_prefix", default=False) + capitalise_enum_members = opt("capitalise_enum_members", default=False) + keep_model_order = opt("keep_model_order", default=False) + use_one_literal_as_default = opt("use_one_literal_as_default", default=False) + use_enum_values_in_discriminator = opt("use_enum_values_in_discriminator", default=False) + known_third_party = opt("known_third_party", default=None) + custom_formatters = opt("custom_formatters", default=None) + custom_formatters_kwargs = opt("custom_formatters_kwargs", default=None) + use_pendulum = opt("use_pendulum", default=False) + use_standard_primitive_types = opt("use_standard_primitive_types", default=False) + http_query_parameters = opt("http_query_parameters", default=None) + treat_dot_as_module = opt("treat_dot_as_module", default=None) + use_exact_imports = opt("use_exact_imports", default=False) + default_field_extras = opt("default_field_extras", default=None) + target_datetime_class = opt("target_datetime_class", default=None) + target_date_class = opt("target_date_class", default=None) + keyword_only = opt("keyword_only", default=False) + frozen_dataclasses = opt("frozen_dataclasses", default=False) + no_alias = opt("no_alias", default=False) + use_frozen_field = opt("use_frozen_field", default=False) + use_default_factory_for_optional_nested_models = opt( + "use_default_factory_for_optional_nested_models", default=False + ) + formatters = opt("formatters", default=DEFAULT_FORMATTERS) + defer_formatting = opt("defer_formatting", default=False) + parent_scoped_naming = opt("parent_scoped_naming", default=False) + naming_strategy = opt("naming_strategy", default=None) + duplicate_name_suffix = opt("duplicate_name_suffix", default=None) + dataclass_arguments = opt("dataclass_arguments", default=None) + type_mappings = opt("type_mappings", default=None) + type_overrides = opt("type_overrides", default=None) + read_only_write_only_model_type = opt("read_only_write_only_model_type", default=None) + field_type_collision_strategy = opt("field_type_collision_strategy", default=None) + target_pydantic_version = opt("target_pydantic_version", default=None) + + # Convert extra_template_data to defaultdict if needed + if extra_template_data is not None and not isinstance(extra_template_data, defaultdict): + extra_template_data = defaultdict(dict, extra_template_data) self.keyword_only = keyword_only self.target_pydantic_version = target_pydantic_version diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index cb23535be..457fc5e21 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -13,38 +13,23 @@ ) from urllib.parse import ParseResult -from datamodel_code_generator import ( - DEFAULT_SHARED_MODULE_NAME, - AllOfMergeMode, - CollapseRootModelsNameStrategy, - DataclassArguments, - DefaultPutDict, - FieldTypeCollisionStrategy, - LiteralType, - NamingStrategy, - PythonVersion, - PythonVersionMin, - ReadOnlyWriteOnlyModelType, - ReuseScope, - TargetPydanticVersion, - snooper_to_methods, -) -from datamodel_code_generator.format import DEFAULT_FORMATTERS, DateClassType, DatetimeClassType, Formatter -from datamodel_code_generator.model import DataModel, DataModelFieldBase -from datamodel_code_generator.model import pydantic as pydantic_model +from datamodel_code_generator import snooper_to_methods +from datamodel_code_generator.format import DatetimeClassType from datamodel_code_generator.model.dataclass import DataClass from datamodel_code_generator.model.enum import SPECIALIZED_ENUM_TYPE_MATCH, Enum from datamodel_code_generator.model.pydantic_v2.dataclass import DataClass as PydanticV2DataClass from datamodel_code_generator.model.scalar import DataTypeScalar from datamodel_code_generator.model.union import DataTypeUnion +from datamodel_code_generator.parser import LiteralType from datamodel_code_generator.parser.base import ( DataType, Parser, Source, + _get_option, escape_characters, ) from datamodel_code_generator.reference import ModelType, Reference -from datamodel_code_generator.types import DataTypeManager, StrictTypes, Types +from datamodel_code_generator.types import Types try: import graphql @@ -54,10 +39,10 @@ if TYPE_CHECKING: - from collections import defaultdict - from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence + from collections.abc import Iterator from datamodel_code_generator.config import GraphQLParserConfig + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase # graphql-core >=3.2.7 removed TypeResolvers in favor of TypeFields.kind. # Normalize to a single callable for resolving type kinds. @@ -101,245 +86,24 @@ class GraphQLParser(Parser): graphql.type.introspection.TypeKind.UNION, ] - def __init__( # noqa: PLR0913 + def __init__( self, source: str | Path | ParseResult, *, config: GraphQLParserConfig | None = None, - data_model_type: type[DataModel] = pydantic_model.BaseModel, - data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, - data_model_scalar_type: type[DataModel] = DataTypeScalar, - data_model_union_type: type[DataModel] = DataTypeUnion, - data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, - data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField, - base_class: str | None = None, - base_class_map: dict[str, str] | None = None, - additional_imports: list[str] | None = None, - class_decorators: list[str] | None = None, - custom_template_dir: Path | None = None, - extra_template_data: defaultdict[str, dict[str, Any]] | None = None, - target_python_version: PythonVersion = PythonVersionMin, - dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = None, - validation: bool = False, - field_constraints: bool = False, - snake_case_field: bool = False, - strip_default_none: bool = False, - aliases: Mapping[str, str] | None = None, - allow_population_by_field_name: bool = False, - apply_default_values_for_required_fields: bool = False, - allow_extra_fields: bool = False, - extra_fields: str | None = None, - use_generic_base_class: bool = False, - force_optional_for_required_fields: bool = False, - class_name: str | None = None, - use_standard_collections: bool = False, - base_path: Path | None = None, - use_schema_description: bool = False, - use_field_description: bool = False, - use_field_description_example: bool = False, - use_attribute_docstrings: bool = False, - use_inline_field_description: bool = False, - use_default_kwarg: bool = False, - reuse_model: bool = False, - reuse_scope: ReuseScope | None = None, - shared_module_name: str = DEFAULT_SHARED_MODULE_NAME, - encoding: str = "utf-8", - enum_field_as_literal: LiteralType | None = None, - enum_field_as_literal_map: dict[str, str] | None = None, - ignore_enum_constraints: bool = False, - set_default_enum_member: bool = False, - use_subclass_enum: bool = False, - use_specialized_enum: bool = True, - strict_nullable: bool = False, - use_generic_container_types: bool = False, - enable_faux_immutability: bool = False, - remote_text_cache: DefaultPutDict[str, str] | None = None, - disable_appending_item_suffix: bool = False, - strict_types: Sequence[StrictTypes] | None = None, - empty_enum_field_name: str | None = None, - custom_class_name_generator: Callable[[str], str] | None = None, - field_extra_keys: set[str] | None = None, - field_include_all_keys: bool = False, - field_extra_keys_without_x_prefix: set[str] | None = None, - model_extra_keys: set[str] | None = None, - model_extra_keys_without_x_prefix: set[str] | None = None, - wrap_string_literal: bool | None = None, - use_title_as_name: bool = False, - use_operation_id_as_name: bool = False, - use_unique_items_as_set: bool = False, - use_tuple_for_fixed_items: bool = False, - allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints, - http_headers: Sequence[tuple[str, str]] | None = None, - http_ignore_tls: bool = False, - http_timeout: float | None = None, - use_annotated: bool = False, - use_non_positive_negative_number_constrained_types: bool = False, - use_decimal_for_multiple_of: bool = False, - original_field_name_delimiter: str | None = None, - use_double_quotes: bool = False, - use_union_operator: bool = False, - allow_responses_without_content: bool = False, - collapse_root_models: bool = False, - collapse_root_models_name_strategy: CollapseRootModelsNameStrategy | None = None, - collapse_reuse_models: bool = False, - skip_root_model: bool = False, - use_type_alias: bool = False, - special_field_name_prefix: str | None = None, - remove_special_field_name_prefix: bool = False, - capitalise_enum_members: bool = False, - keep_model_order: bool = False, - use_one_literal_as_default: bool = False, - use_enum_values_in_discriminator: bool = False, - known_third_party: list[str] | None = None, - custom_formatters: list[str] | None = None, - custom_formatters_kwargs: dict[str, Any] | None = None, - use_pendulum: bool = False, - use_standard_primitive_types: bool = False, - http_query_parameters: Sequence[tuple[str, str]] | None = None, - treat_dot_as_module: bool | None = None, - use_exact_imports: bool = False, - default_field_extras: dict[str, Any] | None = None, - target_datetime_class: DatetimeClassType = DatetimeClassType.Datetime, - target_date_class: DateClassType | None = None, - keyword_only: bool = False, - frozen_dataclasses: bool = False, - no_alias: bool = False, - formatters: list[Formatter] = DEFAULT_FORMATTERS, - defer_formatting: bool = False, - parent_scoped_naming: bool = False, - naming_strategy: NamingStrategy | None = None, - duplicate_name_suffix: dict[str, str] | None = None, - dataclass_arguments: DataclassArguments | None = None, - type_mappings: list[str] | None = None, - type_overrides: dict[str, str] | None = None, - read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None, - use_serialize_as_any: bool = False, - use_frozen_field: bool = False, - use_default_factory_for_optional_nested_models: bool = False, - field_type_collision_strategy: FieldTypeCollisionStrategy | None = None, - target_pydantic_version: TargetPydanticVersion | None = None, + **options: Any, ) -> None: """Initialize the GraphQL parser with configuration options.""" - if config is not None: - if config.data_model_scalar_type is not None and data_model_scalar_type is DataTypeScalar: - data_model_scalar_type = config.data_model_scalar_type - if config.data_model_union_type is not None and data_model_union_type is DataTypeUnion: - data_model_union_type = config.data_model_union_type - - super().__init__( - source=source, - config=config, - data_model_type=data_model_type, - data_model_root_type=data_model_root_type, - data_type_manager_type=data_type_manager_type, - data_model_field_type=data_model_field_type, - base_class=base_class, - base_class_map=base_class_map, - additional_imports=additional_imports, - class_decorators=class_decorators, - custom_template_dir=custom_template_dir, - extra_template_data=extra_template_data, - target_python_version=target_python_version, - dump_resolve_reference_action=dump_resolve_reference_action, - validation=validation, - field_constraints=field_constraints, - snake_case_field=snake_case_field, - strip_default_none=strip_default_none, - aliases=aliases, - allow_population_by_field_name=allow_population_by_field_name, - allow_extra_fields=allow_extra_fields, - extra_fields=extra_fields, - use_generic_base_class=use_generic_base_class, - apply_default_values_for_required_fields=apply_default_values_for_required_fields, - force_optional_for_required_fields=force_optional_for_required_fields, - class_name=class_name, - use_standard_collections=use_standard_collections, - base_path=base_path, - use_schema_description=use_schema_description, - use_field_description=use_field_description, - use_field_description_example=use_field_description_example, - use_attribute_docstrings=use_attribute_docstrings, - use_inline_field_description=use_inline_field_description, - use_default_kwarg=use_default_kwarg, - reuse_model=reuse_model, - reuse_scope=reuse_scope, - shared_module_name=shared_module_name, - encoding=encoding, - enum_field_as_literal=enum_field_as_literal, - enum_field_as_literal_map=enum_field_as_literal_map, - ignore_enum_constraints=ignore_enum_constraints, - use_one_literal_as_default=use_one_literal_as_default, - use_enum_values_in_discriminator=use_enum_values_in_discriminator, - set_default_enum_member=set_default_enum_member, - use_subclass_enum=use_subclass_enum, - use_specialized_enum=use_specialized_enum, - strict_nullable=strict_nullable, - use_generic_container_types=use_generic_container_types, - enable_faux_immutability=enable_faux_immutability, - remote_text_cache=remote_text_cache, - disable_appending_item_suffix=disable_appending_item_suffix, - strict_types=strict_types, - empty_enum_field_name=empty_enum_field_name, - custom_class_name_generator=custom_class_name_generator, - field_extra_keys=field_extra_keys, - field_include_all_keys=field_include_all_keys, - field_extra_keys_without_x_prefix=field_extra_keys_without_x_prefix, - model_extra_keys=model_extra_keys, - model_extra_keys_without_x_prefix=model_extra_keys_without_x_prefix, - wrap_string_literal=wrap_string_literal, - use_title_as_name=use_title_as_name, - use_operation_id_as_name=use_operation_id_as_name, - use_unique_items_as_set=use_unique_items_as_set, - use_tuple_for_fixed_items=use_tuple_for_fixed_items, - allof_merge_mode=allof_merge_mode, - http_headers=http_headers, - http_ignore_tls=http_ignore_tls, - http_timeout=http_timeout, - use_annotated=use_annotated, - use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types, - use_decimal_for_multiple_of=use_decimal_for_multiple_of, - original_field_name_delimiter=original_field_name_delimiter, - use_double_quotes=use_double_quotes, - use_union_operator=use_union_operator, - allow_responses_without_content=allow_responses_without_content, - collapse_root_models=collapse_root_models, - collapse_root_models_name_strategy=collapse_root_models_name_strategy, - collapse_reuse_models=collapse_reuse_models, - skip_root_model=skip_root_model, - use_type_alias=use_type_alias, - special_field_name_prefix=special_field_name_prefix, - remove_special_field_name_prefix=remove_special_field_name_prefix, - capitalise_enum_members=capitalise_enum_members, - keep_model_order=keep_model_order, - known_third_party=known_third_party, - custom_formatters=custom_formatters, - custom_formatters_kwargs=custom_formatters_kwargs, - use_pendulum=use_pendulum, - use_standard_primitive_types=use_standard_primitive_types, - http_query_parameters=http_query_parameters, - treat_dot_as_module=treat_dot_as_module, - use_exact_imports=use_exact_imports, - default_field_extras=default_field_extras, - target_datetime_class=target_datetime_class, - target_date_class=target_date_class, - keyword_only=keyword_only, - frozen_dataclasses=frozen_dataclasses, - no_alias=no_alias, - formatters=formatters, - defer_formatting=defer_formatting, - parent_scoped_naming=parent_scoped_naming, - naming_strategy=naming_strategy, - duplicate_name_suffix=duplicate_name_suffix, - dataclass_arguments=dataclass_arguments, - type_mappings=type_mappings, - type_overrides=type_overrides, - read_only_write_only_model_type=read_only_write_only_model_type, - use_serialize_as_any=use_serialize_as_any, - use_frozen_field=use_frozen_field, - use_default_factory_for_optional_nested_models=use_default_factory_for_optional_nested_models, - field_type_collision_strategy=field_type_collision_strategy, - target_pydantic_version=target_pydantic_version, - ) + # Extract GraphQL-specific options + data_model_scalar_type = _get_option(options, "data_model_scalar_type", config, default=DataTypeScalar) + data_model_union_type = _get_option(options, "data_model_union_type", config, default=DataTypeUnion) + use_standard_collections = _get_option(options, "use_standard_collections", config, default=False) + use_union_operator = _get_option(options, "use_union_operator", config, default=False) + + if options.get("target_datetime_class") is None and (config is None or config.target_datetime_class is None): + options["target_datetime_class"] = DatetimeClassType.Datetime + + super().__init__(source=source, config=config, **options) self.data_model_scalar_type = data_model_scalar_type self.data_model_union_type = data_model_union_type diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 4ada8fc84..99e2cba79 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -24,33 +24,20 @@ ) from datamodel_code_generator import ( - DEFAULT_SHARED_MODULE_NAME, AllOfMergeMode, - CollapseRootModelsNameStrategy, - DataclassArguments, - FieldTypeCollisionStrategy, InvalidClassNameError, - NamingStrategy, ReadOnlyWriteOnlyModelType, - ReuseScope, SchemaParseError, - TargetPydanticVersion, YamlValue, load_data, load_data_from_path, snooper_to_methods, ) from datamodel_code_generator.format import ( - DEFAULT_FORMATTERS, - DateClassType, DatetimeClassType, - Formatter, - PythonVersion, - PythonVersionMin, ) from datamodel_code_generator.imports import IMPORT_ANY, Import from datamodel_code_generator.model import DataModel, DataModelFieldBase -from datamodel_code_generator.model import pydantic as pydantic_model from datamodel_code_generator.model.base import UNDEFINED, get_module_name, sanitize_module_name from datamodel_code_generator.model.dataclass import DataClass from datamodel_code_generator.model.enum import ( @@ -72,9 +59,7 @@ from datamodel_code_generator.types import ( ANY, DataType, - DataTypeManager, EmptyDataType, - StrictTypes, Types, UnionIntFloat, extract_qualified_names, @@ -96,7 +81,7 @@ from pydantic import ConfigDict if TYPE_CHECKING: - from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence + from collections.abc import Callable, Generator, Iterable, Iterator from datamodel_code_generator.config import ParserConfig @@ -639,238 +624,18 @@ class JsonSchemaParser(Parser): "ChainMap", }) - def __init__( # noqa: PLR0913 + def __init__( self, source: str | Path | list[Path] | ParseResult, *, config: ParserConfig | None = None, - data_model_type: type[DataModel] = pydantic_model.BaseModel, - data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, - data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, - data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField, - base_class: str | None = None, - base_class_map: dict[str, str] | None = None, - additional_imports: list[str] | None = None, - class_decorators: list[str] | None = None, - custom_template_dir: Path | None = None, - extra_template_data: defaultdict[str, dict[str, Any]] | None = None, - target_python_version: PythonVersion = PythonVersionMin, - dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = None, - validation: bool = False, - field_constraints: bool = False, - snake_case_field: bool = False, - strip_default_none: bool = False, - aliases: Mapping[str, str] | None = None, - allow_population_by_field_name: bool = False, - apply_default_values_for_required_fields: bool = False, - allow_extra_fields: bool = False, - extra_fields: str | None = None, - use_generic_base_class: bool = False, - force_optional_for_required_fields: bool = False, - class_name: str | None = None, - use_standard_collections: bool = False, - base_path: Path | None = None, - use_schema_description: bool = False, - use_field_description: bool = False, - use_field_description_example: bool = False, - use_attribute_docstrings: bool = False, - use_inline_field_description: bool = False, - use_default_kwarg: bool = False, - reuse_model: bool = False, - reuse_scope: ReuseScope | None = None, - shared_module_name: str = DEFAULT_SHARED_MODULE_NAME, - encoding: str = "utf-8", - enum_field_as_literal: LiteralType | None = None, - enum_field_as_literal_map: dict[str, str] | None = None, - ignore_enum_constraints: bool = False, - use_one_literal_as_default: bool = False, - use_enum_values_in_discriminator: bool = False, - set_default_enum_member: bool = False, - use_subclass_enum: bool = False, - use_specialized_enum: bool = True, - strict_nullable: bool = False, - use_generic_container_types: bool = False, - enable_faux_immutability: bool = False, - remote_text_cache: DefaultPutDict[str, str] | None = None, - disable_appending_item_suffix: bool = False, - strict_types: Sequence[StrictTypes] | None = None, - empty_enum_field_name: str | None = None, - custom_class_name_generator: Callable[[str], str] | None = None, - field_extra_keys: set[str] | None = None, - field_include_all_keys: bool = False, - field_extra_keys_without_x_prefix: set[str] | None = None, - model_extra_keys: set[str] | None = None, - model_extra_keys_without_x_prefix: set[str] | None = None, - wrap_string_literal: bool | None = None, - use_title_as_name: bool = False, - use_operation_id_as_name: bool = False, - use_unique_items_as_set: bool = False, - use_tuple_for_fixed_items: bool = False, - allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints, - http_headers: Sequence[tuple[str, str]] | None = None, - http_ignore_tls: bool = False, - http_timeout: float | None = None, - use_annotated: bool = False, - use_serialize_as_any: bool = False, - use_non_positive_negative_number_constrained_types: bool = False, - use_decimal_for_multiple_of: bool = False, - original_field_name_delimiter: str | None = None, - use_double_quotes: bool = False, - use_union_operator: bool = False, - allow_responses_without_content: bool = False, - collapse_root_models: bool = False, - collapse_root_models_name_strategy: CollapseRootModelsNameStrategy | None = None, - collapse_reuse_models: bool = False, - skip_root_model: bool = False, - use_type_alias: bool = False, - special_field_name_prefix: str | None = None, - remove_special_field_name_prefix: bool = False, - capitalise_enum_members: bool = False, - keep_model_order: bool = False, - known_third_party: list[str] | None = None, - custom_formatters: list[str] | None = None, - custom_formatters_kwargs: dict[str, Any] | None = None, - use_pendulum: bool = False, - use_standard_primitive_types: bool = False, - http_query_parameters: Sequence[tuple[str, str]] | None = None, - treat_dot_as_module: bool | None = None, - use_exact_imports: bool = False, - default_field_extras: dict[str, Any] | None = None, - target_datetime_class: DatetimeClassType | None = None, - target_date_class: DateClassType | None = None, - keyword_only: bool = False, - frozen_dataclasses: bool = False, - no_alias: bool = False, - use_frozen_field: bool = False, - use_default_factory_for_optional_nested_models: bool = False, - formatters: list[Formatter] = DEFAULT_FORMATTERS, - defer_formatting: bool = False, - parent_scoped_naming: bool = False, - naming_strategy: NamingStrategy | None = None, - duplicate_name_suffix: dict[str, str] | None = None, - dataclass_arguments: DataclassArguments | None = None, - type_mappings: list[str] | None = None, - type_overrides: dict[str, str] | None = None, - read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None, - field_type_collision_strategy: FieldTypeCollisionStrategy | None = None, - target_pydantic_version: TargetPydanticVersion | None = None, + **options: Any, ) -> None: """Initialize the JSON Schema parser with configuration options.""" - target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime - super().__init__( - source=source, - config=config, - data_model_type=data_model_type, - data_model_root_type=data_model_root_type, - data_type_manager_type=data_type_manager_type, - data_model_field_type=data_model_field_type, - base_class=base_class, - base_class_map=base_class_map, - additional_imports=additional_imports, - class_decorators=class_decorators, - custom_template_dir=custom_template_dir, - extra_template_data=extra_template_data, - target_python_version=target_python_version, - dump_resolve_reference_action=dump_resolve_reference_action, - validation=validation, - field_constraints=field_constraints, - snake_case_field=snake_case_field, - strip_default_none=strip_default_none, - aliases=aliases, - allow_population_by_field_name=allow_population_by_field_name, - allow_extra_fields=allow_extra_fields, - extra_fields=extra_fields, - use_generic_base_class=use_generic_base_class, - apply_default_values_for_required_fields=apply_default_values_for_required_fields, - force_optional_for_required_fields=force_optional_for_required_fields, - class_name=class_name, - use_standard_collections=use_standard_collections, - base_path=base_path, - use_schema_description=use_schema_description, - use_field_description=use_field_description, - use_field_description_example=use_field_description_example, - use_attribute_docstrings=use_attribute_docstrings, - use_inline_field_description=use_inline_field_description, - use_default_kwarg=use_default_kwarg, - reuse_model=reuse_model, - reuse_scope=reuse_scope, - shared_module_name=shared_module_name, - encoding=encoding, - enum_field_as_literal=enum_field_as_literal, - enum_field_as_literal_map=enum_field_as_literal_map, - ignore_enum_constraints=ignore_enum_constraints, - use_one_literal_as_default=use_one_literal_as_default, - use_enum_values_in_discriminator=use_enum_values_in_discriminator, - set_default_enum_member=set_default_enum_member, - use_subclass_enum=use_subclass_enum, - use_specialized_enum=use_specialized_enum, - strict_nullable=strict_nullable, - use_generic_container_types=use_generic_container_types, - enable_faux_immutability=enable_faux_immutability, - remote_text_cache=remote_text_cache, - disable_appending_item_suffix=disable_appending_item_suffix, - strict_types=strict_types, - empty_enum_field_name=empty_enum_field_name, - custom_class_name_generator=custom_class_name_generator, - field_extra_keys=field_extra_keys, - field_include_all_keys=field_include_all_keys, - field_extra_keys_without_x_prefix=field_extra_keys_without_x_prefix, - model_extra_keys=model_extra_keys, - model_extra_keys_without_x_prefix=model_extra_keys_without_x_prefix, - wrap_string_literal=wrap_string_literal, - use_title_as_name=use_title_as_name, - use_operation_id_as_name=use_operation_id_as_name, - use_unique_items_as_set=use_unique_items_as_set, - use_tuple_for_fixed_items=use_tuple_for_fixed_items, - allof_merge_mode=allof_merge_mode, - http_headers=http_headers, - http_ignore_tls=http_ignore_tls, - http_timeout=http_timeout, - use_annotated=use_annotated, - use_serialize_as_any=use_serialize_as_any, - use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types, - use_decimal_for_multiple_of=use_decimal_for_multiple_of, - original_field_name_delimiter=original_field_name_delimiter, - use_double_quotes=use_double_quotes, - use_union_operator=use_union_operator, - allow_responses_without_content=allow_responses_without_content, - collapse_root_models=collapse_root_models, - collapse_root_models_name_strategy=collapse_root_models_name_strategy, - collapse_reuse_models=collapse_reuse_models, - skip_root_model=skip_root_model, - use_type_alias=use_type_alias, - special_field_name_prefix=special_field_name_prefix, - remove_special_field_name_prefix=remove_special_field_name_prefix, - capitalise_enum_members=capitalise_enum_members, - keep_model_order=keep_model_order, - known_third_party=known_third_party, - custom_formatters=custom_formatters, - custom_formatters_kwargs=custom_formatters_kwargs, - use_pendulum=use_pendulum, - use_standard_primitive_types=use_standard_primitive_types, - http_query_parameters=http_query_parameters, - treat_dot_as_module=treat_dot_as_module, - use_exact_imports=use_exact_imports, - default_field_extras=default_field_extras, - target_datetime_class=target_datetime_class, - target_date_class=target_date_class, - keyword_only=keyword_only, - frozen_dataclasses=frozen_dataclasses, - no_alias=no_alias, - use_frozen_field=use_frozen_field, - use_default_factory_for_optional_nested_models=use_default_factory_for_optional_nested_models, - formatters=formatters, - defer_formatting=defer_formatting, - parent_scoped_naming=parent_scoped_naming, - naming_strategy=naming_strategy, - duplicate_name_suffix=duplicate_name_suffix, - dataclass_arguments=dataclass_arguments, - type_mappings=type_mappings, - type_overrides=type_overrides, - read_only_write_only_model_type=read_only_write_only_model_type, - field_type_collision_strategy=field_type_collision_strategy, - target_pydantic_version=target_pydantic_version, - ) + if options.get("target_datetime_class") is None and (config is None or config.target_datetime_class is None): + options["target_datetime_class"] = DatetimeClassType.Awaredatetime + + super().__init__(source=source, config=config, **options) self.remote_object_cache: DefaultPutDict[str, dict[str, YamlValue]] = DefaultPutDict() self.raw_obj: dict[str, YamlValue] = {} diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index a17b18cec..5d54e8f35 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -18,48 +18,28 @@ from pydantic import Field from datamodel_code_generator import ( - DEFAULT_SHARED_MODULE_NAME, - AllOfMergeMode, - CollapseRootModelsNameStrategy, - DataclassArguments, Error, - FieldTypeCollisionStrategy, - LiteralType, - NamingStrategy, OpenAPIScope, - PythonVersion, - PythonVersionMin, - ReadOnlyWriteOnlyModelType, - ReuseScope, - TargetPydanticVersion, YamlValue, load_data, snooper_to_methods, ) -from datamodel_code_generator.format import DEFAULT_FORMATTERS, DateClassType, DatetimeClassType, Formatter -from datamodel_code_generator.model import DataModel, DataModelFieldBase -from datamodel_code_generator.model import pydantic as pydantic_model -from datamodel_code_generator.parser.base import get_special_path +from datamodel_code_generator.format import DatetimeClassType +from datamodel_code_generator.parser.base import _get_option, get_special_path from datamodel_code_generator.parser.jsonschema import ( JsonSchemaObject, JsonSchemaParser, get_model_by_path, ) from datamodel_code_generator.reference import FieldNameResolver, is_url, snake_to_upper_camel -from datamodel_code_generator.types import ( - DataType, - DataTypeManager, - EmptyDataType, - StrictTypes, -) +from datamodel_code_generator.types import DataType, EmptyDataType from datamodel_code_generator.util import BaseModel, model_dump, model_validate if TYPE_CHECKING: - from collections.abc import Callable, Iterable, Mapping, Sequence from urllib.parse import ParseResult from datamodel_code_generator.config import OpenAPIParserConfig - from datamodel_code_generator.parser import DefaultPutDict + from datamodel_code_generator.model.base import DataModelFieldBase RE_APPLICATION_JSON_PATTERN: Pattern[str] = re.compile(r"^application/.*json$") @@ -182,248 +162,26 @@ class OpenAPIParser(JsonSchemaParser): SCHEMA_PATHS: ClassVar[list[str]] = ["#/components/schemas"] - def __init__( # noqa: PLR0913 + def __init__( self, source: str | Path | list[Path] | ParseResult, *, config: OpenAPIParserConfig | None = None, - data_model_type: type[DataModel] = pydantic_model.BaseModel, - data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, - data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, - data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField, - base_class: str | None = None, - base_class_map: dict[str, str] | None = None, - additional_imports: list[str] | None = None, - class_decorators: list[str] | None = None, - custom_template_dir: Path | None = None, - extra_template_data: defaultdict[str, dict[str, Any]] | None = None, - target_python_version: PythonVersion = PythonVersionMin, - dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = None, - validation: bool = False, - field_constraints: bool = False, - snake_case_field: bool = False, - strip_default_none: bool = False, - aliases: Mapping[str, str] | None = None, - allow_population_by_field_name: bool = False, - allow_extra_fields: bool = False, - extra_fields: str | None = None, - use_generic_base_class: bool = False, - apply_default_values_for_required_fields: bool = False, - force_optional_for_required_fields: bool = False, - class_name: str | None = None, - use_standard_collections: bool = False, - base_path: Path | None = None, - use_schema_description: bool = False, - use_field_description: bool = False, - use_field_description_example: bool = False, - use_attribute_docstrings: bool = False, - use_inline_field_description: bool = False, - use_default_kwarg: bool = False, - reuse_model: bool = False, - reuse_scope: ReuseScope | None = None, - shared_module_name: str = DEFAULT_SHARED_MODULE_NAME, - encoding: str = "utf-8", - enum_field_as_literal: LiteralType | None = None, - enum_field_as_literal_map: dict[str, str] | None = None, - ignore_enum_constraints: bool = False, - use_one_literal_as_default: bool = False, - use_enum_values_in_discriminator: bool = False, - set_default_enum_member: bool = False, - use_subclass_enum: bool = False, - use_specialized_enum: bool = True, - strict_nullable: bool = False, - use_generic_container_types: bool = False, - enable_faux_immutability: bool = False, - remote_text_cache: DefaultPutDict[str, str] | None = None, - disable_appending_item_suffix: bool = False, - strict_types: Sequence[StrictTypes] | None = None, - empty_enum_field_name: str | None = None, - custom_class_name_generator: Callable[[str], str] | None = None, - field_extra_keys: set[str] | None = None, - field_include_all_keys: bool = False, - field_extra_keys_without_x_prefix: set[str] | None = None, - model_extra_keys: set[str] | None = None, - model_extra_keys_without_x_prefix: set[str] | None = None, - openapi_scopes: list[OpenAPIScope] | None = None, - include_path_parameters: bool = False, - wrap_string_literal: bool | None = False, - use_title_as_name: bool = False, - use_operation_id_as_name: bool = False, - use_unique_items_as_set: bool = False, - use_tuple_for_fixed_items: bool = False, - allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints, - http_headers: Sequence[tuple[str, str]] | None = None, - http_ignore_tls: bool = False, - http_timeout: float | None = None, - use_annotated: bool = False, - use_serialize_as_any: bool = False, - use_non_positive_negative_number_constrained_types: bool = False, - use_decimal_for_multiple_of: bool = False, - original_field_name_delimiter: str | None = None, - use_double_quotes: bool = False, - use_union_operator: bool = False, - allow_responses_without_content: bool = False, - collapse_root_models: bool = False, - collapse_root_models_name_strategy: CollapseRootModelsNameStrategy | None = None, - collapse_reuse_models: bool = False, - skip_root_model: bool = False, - use_type_alias: bool = False, - special_field_name_prefix: str | None = None, - remove_special_field_name_prefix: bool = False, - capitalise_enum_members: bool = False, - keep_model_order: bool = False, - known_third_party: list[str] | None = None, - custom_formatters: list[str] | None = None, - custom_formatters_kwargs: dict[str, Any] | None = None, - use_pendulum: bool = False, - use_standard_primitive_types: bool = False, - http_query_parameters: Sequence[tuple[str, str]] | None = None, - treat_dot_as_module: bool | None = None, - use_exact_imports: bool = False, - default_field_extras: dict[str, Any] | None = None, - target_datetime_class: DatetimeClassType | None = None, - target_date_class: DateClassType | None = None, - keyword_only: bool = False, - frozen_dataclasses: bool = False, - no_alias: bool = False, - formatters: list[Formatter] = DEFAULT_FORMATTERS, - defer_formatting: bool = False, - parent_scoped_naming: bool = False, - naming_strategy: NamingStrategy | None = None, - duplicate_name_suffix: dict[str, str] | None = None, - dataclass_arguments: DataclassArguments | None = None, - type_mappings: list[str] | None = None, - type_overrides: dict[str, str] | None = None, - read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None, - use_frozen_field: bool = False, - use_default_factory_for_optional_nested_models: bool = False, - use_status_code_in_response_name: bool = False, - field_type_collision_strategy: FieldTypeCollisionStrategy | None = None, - target_pydantic_version: TargetPydanticVersion | None = None, + **options: Any, ) -> None: - """Initialize the OpenAPI parser with extensive configuration options.""" - if config is not None: - openapi_scopes = config.openapi_scopes if openapi_scopes is None else openapi_scopes - include_path_parameters = include_path_parameters or config.include_path_parameters - use_status_code_in_response_name = ( - use_status_code_in_response_name or config.use_status_code_in_response_name - ) - - target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime - super().__init__( - source=source, - config=config, - data_model_type=data_model_type, - data_model_root_type=data_model_root_type, - data_type_manager_type=data_type_manager_type, - data_model_field_type=data_model_field_type, - base_class=base_class, - base_class_map=base_class_map, - additional_imports=additional_imports, - class_decorators=class_decorators, - custom_template_dir=custom_template_dir, - extra_template_data=extra_template_data, - target_python_version=target_python_version, - dump_resolve_reference_action=dump_resolve_reference_action, - validation=validation, - field_constraints=field_constraints, - snake_case_field=snake_case_field, - strip_default_none=strip_default_none, - aliases=aliases, - allow_population_by_field_name=allow_population_by_field_name, - allow_extra_fields=allow_extra_fields, - extra_fields=extra_fields, - use_generic_base_class=use_generic_base_class, - apply_default_values_for_required_fields=apply_default_values_for_required_fields, - force_optional_for_required_fields=force_optional_for_required_fields, - class_name=class_name, - use_standard_collections=use_standard_collections, - base_path=base_path, - use_schema_description=use_schema_description, - use_field_description=use_field_description, - use_field_description_example=use_field_description_example, - use_attribute_docstrings=use_attribute_docstrings, - use_inline_field_description=use_inline_field_description, - use_default_kwarg=use_default_kwarg, - reuse_model=reuse_model, - reuse_scope=reuse_scope, - shared_module_name=shared_module_name, - encoding=encoding, - enum_field_as_literal=enum_field_as_literal, - enum_field_as_literal_map=enum_field_as_literal_map, - ignore_enum_constraints=ignore_enum_constraints, - use_one_literal_as_default=use_one_literal_as_default, - use_enum_values_in_discriminator=use_enum_values_in_discriminator, - set_default_enum_member=set_default_enum_member, - use_subclass_enum=use_subclass_enum, - use_specialized_enum=use_specialized_enum, - strict_nullable=strict_nullable, - use_generic_container_types=use_generic_container_types, - enable_faux_immutability=enable_faux_immutability, - remote_text_cache=remote_text_cache, - disable_appending_item_suffix=disable_appending_item_suffix, - strict_types=strict_types, - empty_enum_field_name=empty_enum_field_name, - custom_class_name_generator=custom_class_name_generator, - field_extra_keys=field_extra_keys, - field_include_all_keys=field_include_all_keys, - field_extra_keys_without_x_prefix=field_extra_keys_without_x_prefix, - model_extra_keys=model_extra_keys, - model_extra_keys_without_x_prefix=model_extra_keys_without_x_prefix, - wrap_string_literal=wrap_string_literal, - use_title_as_name=use_title_as_name, - use_operation_id_as_name=use_operation_id_as_name, - use_unique_items_as_set=use_unique_items_as_set, - use_tuple_for_fixed_items=use_tuple_for_fixed_items, - allof_merge_mode=allof_merge_mode, - http_headers=http_headers, - http_ignore_tls=http_ignore_tls, - http_timeout=http_timeout, - use_annotated=use_annotated, - use_serialize_as_any=use_serialize_as_any, - use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types, - use_decimal_for_multiple_of=use_decimal_for_multiple_of, - original_field_name_delimiter=original_field_name_delimiter, - use_double_quotes=use_double_quotes, - use_union_operator=use_union_operator, - allow_responses_without_content=allow_responses_without_content, - collapse_root_models=collapse_root_models, - collapse_root_models_name_strategy=collapse_root_models_name_strategy, - collapse_reuse_models=collapse_reuse_models, - skip_root_model=skip_root_model, - use_type_alias=use_type_alias, - special_field_name_prefix=special_field_name_prefix, - remove_special_field_name_prefix=remove_special_field_name_prefix, - capitalise_enum_members=capitalise_enum_members, - keep_model_order=keep_model_order, - known_third_party=known_third_party, - custom_formatters=custom_formatters, - custom_formatters_kwargs=custom_formatters_kwargs, - use_pendulum=use_pendulum, - use_standard_primitive_types=use_standard_primitive_types, - http_query_parameters=http_query_parameters, - treat_dot_as_module=treat_dot_as_module, - use_exact_imports=use_exact_imports, - default_field_extras=default_field_extras, - target_datetime_class=target_datetime_class, - target_date_class=target_date_class, - keyword_only=keyword_only, - frozen_dataclasses=frozen_dataclasses, - no_alias=no_alias, - formatters=formatters, - defer_formatting=defer_formatting, - parent_scoped_naming=parent_scoped_naming, - naming_strategy=naming_strategy, - duplicate_name_suffix=duplicate_name_suffix, - dataclass_arguments=dataclass_arguments, - type_mappings=type_mappings, - type_overrides=type_overrides, - read_only_write_only_model_type=read_only_write_only_model_type, - use_frozen_field=use_frozen_field, - use_default_factory_for_optional_nested_models=use_default_factory_for_optional_nested_models, - field_type_collision_strategy=field_type_collision_strategy, - target_pydantic_version=target_pydantic_version, + """Initialize the OpenAPI parser with configuration options.""" + # Extract OpenAPI-specific options + openapi_scopes = _get_option(options, "openapi_scopes", config, default=None) + include_path_parameters = _get_option(options, "include_path_parameters", config, default=False) + use_status_code_in_response_name = _get_option( + options, "use_status_code_in_response_name", config, default=False ) + + if options.get("target_datetime_class") is None and (config is None or config.target_datetime_class is None): + options["target_datetime_class"] = DatetimeClassType.Awaredatetime + + super().__init__(source=source, config=config, **options) + self.open_api_scopes: list[OpenAPIScope] = openapi_scopes or [OpenAPIScope.Schemas] self.include_path_parameters: bool = include_path_parameters self.use_status_code_in_response_name: bool = use_status_code_in_response_name diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 49347e081..884e62fb3 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -27,26 +27,22 @@ TargetPydanticVersion, ) from datamodel_code_generator.format import PythonVersionMin -from datamodel_code_generator.model import DataModel, DataModelFieldBase -from datamodel_code_generator.model import pydantic as pydantic_model -from datamodel_code_generator.model.pydantic import BaseModel -from datamodel_code_generator.parser.base import Parser, YamlValue, title_to_class_name +from datamodel_code_generator.parser.base import Parser, YamlValue from datamodel_code_generator.util import is_pydantic_v2 PYDANTIC_V2_SKIP = pytest.mark.skipif(not is_pydantic_v2(), reason="Pydantic v2 required") if TYPE_CHECKING: from collections import defaultdict - from collections.abc import Callable, Iterable, Mapping, Sequence + from collections.abc import Callable, Mapping, Sequence from pathlib import Path from urllib.parse import ParseResult from datamodel_code_generator.config import GenerateConfig, ParserConfig from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion from datamodel_code_generator.model.dataclass import DataclassArguments - from datamodel_code_generator.model.pydantic import DataTypeManager from datamodel_code_generator.model.pydantic_v2 import UnionMode - from datamodel_code_generator.parser import DefaultPutDict, LiteralType + from datamodel_code_generator.parser import LiteralType from datamodel_code_generator.types import StrictTypes @@ -184,116 +180,7 @@ def __init__( source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, config: ParserConfig | None = None, - data_model_type: type[DataModel] = BaseModel, - data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, - data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, - data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField, - base_class: str | None = None, - base_class_map: dict[str, str] | None = None, - additional_imports: list[str] | None = None, - class_decorators: list[str] | None = None, - custom_template_dir: Path | None = None, - extra_template_data: defaultdict[str, dict[str, Any]] | None = None, - target_python_version: PythonVersion = PythonVersionMin, - dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = None, - validation: bool = False, - field_constraints: bool = False, - snake_case_field: bool = False, - strip_default_none: bool = False, - aliases: Mapping[str, str] | None = None, - allow_population_by_field_name: bool = False, - apply_default_values_for_required_fields: bool = False, - allow_extra_fields: bool = False, - extra_fields: str | None = None, - use_generic_base_class: bool = False, - force_optional_for_required_fields: bool = False, - class_name: str | None = None, - use_standard_collections: bool = False, - base_path: Path | None = None, - use_schema_description: bool = False, - use_field_description: bool = False, - use_field_description_example: bool = False, - use_attribute_docstrings: bool = False, - use_inline_field_description: bool = False, - use_default_kwarg: bool = False, - reuse_model: bool = False, - reuse_scope: ReuseScope | None = None, - shared_module_name: str = DEFAULT_SHARED_MODULE_NAME, - encoding: str = "utf-8", - enum_field_as_literal: LiteralType | None = None, - enum_field_as_literal_map: dict[str, str] | None = None, - ignore_enum_constraints: bool = False, - set_default_enum_member: bool = False, - use_subclass_enum: bool = False, - use_specialized_enum: bool = True, - strict_nullable: bool = False, - use_generic_container_types: bool = False, - enable_faux_immutability: bool = False, - remote_text_cache: DefaultPutDict[str, str] | None = None, - disable_appending_item_suffix: bool = False, - strict_types: Sequence[StrictTypes] | None = None, - empty_enum_field_name: str | None = None, - custom_class_name_generator: Callable[[str], str] | None = title_to_class_name, - field_extra_keys: set[str] | None = None, - field_include_all_keys: bool = False, - field_extra_keys_without_x_prefix: set[str] | None = None, - model_extra_keys: set[str] | None = None, - model_extra_keys_without_x_prefix: set[str] | None = None, - wrap_string_literal: bool | None = None, - use_title_as_name: bool = False, - use_operation_id_as_name: bool = False, - use_unique_items_as_set: bool = False, - use_tuple_for_fixed_items: bool = False, - allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints, - http_headers: Sequence[tuple[str, str]] | None = None, - http_ignore_tls: bool = False, - http_timeout: float | None = None, - use_annotated: bool = False, - use_serialize_as_any: bool = False, - use_non_positive_negative_number_constrained_types: bool = False, - use_decimal_for_multiple_of: bool = False, - original_field_name_delimiter: str | None = None, - use_double_quotes: bool = False, - use_union_operator: bool = False, - allow_responses_without_content: bool = False, - collapse_root_models: bool = False, - collapse_root_models_name_strategy: CollapseRootModelsNameStrategy | None = None, - collapse_reuse_models: bool = False, - skip_root_model: bool = False, - use_type_alias: bool = False, - special_field_name_prefix: str | None = None, - remove_special_field_name_prefix: bool = False, - capitalise_enum_members: bool = False, - keep_model_order: bool = False, - use_one_literal_as_default: bool = False, - use_enum_values_in_discriminator: bool = False, - known_third_party: list[str] | None = None, - custom_formatters: list[str] | None = None, - custom_formatters_kwargs: dict[str, Any] | None = None, - use_pendulum: bool = False, - use_standard_primitive_types: bool = False, - http_query_parameters: Sequence[tuple[str, str]] | None = None, - treat_dot_as_module: bool | None = None, - use_exact_imports: bool = False, - default_field_extras: dict[str, Any] | None = None, - target_datetime_class: DatetimeClassType | None = None, - target_date_class: DateClassType | None = None, - keyword_only: bool = False, - frozen_dataclasses: bool = False, - no_alias: bool = False, - use_frozen_field: bool = False, - use_default_factory_for_optional_nested_models: bool = False, - formatters: list[Formatter] = DEFAULT_FORMATTERS, - defer_formatting: bool = False, - parent_scoped_naming: bool = False, - naming_strategy: NamingStrategy | None = None, - duplicate_name_suffix: dict[str, str] | None = None, - dataclass_arguments: DataclassArguments | None = None, - type_mappings: list[str] | None = None, - type_overrides: dict[str, str] | None = None, - read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None, - field_type_collision_strategy: FieldTypeCollisionStrategy | None = None, - target_pydantic_version: TargetPydanticVersion | None = None, + **options: Any, ) -> None: raise NotImplementedError From 97bf992355f15fb514455f8592605eaf713fdd34 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 30 Dec 2025 14:22:13 +0000 Subject: [PATCH 4/7] Refactor Parser __init__ to use config pattern with model_validate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove _get_option helper function from base.py - Use ParserConfig.model_validate(options) to convert options to config - Update all instance variable references to use config.xxx pattern - Apply same pattern to JsonSchemaParser, OpenAPIParser, GraphQLParser - Fix aliases type from Mapping[str, str] to Mapping[str, str | list[str]] to support multiple aliases per field - Add model_rebuild() calls to resolve forward references before validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src/datamodel_code_generator/__init__.py | 2 +- .../_types/generate_config_dict.py | 2 +- .../_types/parser_config_dict.py | 2 +- src/datamodel_code_generator/config.py | 4 +- src/datamodel_code_generator/parser/base.py | 432 +++++++----------- .../parser/graphql.py | 51 ++- .../parser/jsonschema.py | 33 +- .../parser/openapi.py | 48 +- src/datamodel_code_generator/reference.py | 2 +- 9 files changed, 266 insertions(+), 310 deletions(-) diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index d544eef7a..f535dcd70 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -469,7 +469,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915 field_constraints: bool = False, snake_case_field: bool = False, strip_default_none: bool = False, - aliases: Mapping[str, str] | None = None, + aliases: Mapping[str, str | list[str]] | None = None, disable_timestamp: bool = False, enable_version_header: bool = False, enable_command_header: bool = False, diff --git a/src/datamodel_code_generator/_types/generate_config_dict.py b/src/datamodel_code_generator/_types/generate_config_dict.py index 7cbbe3daf..8feac2687 100644 --- a/src/datamodel_code_generator/_types/generate_config_dict.py +++ b/src/datamodel_code_generator/_types/generate_config_dict.py @@ -52,7 +52,7 @@ class GenerateConfigDict(TypedDict): field_constraints: NotRequired[bool] snake_case_field: NotRequired[bool] strip_default_none: NotRequired[bool] - aliases: NotRequired[Mapping[str, str] | None] + aliases: NotRequired[Mapping[str, str | list[str]] | None] disable_timestamp: NotRequired[bool] enable_version_header: NotRequired[bool] enable_command_header: NotRequired[bool] diff --git a/src/datamodel_code_generator/_types/parser_config_dict.py b/src/datamodel_code_generator/_types/parser_config_dict.py index c78b052c2..bac1dfe4e 100644 --- a/src/datamodel_code_generator/_types/parser_config_dict.py +++ b/src/datamodel_code_generator/_types/parser_config_dict.py @@ -46,7 +46,7 @@ class ParserConfigDict(TypedDict): field_constraints: NotRequired[bool] snake_case_field: NotRequired[bool] strip_default_none: NotRequired[bool] - aliases: NotRequired[Mapping[str, str] | None] + aliases: NotRequired[Mapping[str, str | list[str]] | None] allow_population_by_field_name: NotRequired[bool] apply_default_values_for_required_fields: NotRequired[bool] allow_extra_fields: NotRequired[bool] diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index ba3d4b5b0..76364197a 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -85,7 +85,7 @@ class Config: field_constraints: bool = False snake_case_field: bool = False strip_default_none: bool = False - aliases: Mapping[str, str] | None = None + aliases: Mapping[str, str | list[str]] | None = None disable_timestamp: bool = False enable_version_header: bool = False enable_command_header: bool = False @@ -220,7 +220,7 @@ class Config: field_constraints: bool = False snake_case_field: bool = False strip_default_none: bool = False - aliases: Mapping[str, str] | None = None + aliases: Mapping[str, str | list[str]] | None = None allow_population_by_field_name: bool = False apply_default_values_for_required_fields: bool = False allow_extra_fields: bool = False diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 7a672c1bd..f045166bc 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -80,21 +80,6 @@ from datamodel_code_generator.config import ParserConfig -def _get_option( - options: dict[str, Any], - key: str, - config: ParserConfig | None, - *, - default: Any, -) -> Any: - """Get option value with precedence: options > config > default.""" - if key in options: - return options[key] - if config is not None: - return getattr(config, key, default) - return default - - @runtime_checkable class HashableComparable(Hashable, Protocol): """Protocol for types that are both hashable and support comparison.""" @@ -697,7 +682,7 @@ class Parser(ABC): parse_raw() to handle specific schema formats. """ - def __init__( # noqa: PLR0912, PLR0914, PLR0915 + def __init__( # noqa: PLR0912, PLR0915 self, source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, @@ -705,309 +690,212 @@ def __init__( # noqa: PLR0912, PLR0914, PLR0915 **options: Any, ) -> None: """Initialize the Parser with configuration options.""" - - def opt(key: str, *, default: Any) -> Any: - return _get_option(options, key, config, default=default) - - data_model_type = opt("data_model_type", default=pydantic_model.BaseModel) - data_model_root_type = opt("data_model_root_type", default=pydantic_model.CustomRootType) - data_type_manager_type = opt("data_type_manager_type", default=pydantic_model.DataTypeManager) - data_model_field_type = opt("data_model_field_type", default=pydantic_model.DataModelField) - base_class = opt("base_class", default=None) - base_class_map = opt("base_class_map", default=None) - additional_imports = opt("additional_imports", default=None) - class_decorators = opt("class_decorators", default=None) - custom_template_dir = opt("custom_template_dir", default=None) - extra_template_data = opt("extra_template_data", default=None) - target_python_version = opt("target_python_version", default=PythonVersionMin) - dump_resolve_reference_action = opt("dump_resolve_reference_action", default=None) - validation = opt("validation", default=False) - field_constraints = opt("field_constraints", default=False) - snake_case_field = opt("snake_case_field", default=False) - strip_default_none = opt("strip_default_none", default=False) - aliases = opt("aliases", default=None) - allow_population_by_field_name = opt("allow_population_by_field_name", default=False) - apply_default_values_for_required_fields = opt("apply_default_values_for_required_fields", default=False) - allow_extra_fields = opt("allow_extra_fields", default=False) - extra_fields = opt("extra_fields", default=None) - use_generic_base_class = opt("use_generic_base_class", default=False) - force_optional_for_required_fields = opt("force_optional_for_required_fields", default=False) - class_name = opt("class_name", default=None) - use_standard_collections = opt("use_standard_collections", default=False) - base_path = opt("base_path", default=None) - use_schema_description = opt("use_schema_description", default=False) - use_field_description = opt("use_field_description", default=False) - use_field_description_example = opt("use_field_description_example", default=False) - use_attribute_docstrings = opt("use_attribute_docstrings", default=False) - use_inline_field_description = opt("use_inline_field_description", default=False) - use_default_kwarg = opt("use_default_kwarg", default=False) - reuse_model = opt("reuse_model", default=False) - reuse_scope = opt("reuse_scope", default=None) - shared_module_name = opt("shared_module_name", default=DEFAULT_SHARED_MODULE_NAME) - encoding = opt("encoding", default="utf-8") - enum_field_as_literal = opt("enum_field_as_literal", default=None) - enum_field_as_literal_map = opt("enum_field_as_literal_map", default=None) - ignore_enum_constraints = opt("ignore_enum_constraints", default=False) - set_default_enum_member = opt("set_default_enum_member", default=False) - use_subclass_enum = opt("use_subclass_enum", default=False) - use_specialized_enum = opt("use_specialized_enum", default=True) - strict_nullable = opt("strict_nullable", default=False) - use_generic_container_types = opt("use_generic_container_types", default=False) - enable_faux_immutability = opt("enable_faux_immutability", default=False) - remote_text_cache = opt("remote_text_cache", default=None) - disable_appending_item_suffix = opt("disable_appending_item_suffix", default=False) - strict_types = opt("strict_types", default=None) - empty_enum_field_name = opt("empty_enum_field_name", default=None) - custom_class_name_generator = opt("custom_class_name_generator", default=None) - field_extra_keys = opt("field_extra_keys", default=None) - field_include_all_keys = opt("field_include_all_keys", default=False) - field_extra_keys_without_x_prefix = opt("field_extra_keys_without_x_prefix", default=None) - model_extra_keys = opt("model_extra_keys", default=None) - model_extra_keys_without_x_prefix = opt("model_extra_keys_without_x_prefix", default=None) - wrap_string_literal = opt("wrap_string_literal", default=None) - use_title_as_name = opt("use_title_as_name", default=False) - use_operation_id_as_name = opt("use_operation_id_as_name", default=False) - use_unique_items_as_set = opt("use_unique_items_as_set", default=False) - use_tuple_for_fixed_items = opt("use_tuple_for_fixed_items", default=False) - allof_merge_mode = opt("allof_merge_mode", default=AllOfMergeMode.Constraints) - http_headers = opt("http_headers", default=None) - http_ignore_tls = opt("http_ignore_tls", default=False) - http_timeout = opt("http_timeout", default=None) - use_annotated = opt("use_annotated", default=False) - use_serialize_as_any = opt("use_serialize_as_any", default=False) - use_non_positive_negative_number_constrained_types = opt( - "use_non_positive_negative_number_constrained_types", default=False - ) - use_decimal_for_multiple_of = opt("use_decimal_for_multiple_of", default=False) - original_field_name_delimiter = opt("original_field_name_delimiter", default=None) - use_double_quotes = opt("use_double_quotes", default=False) - use_union_operator = opt("use_union_operator", default=False) - allow_responses_without_content = opt("allow_responses_without_content", default=False) - collapse_root_models = opt("collapse_root_models", default=False) - collapse_root_models_name_strategy = opt("collapse_root_models_name_strategy", default=None) - collapse_reuse_models = opt("collapse_reuse_models", default=False) - skip_root_model = opt("skip_root_model", default=False) - use_type_alias = opt("use_type_alias", default=False) - special_field_name_prefix = opt("special_field_name_prefix", default=None) - remove_special_field_name_prefix = opt("remove_special_field_name_prefix", default=False) - capitalise_enum_members = opt("capitalise_enum_members", default=False) - keep_model_order = opt("keep_model_order", default=False) - use_one_literal_as_default = opt("use_one_literal_as_default", default=False) - use_enum_values_in_discriminator = opt("use_enum_values_in_discriminator", default=False) - known_third_party = opt("known_third_party", default=None) - custom_formatters = opt("custom_formatters", default=None) - custom_formatters_kwargs = opt("custom_formatters_kwargs", default=None) - use_pendulum = opt("use_pendulum", default=False) - use_standard_primitive_types = opt("use_standard_primitive_types", default=False) - http_query_parameters = opt("http_query_parameters", default=None) - treat_dot_as_module = opt("treat_dot_as_module", default=None) - use_exact_imports = opt("use_exact_imports", default=False) - default_field_extras = opt("default_field_extras", default=None) - target_datetime_class = opt("target_datetime_class", default=None) - target_date_class = opt("target_date_class", default=None) - keyword_only = opt("keyword_only", default=False) - frozen_dataclasses = opt("frozen_dataclasses", default=False) - no_alias = opt("no_alias", default=False) - use_frozen_field = opt("use_frozen_field", default=False) - use_default_factory_for_optional_nested_models = opt( - "use_default_factory_for_optional_nested_models", default=False + from datamodel_code_generator.config import ParserConfig + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + # Rebuild the model to resolve forward references before validation + ParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } ) - formatters = opt("formatters", default=DEFAULT_FORMATTERS) - defer_formatting = opt("defer_formatting", default=False) - parent_scoped_naming = opt("parent_scoped_naming", default=False) - naming_strategy = opt("naming_strategy", default=None) - duplicate_name_suffix = opt("duplicate_name_suffix", default=None) - dataclass_arguments = opt("dataclass_arguments", default=None) - type_mappings = opt("type_mappings", default=None) - type_overrides = opt("type_overrides", default=None) - read_only_write_only_model_type = opt("read_only_write_only_model_type", default=None) - field_type_collision_strategy = opt("field_type_collision_strategy", default=None) - target_pydantic_version = opt("target_pydantic_version", default=None) - - # Convert extra_template_data to defaultdict if needed + + if config is None: + config = ParserConfig.model_validate(options) + elif options: + # Merge options into config (options take precedence) + config_dict = config.model_dump() + config_dict.update(options) + config = ParserConfig.model_validate(config_dict) + + extra_template_data: defaultdict[str, Any] | None = config.extra_template_data if extra_template_data is not None and not isinstance(extra_template_data, defaultdict): extra_template_data = defaultdict(dict, extra_template_data) - self.keyword_only = keyword_only - self.target_pydantic_version = target_pydantic_version - self.frozen_dataclasses = frozen_dataclasses - self.data_type_manager: DataTypeManager = data_type_manager_type( - python_version=target_python_version, - use_standard_collections=use_standard_collections, - use_generic_container_types=use_generic_container_types, - use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types, - use_decimal_for_multiple_of=use_decimal_for_multiple_of, - strict_types=strict_types, - use_union_operator=use_union_operator, - use_pendulum=use_pendulum, - use_standard_primitive_types=use_standard_primitive_types, - target_datetime_class=target_datetime_class, - target_date_class=target_date_class, - treat_dot_as_module=treat_dot_as_module or False, - use_serialize_as_any=use_serialize_as_any, + self.keyword_only = config.keyword_only + self.target_pydantic_version = config.target_pydantic_version + self.frozen_dataclasses = config.frozen_dataclasses + self.data_type_manager: DataTypeManager = config.data_type_manager_type( + python_version=config.target_python_version, + use_standard_collections=config.use_standard_collections, + use_generic_container_types=config.use_generic_container_types, + use_non_positive_negative_number_constrained_types=config.use_non_positive_negative_number_constrained_types, + use_decimal_for_multiple_of=config.use_decimal_for_multiple_of, + strict_types=config.strict_types, + use_union_operator=config.use_union_operator, + use_pendulum=config.use_pendulum, + use_standard_primitive_types=config.use_standard_primitive_types, + target_datetime_class=config.target_datetime_class, + target_date_class=config.target_date_class, + treat_dot_as_module=config.treat_dot_as_module or False, + use_serialize_as_any=config.use_serialize_as_any, ) - self.data_model_type: type[DataModel] = data_model_type - self.data_model_root_type: type[DataModel] = data_model_root_type - self.data_model_field_type: type[DataModelFieldBase] = data_model_field_type - - self.imports: Imports = Imports(use_exact_imports) - self.use_exact_imports: bool = use_exact_imports - self._append_additional_imports(additional_imports=additional_imports) - self.class_decorators: list[str] = class_decorators or [] - - self.base_class: str | None = base_class - self.base_class_map: dict[str, str] | None = base_class_map - self.target_python_version: PythonVersion = target_python_version + self.data_model_type: type[DataModel] = config.data_model_type + self.data_model_root_type: type[DataModel] = config.data_model_root_type + self.data_model_field_type: type[DataModelFieldBase] = config.data_model_field_type + + self.imports: Imports = Imports(config.use_exact_imports) + self.use_exact_imports: bool = config.use_exact_imports + self._append_additional_imports(additional_imports=config.additional_imports) + self.class_decorators: list[str] = config.class_decorators or [] + + self.base_class: str | None = config.base_class + self.base_class_map: dict[str, str] | None = config.base_class_map + self.target_python_version: PythonVersion = config.target_python_version self.results: list[DataModel] = [] - self.dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = dump_resolve_reference_action - self.validation: bool = validation - self.field_constraints: bool = field_constraints - self.snake_case_field: bool = snake_case_field - self.strip_default_none: bool = strip_default_none - self.apply_default_values_for_required_fields: bool = apply_default_values_for_required_fields - self.force_optional_for_required_fields: bool = force_optional_for_required_fields - self.use_schema_description: bool = use_schema_description - self.use_field_description: bool = use_field_description - self.use_field_description_example: bool = use_field_description_example - self.use_inline_field_description: bool = use_inline_field_description - self.use_default_kwarg: bool = use_default_kwarg - self.reuse_model: bool = reuse_model - self.reuse_scope: ReuseScope | None = reuse_scope - self.shared_module_name: str = shared_module_name - self.encoding: str = encoding - self.enum_field_as_literal: LiteralType | None = enum_field_as_literal - self.enum_field_as_literal_map: dict[str, str] = enum_field_as_literal_map or {} - self.ignore_enum_constraints: bool = ignore_enum_constraints - self.set_default_enum_member: bool = set_default_enum_member - self.use_subclass_enum: bool = use_subclass_enum - self.use_specialized_enum: bool = use_specialized_enum - self.strict_nullable: bool = strict_nullable - self.use_generic_container_types: bool = use_generic_container_types - self.use_union_operator: bool = use_union_operator - self.enable_faux_immutability: bool = enable_faux_immutability - self.custom_class_name_generator: Callable[[str], str] | None = custom_class_name_generator - self.field_extra_keys: set[str] = field_extra_keys or set() - self.field_extra_keys_without_x_prefix: set[str] = field_extra_keys_without_x_prefix or set() - self.model_extra_keys: set[str] = model_extra_keys or set() - self.model_extra_keys_without_x_prefix: set[str] = model_extra_keys_without_x_prefix or set() - self.field_include_all_keys: bool = field_include_all_keys - - self.remote_text_cache: DefaultPutDict[str, str] = remote_text_cache or DefaultPutDict() + self.dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = config.dump_resolve_reference_action + self.validation: bool = config.validation + self.field_constraints: bool = config.field_constraints + self.snake_case_field: bool = config.snake_case_field + self.strip_default_none: bool = config.strip_default_none + self.apply_default_values_for_required_fields: bool = config.apply_default_values_for_required_fields + self.force_optional_for_required_fields: bool = config.force_optional_for_required_fields + self.use_schema_description: bool = config.use_schema_description + self.use_field_description: bool = config.use_field_description + self.use_field_description_example: bool = config.use_field_description_example + self.use_inline_field_description: bool = config.use_inline_field_description + self.use_default_kwarg: bool = config.use_default_kwarg + self.reuse_model: bool = config.reuse_model + self.reuse_scope: ReuseScope | None = config.reuse_scope + self.shared_module_name: str = config.shared_module_name + self.encoding: str = config.encoding + self.enum_field_as_literal: LiteralType | None = config.enum_field_as_literal + self.enum_field_as_literal_map: dict[str, str] = config.enum_field_as_literal_map or {} + self.ignore_enum_constraints: bool = config.ignore_enum_constraints + self.set_default_enum_member: bool = config.set_default_enum_member + self.use_subclass_enum: bool = config.use_subclass_enum + self.use_specialized_enum: bool = config.use_specialized_enum + self.strict_nullable: bool = config.strict_nullable + self.use_generic_container_types: bool = config.use_generic_container_types + self.use_union_operator: bool = config.use_union_operator + self.enable_faux_immutability: bool = config.enable_faux_immutability + self.custom_class_name_generator: Callable[[str], str] | None = config.custom_class_name_generator + self.field_extra_keys: set[str] = config.field_extra_keys or set() + self.field_extra_keys_without_x_prefix: set[str] = config.field_extra_keys_without_x_prefix or set() + self.model_extra_keys: set[str] = config.model_extra_keys or set() + self.model_extra_keys_without_x_prefix: set[str] = config.model_extra_keys_without_x_prefix or set() + self.field_include_all_keys: bool = config.field_include_all_keys + + self.remote_text_cache: DefaultPutDict[str, str] = config.remote_text_cache or DefaultPutDict() self.current_source_path: Path | None = None - self.use_title_as_name: bool = use_title_as_name - self.use_operation_id_as_name: bool = use_operation_id_as_name - self.use_unique_items_as_set: bool = use_unique_items_as_set - self.use_tuple_for_fixed_items: bool = use_tuple_for_fixed_items - self.allof_merge_mode: AllOfMergeMode = allof_merge_mode - self.dataclass_arguments = dataclass_arguments - - if base_path: - self.base_path = base_path + self.use_title_as_name: bool = config.use_title_as_name + self.use_operation_id_as_name: bool = config.use_operation_id_as_name + self.use_unique_items_as_set: bool = config.use_unique_items_as_set + self.use_tuple_for_fixed_items: bool = config.use_tuple_for_fixed_items + self.allof_merge_mode: AllOfMergeMode = config.allof_merge_mode + self.dataclass_arguments = config.dataclass_arguments + + if config.base_path: + self.base_path = config.base_path elif isinstance(source, Path): self.base_path = source.absolute() if source.is_dir() else source.absolute().parent else: self.base_path = Path.cwd() self.source: str | Path | list[Path] | ParseResult | dict[str, YamlValue] = source - self.custom_template_dir = custom_template_dir + self.custom_template_dir = config.custom_template_dir self.extra_template_data: defaultdict[str, Any] = extra_template_data or defaultdict(dict) - self.use_generic_base_class: bool = use_generic_base_class + self.use_generic_base_class: bool = config.use_generic_base_class self.generic_base_class_config: dict[str, Any] = {} - if allow_population_by_field_name: - if use_generic_base_class: + if config.allow_population_by_field_name: + if config.use_generic_base_class: self.generic_base_class_config["allow_population_by_field_name"] = True else: self.extra_template_data[ALL_MODEL]["allow_population_by_field_name"] = True - if allow_extra_fields: - if use_generic_base_class: + if config.allow_extra_fields: + if config.use_generic_base_class: self.generic_base_class_config["allow_extra_fields"] = True else: self.extra_template_data[ALL_MODEL]["allow_extra_fields"] = True - if extra_fields: - if use_generic_base_class: - self.generic_base_class_config["extra_fields"] = extra_fields + if config.extra_fields: + if config.use_generic_base_class: + self.generic_base_class_config["extra_fields"] = config.extra_fields else: - self.extra_template_data[ALL_MODEL]["extra_fields"] = extra_fields + self.extra_template_data[ALL_MODEL]["extra_fields"] = config.extra_fields - if enable_faux_immutability: - if use_generic_base_class: + if config.enable_faux_immutability: + if config.use_generic_base_class: self.generic_base_class_config["allow_mutation"] = False else: self.extra_template_data[ALL_MODEL]["allow_mutation"] = False - if use_attribute_docstrings: - if use_generic_base_class: + if config.use_attribute_docstrings: + if config.use_generic_base_class: self.generic_base_class_config["use_attribute_docstrings"] = True else: self.extra_template_data[ALL_MODEL]["use_attribute_docstrings"] = True - if target_pydantic_version: - if use_generic_base_class: - self.generic_base_class_config["target_pydantic_version"] = target_pydantic_version + if config.target_pydantic_version: + if config.use_generic_base_class: + self.generic_base_class_config["target_pydantic_version"] = config.target_pydantic_version else: - self.extra_template_data[ALL_MODEL]["target_pydantic_version"] = target_pydantic_version + self.extra_template_data[ALL_MODEL]["target_pydantic_version"] = config.target_pydantic_version self.model_resolver = ModelResolver( base_url=source.geturl() if isinstance(source, ParseResult) else None, - singular_name_suffix="" if disable_appending_item_suffix else None, - aliases=aliases, - empty_field_name=empty_enum_field_name, - snake_case_field=snake_case_field, - custom_class_name_generator=custom_class_name_generator, + singular_name_suffix="" if config.disable_appending_item_suffix else None, + aliases=config.aliases, + empty_field_name=config.empty_enum_field_name, + snake_case_field=config.snake_case_field, + custom_class_name_generator=config.custom_class_name_generator, base_path=self.base_path, - original_field_name_delimiter=original_field_name_delimiter, - special_field_name_prefix=special_field_name_prefix, - remove_special_field_name_prefix=remove_special_field_name_prefix, - capitalise_enum_members=capitalise_enum_members, - no_alias=no_alias, - parent_scoped_naming=parent_scoped_naming, - treat_dot_as_module=treat_dot_as_module, - naming_strategy=naming_strategy, - duplicate_name_suffix_map=duplicate_name_suffix, + original_field_name_delimiter=config.original_field_name_delimiter, + special_field_name_prefix=config.special_field_name_prefix, + remove_special_field_name_prefix=config.remove_special_field_name_prefix, + capitalise_enum_members=config.capitalise_enum_members, + no_alias=config.no_alias, + parent_scoped_naming=config.parent_scoped_naming, + treat_dot_as_module=config.treat_dot_as_module, + naming_strategy=config.naming_strategy, + duplicate_name_suffix_map=config.duplicate_name_suffix, ) - self.class_name: str | None = class_name - self.wrap_string_literal: bool | None = wrap_string_literal - self.http_headers: Sequence[tuple[str, str]] | None = http_headers - self.http_query_parameters: Sequence[tuple[str, str]] | None = http_query_parameters - self.http_ignore_tls: bool = http_ignore_tls - self.http_timeout: float | None = http_timeout - self.use_annotated: bool = use_annotated + self.class_name: str | None = config.class_name + self.wrap_string_literal: bool | None = config.wrap_string_literal + self.http_headers: Sequence[tuple[str, str]] | None = config.http_headers + self.http_query_parameters: Sequence[tuple[str, str]] | None = config.http_query_parameters + self.http_ignore_tls: bool = config.http_ignore_tls + self.http_timeout: float | None = config.http_timeout + self.use_annotated: bool = config.use_annotated if self.use_annotated and not self.field_constraints: # pragma: no cover msg = "`use_annotated=True` has to be used with `field_constraints=True`" raise Exception(msg) # noqa: TRY002 - self.use_serialize_as_any: bool = use_serialize_as_any - self.use_non_positive_negative_number_constrained_types = use_non_positive_negative_number_constrained_types - self.use_double_quotes = use_double_quotes - self.allow_responses_without_content = allow_responses_without_content - self.collapse_root_models = collapse_root_models - self.collapse_root_models_name_strategy = collapse_root_models_name_strategy - self.collapse_reuse_models = collapse_reuse_models - self.skip_root_model = skip_root_model - self.use_type_alias = use_type_alias - self.capitalise_enum_members = capitalise_enum_members - self.keep_model_order = keep_model_order - self.use_one_literal_as_default = use_one_literal_as_default - self.use_enum_values_in_discriminator = use_enum_values_in_discriminator - self.known_third_party = known_third_party - self.custom_formatter = custom_formatters - self.custom_formatters_kwargs = custom_formatters_kwargs - self.treat_dot_as_module = treat_dot_as_module - self.default_field_extras: dict[str, Any] | None = default_field_extras - self.formatters: list[Formatter] = formatters - self.defer_formatting: bool = defer_formatting - self.type_mappings: dict[tuple[str, str], str] = Parser._parse_type_mappings(type_mappings) - self.type_overrides: dict[str, str] = type_overrides or {} + self.use_serialize_as_any: bool = config.use_serialize_as_any + self.use_non_positive_negative_number_constrained_types = config.use_non_positive_negative_number_constrained_types + self.use_double_quotes = config.use_double_quotes + self.allow_responses_without_content = config.allow_responses_without_content + self.collapse_root_models = config.collapse_root_models + self.collapse_root_models_name_strategy = config.collapse_root_models_name_strategy + self.collapse_reuse_models = config.collapse_reuse_models + self.skip_root_model = config.skip_root_model + self.use_type_alias = config.use_type_alias + self.capitalise_enum_members = config.capitalise_enum_members + self.keep_model_order = config.keep_model_order + self.use_one_literal_as_default = config.use_one_literal_as_default + self.use_enum_values_in_discriminator = config.use_enum_values_in_discriminator + self.known_third_party = config.known_third_party + self.custom_formatter = config.custom_formatters + self.custom_formatters_kwargs = config.custom_formatters_kwargs + self.treat_dot_as_module = config.treat_dot_as_module + self.default_field_extras: dict[str, Any] | None = config.default_field_extras + self.formatters: list[Formatter] = config.formatters + self.defer_formatting: bool = config.defer_formatting + self.type_mappings: dict[tuple[str, str], str] = Parser._parse_type_mappings(config.type_mappings) + self.type_overrides: dict[str, str] = config.type_overrides or {} self._type_override_imports: dict[str, Import] = { key: Import.from_full_path(value) for key, value in self.type_overrides.items() } - self.read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = read_only_write_only_model_type - self.use_frozen_field: bool = use_frozen_field - self.use_default_factory_for_optional_nested_models: bool = use_default_factory_for_optional_nested_models - self.field_type_collision_strategy: FieldTypeCollisionStrategy | None = field_type_collision_strategy + self.read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = config.read_only_write_only_model_type + self.use_frozen_field: bool = config.use_frozen_field + self.use_default_factory_for_optional_nested_models: bool = config.use_default_factory_for_optional_nested_models + self.field_type_collision_strategy: FieldTypeCollisionStrategy | None = config.field_type_collision_strategy @property def field_name_model_type(self) -> ModelType: diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index 457fc5e21..160b72245 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -25,7 +25,6 @@ DataType, Parser, Source, - _get_option, escape_characters, ) from datamodel_code_generator.reference import ModelType, Reference @@ -94,21 +93,43 @@ def __init__( **options: Any, ) -> None: """Initialize the GraphQL parser with configuration options.""" - # Extract GraphQL-specific options - data_model_scalar_type = _get_option(options, "data_model_scalar_type", config, default=DataTypeScalar) - data_model_union_type = _get_option(options, "data_model_union_type", config, default=DataTypeUnion) - use_standard_collections = _get_option(options, "use_standard_collections", config, default=False) - use_union_operator = _get_option(options, "use_union_operator", config, default=False) - - if options.get("target_datetime_class") is None and (config is None or config.target_datetime_class is None): - options["target_datetime_class"] = DatetimeClassType.Datetime - - super().__init__(source=source, config=config, **options) + from datamodel_code_generator.config import GraphQLParserConfig as GraphQLParserConfigClass + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + # Rebuild the model to resolve forward references before validation + GraphQLParserConfigClass.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) - self.data_model_scalar_type = data_model_scalar_type - self.data_model_union_type = data_model_union_type - self.use_standard_collections = use_standard_collections - self.use_union_operator = use_union_operator + if config is None: + config = GraphQLParserConfigClass.model_validate(options) + options = {} # Clear options since they're now in config + elif options: + # Merge options into config (options take precedence) + config_dict = config.model_dump() + config_dict.update(options) + config = GraphQLParserConfigClass.model_validate(config_dict) + options = {} # Clear options since they're now in config + + if config.target_datetime_class is None: + # Create a new config with the default datetime class for GraphQL + config_dict = config.model_dump() + config_dict["target_datetime_class"] = DatetimeClassType.Datetime + config = GraphQLParserConfigClass.model_validate(config_dict) + + super().__init__(source=source, config=config) + + self.data_model_scalar_type = config.data_model_scalar_type or DataTypeScalar + self.data_model_union_type = config.data_model_union_type or DataTypeUnion + # GraphQL parser overrides these from base Parser for explicit use in parse_field + self.use_standard_collections = config.use_standard_collections + self.use_union_operator = config.use_union_operator def _get_context_source_path_parts(self) -> Iterator[tuple[Source, list[str]]]: # TODO (denisart): Temporarily this method duplicates diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 99e2cba79..464cd8e7a 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -632,10 +632,37 @@ def __init__( **options: Any, ) -> None: """Initialize the JSON Schema parser with configuration options.""" - if options.get("target_datetime_class") is None and (config is None or config.target_datetime_class is None): - options["target_datetime_class"] = DatetimeClassType.Awaredatetime + from datamodel_code_generator.config import ParserConfig as ParserConfigClass + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + # Rebuild the model to resolve forward references before validation + ParserConfigClass.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) - super().__init__(source=source, config=config, **options) + if config is None: + config = ParserConfigClass.model_validate(options) + options = {} # Clear options since they're now in config + elif options: + # Merge options into config (options take precedence) + config_dict = config.model_dump() + config_dict.update(options) + config = ParserConfigClass.model_validate(config_dict) + options = {} # Clear options since they're now in config + + if config.target_datetime_class is None: + # Create a new config with the default datetime class + config_dict = config.model_dump() + config_dict["target_datetime_class"] = DatetimeClassType.Awaredatetime + config = ParserConfigClass.model_validate(config_dict) + + super().__init__(source=source, config=config) self.remote_object_cache: DefaultPutDict[str, dict[str, YamlValue]] = DefaultPutDict() self.raw_obj: dict[str, YamlValue] = {} diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 5d54e8f35..9b2d3732b 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -25,7 +25,7 @@ snooper_to_methods, ) from datamodel_code_generator.format import DatetimeClassType -from datamodel_code_generator.parser.base import _get_option, get_special_path +from datamodel_code_generator.parser.base import get_special_path from datamodel_code_generator.parser.jsonschema import ( JsonSchemaObject, JsonSchemaParser, @@ -170,21 +170,41 @@ def __init__( **options: Any, ) -> None: """Initialize the OpenAPI parser with configuration options.""" - # Extract OpenAPI-specific options - openapi_scopes = _get_option(options, "openapi_scopes", config, default=None) - include_path_parameters = _get_option(options, "include_path_parameters", config, default=False) - use_status_code_in_response_name = _get_option( - options, "use_status_code_in_response_name", config, default=False + from datamodel_code_generator.config import OpenAPIParserConfig as OpenAPIParserConfigClass + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + # Rebuild the model to resolve forward references before validation + OpenAPIParserConfigClass.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } ) - if options.get("target_datetime_class") is None and (config is None or config.target_datetime_class is None): - options["target_datetime_class"] = DatetimeClassType.Awaredatetime - - super().__init__(source=source, config=config, **options) - - self.open_api_scopes: list[OpenAPIScope] = openapi_scopes or [OpenAPIScope.Schemas] - self.include_path_parameters: bool = include_path_parameters - self.use_status_code_in_response_name: bool = use_status_code_in_response_name + if config is None: + config = OpenAPIParserConfigClass.model_validate(options) + options = {} # Clear options since they're now in config + elif options: + # Merge options into config (options take precedence) + config_dict = config.model_dump() + config_dict.update(options) + config = OpenAPIParserConfigClass.model_validate(config_dict) + options = {} # Clear options since they're now in config + + if config.target_datetime_class is None: + # Create a new config with the default datetime class + config_dict = config.model_dump() + config_dict["target_datetime_class"] = DatetimeClassType.Awaredatetime + config = OpenAPIParserConfigClass.model_validate(config_dict) + + super().__init__(source=source, config=config) + + self.open_api_scopes: list[OpenAPIScope] = config.openapi_scopes or [OpenAPIScope.Schemas] + self.include_path_parameters: bool = config.include_path_parameters + self.use_status_code_in_response_name: bool = config.use_status_code_in_response_name self._discriminator_schemas: dict[str, dict[str, Any]] = {} self._discriminator_subtypes: dict[str, list[str]] = defaultdict(list) diff --git a/src/datamodel_code_generator/reference.py b/src/datamodel_code_generator/reference.py index 5f2b0a869..bb549796e 100644 --- a/src/datamodel_code_generator/reference.py +++ b/src/datamodel_code_generator/reference.py @@ -526,7 +526,7 @@ def __init__( # noqa: PLR0913, PLR0917 duplicate_name_suffix: str | None = None, base_url: str | None = None, singular_name_suffix: str | None = None, - aliases: Mapping[str, str] | None = None, + aliases: Mapping[str, str | list[str]] | None = None, snake_case_field: bool = False, # noqa: FBT001, FBT002 empty_field_name: str | None = None, custom_class_name_generator: Callable[[str], str] | None = None, From b0bacf7c8b654ecc384f68f1eb3bbdd4c0f83e6f Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 30 Dec 2025 15:02:49 +0000 Subject: [PATCH 5/7] Add ParserConfig.from_options() classmethod for Pydantic v1/v2 compatibility --- src/datamodel_code_generator/config.py | 34 ++++++- src/datamodel_code_generator/parser/base.py | 33 +++---- .../parser/graphql.py | 29 ++---- .../parser/jsonschema.py | 29 ++---- .../parser/openapi.py | 29 ++---- .../test_public_api_signature_baseline.py | 90 +++++++++++++++++-- 6 files changed, 150 insertions(+), 94 deletions(-) diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index 76364197a..bc2575057 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Annotated, Any from pydantic import BaseModel, Field +from typing_extensions import Self from datamodel_code_generator.enums import ( DEFAULT_SHARED_MODULE_NAME, @@ -201,9 +202,40 @@ class ParserConfig(BaseModel): class Config: """Pydantic v1 model config.""" - extra = "forbid" + extra = "allow" arbitrary_types_allowed = True + @classmethod + def from_options(cls, options: dict[str, Any]) -> Self: + """Create a ParserConfig from options dict with version-compatible validation.""" + if is_pydantic_v2(): + from datamodel_code_generator.model.base import ( # noqa: PLC0415 + DataModel, + DataModelFieldBase, + ) + from datamodel_code_generator.types import DataTypeManager, StrictTypes # noqa: PLC0415 + + cls.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + return cls.model_validate(options) + # For Pydantic v1, use construct() to skip validation (forward refs not resolved) + defaults: dict[str, Any] = {} + for field_name, field in cls.__fields__.items(): # type: ignore[attr-defined] + if field.default is not None: + defaults[field_name] = field.default + elif field.default_factory is not None: + defaults[field_name] = field.default_factory() # type: ignore[misc] + else: + defaults[field_name] = None + defaults.update(options) + return cls.construct(**defaults) # type: ignore[return-value] + data_model_type: type[DataModel] = pydantic_model.BaseModel data_model_root_type: type[DataModel] = pydantic_model.CustomRootType data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index f045166bc..985727cbe 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -23,7 +23,6 @@ from pydantic import BaseModel from datamodel_code_generator import ( - DEFAULT_SHARED_MODULE_NAME, AllExportsCollisionStrategy, AllExportsScope, AllOfMergeMode, @@ -36,11 +35,9 @@ YamlValue, ) from datamodel_code_generator.format import ( - DEFAULT_FORMATTERS, CodeFormatter, Formatter, PythonVersion, - PythonVersionMin, ) from datamodel_code_generator.imports import ( IMPORT_ANNOTATIONS, @@ -690,27 +687,15 @@ def __init__( # noqa: PLR0912, PLR0915 **options: Any, ) -> None: """Initialize the Parser with configuration options.""" - from datamodel_code_generator.config import ParserConfig - from datamodel_code_generator.model.base import DataModel, DataModelFieldBase - from datamodel_code_generator.types import DataTypeManager, StrictTypes - - # Rebuild the model to resolve forward references before validation - ParserConfig.model_rebuild( - _types_namespace={ - "DataModel": DataModel, - "DataModelFieldBase": DataModelFieldBase, - "DataTypeManager": DataTypeManager, - "StrictTypes": StrictTypes, - } - ) + from datamodel_code_generator.config import ParserConfig # noqa: PLC0415 + from datamodel_code_generator.util import model_dump # noqa: PLC0415 if config is None: - config = ParserConfig.model_validate(options) + config = ParserConfig.from_options(options) elif options: - # Merge options into config (options take precedence) - config_dict = config.model_dump() + config_dict = model_dump(config) config_dict.update(options) - config = ParserConfig.model_validate(config_dict) + config = ParserConfig.from_options(config_dict) extra_template_data: defaultdict[str, Any] | None = config.extra_template_data if extra_template_data is not None and not isinstance(extra_template_data, defaultdict): @@ -868,7 +853,9 @@ def __init__( # noqa: PLR0912, PLR0915 msg = "`use_annotated=True` has to be used with `field_constraints=True`" raise Exception(msg) # noqa: TRY002 self.use_serialize_as_any: bool = config.use_serialize_as_any - self.use_non_positive_negative_number_constrained_types = config.use_non_positive_negative_number_constrained_types + self.use_non_positive_negative_number_constrained_types = ( + config.use_non_positive_negative_number_constrained_types + ) self.use_double_quotes = config.use_double_quotes self.allow_responses_without_content = config.allow_responses_without_content self.collapse_root_models = config.collapse_root_models @@ -894,7 +881,9 @@ def __init__( # noqa: PLR0912, PLR0915 } self.read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = config.read_only_write_only_model_type self.use_frozen_field: bool = config.use_frozen_field - self.use_default_factory_for_optional_nested_models: bool = config.use_default_factory_for_optional_nested_models + self.use_default_factory_for_optional_nested_models: bool = ( + config.use_default_factory_for_optional_nested_models + ) self.field_type_collision_strategy: FieldTypeCollisionStrategy | None = config.field_type_collision_strategy @property diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index 160b72245..e6b480c27 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -93,35 +93,22 @@ def __init__( **options: Any, ) -> None: """Initialize the GraphQL parser with configuration options.""" - from datamodel_code_generator.config import GraphQLParserConfig as GraphQLParserConfigClass - from datamodel_code_generator.model.base import DataModel, DataModelFieldBase - from datamodel_code_generator.types import DataTypeManager, StrictTypes - - # Rebuild the model to resolve forward references before validation - GraphQLParserConfigClass.model_rebuild( - _types_namespace={ - "DataModel": DataModel, - "DataModelFieldBase": DataModelFieldBase, - "DataTypeManager": DataTypeManager, - "StrictTypes": StrictTypes, - } + from datamodel_code_generator.config import ( # noqa: PLC0415 + GraphQLParserConfig as GraphQLParserConfigClass, ) + from datamodel_code_generator.util import model_dump # noqa: PLC0415 if config is None: - config = GraphQLParserConfigClass.model_validate(options) - options = {} # Clear options since they're now in config + config = GraphQLParserConfigClass.from_options(options) elif options: - # Merge options into config (options take precedence) - config_dict = config.model_dump() + config_dict = model_dump(config) config_dict.update(options) - config = GraphQLParserConfigClass.model_validate(config_dict) - options = {} # Clear options since they're now in config + config = GraphQLParserConfigClass.from_options(config_dict) if config.target_datetime_class is None: - # Create a new config with the default datetime class for GraphQL - config_dict = config.model_dump() + config_dict = model_dump(config) config_dict["target_datetime_class"] = DatetimeClassType.Datetime - config = GraphQLParserConfigClass.model_validate(config_dict) + config = GraphQLParserConfigClass.from_options(config_dict) super().__init__(source=source, config=config) diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 464cd8e7a..ed85fb25c 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -632,35 +632,20 @@ def __init__( **options: Any, ) -> None: """Initialize the JSON Schema parser with configuration options.""" - from datamodel_code_generator.config import ParserConfig as ParserConfigClass - from datamodel_code_generator.model.base import DataModel, DataModelFieldBase - from datamodel_code_generator.types import DataTypeManager, StrictTypes - - # Rebuild the model to resolve forward references before validation - ParserConfigClass.model_rebuild( - _types_namespace={ - "DataModel": DataModel, - "DataModelFieldBase": DataModelFieldBase, - "DataTypeManager": DataTypeManager, - "StrictTypes": StrictTypes, - } - ) + from datamodel_code_generator.config import ParserConfig as ParserConfigClass # noqa: PLC0415 + from datamodel_code_generator.util import model_dump # noqa: PLC0415 if config is None: - config = ParserConfigClass.model_validate(options) - options = {} # Clear options since they're now in config + config = ParserConfigClass.from_options(options) elif options: - # Merge options into config (options take precedence) - config_dict = config.model_dump() + config_dict = model_dump(config) config_dict.update(options) - config = ParserConfigClass.model_validate(config_dict) - options = {} # Clear options since they're now in config + config = ParserConfigClass.from_options(config_dict) if config.target_datetime_class is None: - # Create a new config with the default datetime class - config_dict = config.model_dump() + config_dict = model_dump(config) config_dict["target_datetime_class"] = DatetimeClassType.Awaredatetime - config = ParserConfigClass.model_validate(config_dict) + config = ParserConfigClass.from_options(config_dict) super().__init__(source=source, config=config) diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 9b2d3732b..83af363b1 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -170,35 +170,22 @@ def __init__( **options: Any, ) -> None: """Initialize the OpenAPI parser with configuration options.""" - from datamodel_code_generator.config import OpenAPIParserConfig as OpenAPIParserConfigClass - from datamodel_code_generator.model.base import DataModel, DataModelFieldBase - from datamodel_code_generator.types import DataTypeManager, StrictTypes - - # Rebuild the model to resolve forward references before validation - OpenAPIParserConfigClass.model_rebuild( - _types_namespace={ - "DataModel": DataModel, - "DataModelFieldBase": DataModelFieldBase, - "DataTypeManager": DataTypeManager, - "StrictTypes": StrictTypes, - } + from datamodel_code_generator.config import ( # noqa: PLC0415 + OpenAPIParserConfig as OpenAPIParserConfigClass, ) + from datamodel_code_generator.util import model_dump # noqa: PLC0415 if config is None: - config = OpenAPIParserConfigClass.model_validate(options) - options = {} # Clear options since they're now in config + config = OpenAPIParserConfigClass.from_options(options) elif options: - # Merge options into config (options take precedence) - config_dict = config.model_dump() + config_dict = model_dump(config) config_dict.update(options) - config = OpenAPIParserConfigClass.model_validate(config_dict) - options = {} # Clear options since they're now in config + config = OpenAPIParserConfigClass.from_options(config_dict) if config.target_datetime_class is None: - # Create a new config with the default datetime class - config_dict = config.model_dump() + config_dict = model_dump(config) config_dict["target_datetime_class"] = DatetimeClassType.Awaredatetime - config = OpenAPIParserConfigClass.model_validate(config_dict) + config = OpenAPIParserConfigClass.from_options(config_dict) super().__init__(source=source, config=config) diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 884e62fb3..f6fc2ea8a 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -66,7 +66,7 @@ def _baseline_generate( field_constraints: bool = False, snake_case_field: bool = False, strip_default_none: bool = False, - aliases: Mapping[str, str] | None = None, + aliases: Mapping[str, str | list[str]] | None = None, disable_timestamp: bool = False, enable_version_header: bool = False, enable_command_header: bool = False, @@ -230,12 +230,88 @@ def _type_to_str(tp: Any) -> str: ) -def _normalize_union_str(type_str: str) -> str: - """Normalize a union type string by sorting its components.""" - if " | " in type_str: - parts = [p.strip() for p in type_str.split(" | ")] - return " | ".join(sorted(parts)) - return type_str +def _normalize_union_str(type_str: str) -> str: # noqa: PLR0912 + """Normalize a union type string by sorting its components recursively. + + Handles both top-level unions and unions inside generics. + """ + # First, recursively normalize any generics (inside brackets) + result = [] + i = 0 + while i < len(type_str): + if type_str[i] == "[": + # Find matching bracket + depth = 1 + start = i + 1 + i += 1 + while i < len(type_str) and depth > 0: + if type_str[i] == "[": + depth += 1 + elif type_str[i] == "]": + depth -= 1 + i += 1 + # Recursively normalize the content inside brackets + inner = type_str[start : i - 1] + # Split on comma at top level for generic args + args = _split_generic_args(inner) + normalized_args = ", ".join(_normalize_union_str(arg.strip()) for arg in args) + result.append("[" + normalized_args + "]") + else: + result.append(type_str[i]) + i += 1 + + type_str = "".join(result) + + # Then handle top-level union + if " | " not in type_str: + return type_str + + parts = [] + current_part = [] + depth = 0 + i = 0 + while i < len(type_str): + char = type_str[i] + if char == "[": + depth += 1 + current_part.append(char) + elif char == "]": + depth -= 1 + current_part.append(char) + elif type_str[i : i + 3] == " | " and depth == 0: + parts.append("".join(current_part).strip()) + current_part = [] + i += 3 + continue + else: + current_part.append(char) + i += 1 + if current_part: + parts.append("".join(current_part).strip()) + + return " | ".join(sorted(parts)) + + +def _split_generic_args(args_str: str) -> list[str]: + """Split generic args on comma, respecting bracket nesting.""" + parts = [] + current_part = [] + depth = 0 + for char in args_str: + if char == "[": + depth += 1 + current_part.append(char) + elif char == "]": + depth -= 1 + current_part.append(char) + elif char == "," and depth == 0: + parts.append("".join(current_part).strip()) + current_part = [] + else: + current_part.append(char) + if current_part: + parts.append("".join(current_part).strip()) + return parts def _normalize_type(tp: Any) -> str: # noqa: PLR0911 From a497759f8b8715afc952964acc26659af5f5b5df Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 30 Dec 2025 15:31:50 +0000 Subject: [PATCH 6/7] Add tests for Parser config+options coverage and simplify base Parser.__init__ --- src/datamodel_code_generator/parser/base.py | 9 +- .../test_public_api_signature_baseline.py | 167 +++++++++++++++++- 2 files changed, 169 insertions(+), 7 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 985727cbe..58959e2df 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -684,18 +684,15 @@ def __init__( # noqa: PLR0912, PLR0915 source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, config: ParserConfig | None = None, - **options: Any, + **options: Any, # NOTE: Subclasses handle config+options merging, so options is typically empty here ) -> None: """Initialize the Parser with configuration options.""" from datamodel_code_generator.config import ParserConfig # noqa: PLC0415 - from datamodel_code_generator.util import model_dump # noqa: PLC0415 + # Subclasses are expected to merge config+options before calling super().__init__() + # This branch handles the case when Parser is used directly (e.g., for testing) if config is None: config = ParserConfig.from_options(options) - elif options: - config_dict = model_dump(config) - config_dict.update(options) - config = ParserConfig.from_options(config_dict) extra_template_data: defaultdict[str, Any] | None = config.extra_template_data if extra_template_data is not None and not isinstance(extra_template_data, defaultdict): diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index f6fc2ea8a..e4be29743 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -4,6 +4,7 @@ import inspect import types +from collections import defaultdict from typing import TYPE_CHECKING, Annotated, Any, ForwardRef, Union, get_args, get_origin import pytest @@ -33,7 +34,6 @@ PYDANTIC_V2_SKIP = pytest.mark.skipif(not is_pydantic_v2(), reason="Pydantic v2 required") if TYPE_CHECKING: - from collections import defaultdict from collections.abc import Callable, Mapping, Sequence from pathlib import Path from urllib.parse import ParseResult @@ -698,3 +698,168 @@ def test_graphql_parser_with_config() -> None: assert parser.snake_case_field is True assert parser.data_model_scalar_type is DataTypeScalar assert parser.data_model_union_type is DataTypeUnion + + +@PYDANTIC_V2_SKIP +def test_jsonschema_parser_with_config_and_options() -> None: + """Test that JsonSchemaParser correctly merges config with options (options take precedence).""" + from datamodel_code_generator.config import ParserConfig + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + ParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + + # Config sets validation=True, snake_case_field=True + config = ParserConfig( + validation=True, + snake_case_field=True, + use_schema_description=False, + ) + + # Options override snake_case_field to False and set use_schema_description=True + parser = JsonSchemaParser( + source='{"type": "object", "properties": {"testField": {"type": "string"}}}', + config=config, + snake_case_field=False, + use_schema_description=True, + ) + + # validation from config, snake_case_field overridden by options, use_schema_description from options + assert parser.validation is True + assert parser.snake_case_field is False + assert parser.use_schema_description is True + + +@PYDANTIC_V2_SKIP +def test_openapi_parser_with_config_and_options() -> None: + """Test that OpenAPIParser correctly merges config with options (options take precedence).""" + from datamodel_code_generator.config import OpenAPIParserConfig + from datamodel_code_generator.enums import OpenAPIScope + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.parser.openapi import OpenAPIParser + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + OpenAPIParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + + config = OpenAPIParserConfig( + validation=True, + snake_case_field=True, + openapi_scopes=[OpenAPIScope.Schemas], + ) + + openapi_spec = """{ + "openapi": "3.0.0", + "info": {"title": "Test", "version": "1.0.0"}, + "paths": {}, + "components": {"schemas": {"Test": {"type": "object"}}} + }""" + + # Options override snake_case_field and openapi_scopes + parser = OpenAPIParser( + source=openapi_spec, + config=config, + snake_case_field=False, + openapi_scopes=[OpenAPIScope.Schemas, OpenAPIScope.Paths], + ) + + assert parser.validation is True + assert parser.snake_case_field is False + assert parser.open_api_scopes == [OpenAPIScope.Schemas, OpenAPIScope.Paths] + + +@PYDANTIC_V2_SKIP +def test_graphql_parser_with_config_and_options() -> None: + """Test that GraphQLParser correctly merges config with options (options take precedence).""" + from datamodel_code_generator.config import GraphQLParserConfig + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.model.scalar import DataTypeScalar + from datamodel_code_generator.model.union import DataTypeUnion + from datamodel_code_generator.parser.graphql import GraphQLParser + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + GraphQLParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + + config = GraphQLParserConfig( + validation=True, + snake_case_field=True, + data_model_scalar_type=DataTypeScalar, + ) + + graphql_schema = """ + type Query { + test: String + } + """ + + # Options override snake_case_field and add data_model_union_type + parser = GraphQLParser( + source=graphql_schema, + config=config, + snake_case_field=False, + data_model_union_type=DataTypeUnion, + ) + + assert parser.validation is True + assert parser.snake_case_field is False + assert parser.data_model_scalar_type is DataTypeScalar + assert parser.data_model_union_type is DataTypeUnion + + +@PYDANTIC_V2_SKIP +def test_parser_with_extra_template_data_as_regular_dict() -> None: + """Test that Parser correctly converts extra_template_data from dict to defaultdict.""" + from datamodel_code_generator.config import ParserConfig + from datamodel_code_generator.model.base import DataModel, DataModelFieldBase + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + from datamodel_code_generator.types import DataTypeManager, StrictTypes + + ParserConfig.model_rebuild( + _types_namespace={ + "DataModel": DataModel, + "DataModelFieldBase": DataModelFieldBase, + "DataTypeManager": DataTypeManager, + "StrictTypes": StrictTypes, + } + ) + + # Pass extra_template_data as a regular dict (not defaultdict) + extra_data: dict[str, dict[str, Any]] = {"TestModel": {"custom_key": "custom_value"}} + + config = ParserConfig( + validation=True, + extra_template_data=extra_data, # type: ignore[arg-type] + ) + + parser = JsonSchemaParser( + source='{"type": "object", "properties": {"testField": {"type": "string"}}}', + config=config, + ) + + # Verify the extra_template_data was converted to defaultdict and is accessible + assert parser.extra_template_data is not None + assert isinstance(parser.extra_template_data, defaultdict) + assert parser.extra_template_data["TestModel"]["custom_key"] == "custom_value" + # Verify defaultdict behavior - accessing non-existent key returns empty dict + assert parser.extra_template_data["NonExistentModel"] == {} From 507c33f046a22935bf461d093ddf7a9d6ba0a4ae Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 30 Dec 2025 15:42:44 +0000 Subject: [PATCH 7/7] Remove dead code in Pydantic v1 from_options path --- src/datamodel_code_generator/config.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index bc2575057..86e8faeb3 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -227,12 +227,8 @@ def from_options(cls, options: dict[str, Any]) -> Self: # For Pydantic v1, use construct() to skip validation (forward refs not resolved) defaults: dict[str, Any] = {} for field_name, field in cls.__fields__.items(): # type: ignore[attr-defined] - if field.default is not None: - defaults[field_name] = field.default - elif field.default_factory is not None: - defaults[field_name] = field.default_factory() # type: ignore[misc] - else: - defaults[field_name] = None + # Note: ParserConfig fields don't use default_factory, so we only handle static defaults + defaults[field_name] = field.default defaults.update(options) return cls.construct(**defaults) # type: ignore[return-value]