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): 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 25635ad25..86e8faeb3 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, @@ -85,7 +86,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 @@ -201,9 +202,36 @@ 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] + # 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] + 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 @@ -220,7 +248,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 @@ -316,6 +344,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..58959e2df 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, @@ -31,20 +30,14 @@ Error, FieldTypeCollisionStrategy, ModuleSplitMode, - NamingStrategy, ReadOnlyWriteOnlyModelType, ReuseScope, - TargetPydanticVersion, YamlValue, ) from datamodel_code_generator.format import ( - DEFAULT_FORMATTERS, CodeFormatter, - DateClassType, - DatetimeClassType, Formatter, PythonVersion, - PythonVersionMin, ) from datamodel_code_generator.imports import ( IMPORT_ANNOTATIONS, @@ -75,13 +68,13 @@ 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 @runtime_checkable @@ -686,302 +679,209 @@ class Parser(ABC): parse_raw() to handle specific schema formats. """ - def __init__( # noqa: PLR0912, PLR0913, PLR0915 + def __init__( # noqa: PLR0912, PLR0915 self, source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, - 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, + config: ParserConfig | None = None, + **options: Any, # NOTE: Subclasses handle config+options merging, so options is typically empty here ) -> None: """Initialize the Parser with configuration options.""" - 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, + from datamodel_code_generator.config import ParserConfig # 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) + + 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 = 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 b583399e6..e6b480c27 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -13,30 +13,14 @@ ) 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, @@ -44,7 +28,7 @@ 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,8 +38,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. @@ -99,242 +85,38 @@ class GraphQLParser(Parser): graphql.type.introspection.TypeKind.UNION, ] - def __init__( # noqa: PLR0913 + def __init__( self, source: str | Path | ParseResult, *, - 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, + config: GraphQLParserConfig | None = None, + **options: Any, ) -> None: """Initialize the GraphQL parser with configuration options.""" - super().__init__( - source=source, - 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, + from datamodel_code_generator.config import ( # noqa: PLC0415 + GraphQLParserConfig as GraphQLParserConfigClass, ) - - 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 + from datamodel_code_generator.util import model_dump # noqa: PLC0415 + + if config is None: + config = GraphQLParserConfigClass.from_options(options) + elif options: + config_dict = model_dump(config) + config_dict.update(options) + config = GraphQLParserConfigClass.from_options(config_dict) + + if config.target_datetime_class is None: + config_dict = model_dump(config) + config_dict["target_datetime_class"] = DatetimeClassType.Datetime + config = GraphQLParserConfigClass.from_options(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 02e5fbb19..ed85fb25c 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,9 @@ 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 def unescape_json_pointer_segment(segment: str) -> str: @@ -637,236 +624,30 @@ class JsonSchemaParser(Parser): "ChainMap", }) - def __init__( # noqa: PLR0913 + def __init__( self, source: str | Path | list[Path] | ParseResult, *, - 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, + config: ParserConfig | 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, - 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, - ) + 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.from_options(options) + elif options: + config_dict = model_dump(config) + config_dict.update(options) + config = ParserConfigClass.from_options(config_dict) + + if config.target_datetime_class is None: + config_dict = model_dump(config) + config_dict["target_datetime_class"] = DatetimeClassType.Awaredatetime + config = ParserConfigClass.from_options(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 f11134fcf..83af363b1 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -18,27 +18,13 @@ 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.format import DatetimeClassType from datamodel_code_generator.parser.base import get_special_path from datamodel_code_generator.parser.jsonschema import ( JsonSchemaObject, @@ -46,19 +32,14 @@ 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.parser import DefaultPutDict + from datamodel_code_generator.config import OpenAPIParserConfig + from datamodel_code_generator.model.base import DataModelFieldBase RE_APPLICATION_JSON_PATTERN: Pattern[str] = re.compile(r"^application/.*json$") @@ -181,242 +162,36 @@ class OpenAPIParser(JsonSchemaParser): SCHEMA_PATHS: ClassVar[list[str]] = ["#/components/schemas"] - def __init__( # noqa: PLR0913 + def __init__( self, source: str | Path | list[Path] | ParseResult, *, - 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, + config: OpenAPIParserConfig | None = None, + **options: Any, ) -> None: - """Initialize the OpenAPI parser with extensive configuration options.""" - target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime - super().__init__( - source=source, - 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.""" + from datamodel_code_generator.config import ( # noqa: PLC0415 + OpenAPIParserConfig as OpenAPIParserConfigClass, ) - 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 + from datamodel_code_generator.util import model_dump # noqa: PLC0415 + + if config is None: + config = OpenAPIParserConfigClass.from_options(options) + elif options: + config_dict = model_dump(config) + config_dict.update(options) + config = OpenAPIParserConfigClass.from_options(config_dict) + + if config.target_datetime_class is None: + config_dict = model_dump(config) + config_dict["target_datetime_class"] = DatetimeClassType.Awaredatetime + config = OpenAPIParserConfigClass.from_options(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, diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 7042596bd..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 @@ -27,26 +28,21 @@ 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 + 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 @@ -70,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, @@ -183,116 +179,8 @@ def __init__( self, source: str | Path | list[Path] | ParseResult | dict[str, YamlValue], *, - 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, + config: ParserConfig | None = None, + **options: Any, ) -> None: raise NotImplementedError @@ -342,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 @@ -603,3 +567,299 @@ 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 + + +@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"] == {}