Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
076e228
Add GenerateConfig class and auto-generate TypedDicts for type-safe c…
koxudaxi Dec 28, 2025
cb34507
Add --unsafe-fixes to ruff-check formatter
koxudaxi Dec 28, 2025
68fb0bd
Skip config-types hook in lint workflow
koxudaxi Dec 28, 2025
b3757ef
Fix UnionType serialization and skip tox-dependent hooks in lint CI
koxudaxi Dec 28, 2025
59cb501
Revert --unsafe-fixes and exclude _types from pre-commit ruff
koxudaxi Dec 28, 2025
389e47c
Regenerate config types after merging main
koxudaxi Dec 28, 2025
313afc8
Remove UnionType handling (should be separate PR)
koxudaxi Dec 28, 2025
b95d4fc
Pin Python 3.14 for config-types CI
koxudaxi Dec 28, 2025
11c5127
Remove unused _from_generate_config methods
koxudaxi Dec 28, 2025
5a43cc3
Fix use_standard_collections merge logic
koxudaxi Dec 28, 2025
3c36b64
Fix coverage omit pattern for _types directory
koxudaxi Dec 28, 2025
c12a383
Regenerate TypedDicts after merging set/frozenset fix
koxudaxi Dec 28, 2025
8d05fb0
Add test for extra_template_data override and coverage pragmas
koxudaxi Dec 29, 2025
ead4e16
Restore --unsafe-fixes flag in ruff commands
koxudaxi Dec 29, 2025
417ac76
Merge remote-tracking branch 'origin/main' into refactor/config-class
koxudaxi Dec 29, 2025
ef68905
Restore _try_rebuild_model and test_input_model_config_class after merge
koxudaxi Dec 29, 2025
445bc44
Use --input-model-ref-strategy to simplify config type generation
koxudaxi Dec 29, 2025
51bfc28
Merge remote-tracking branch 'origin/main' into refactor/config-class
koxudaxi Dec 30, 2025
c8d60ef
Regenerate config types with improved reuse-foreign strategy
koxudaxi Dec 30, 2025
3230759
Merge remote-tracking branch 'origin/main' into refactor/config-class
koxudaxi Dec 30, 2025
5ac576b
Merge remote-tracking branch 'origin/main' into refactor/config-class
koxudaxi Dec 30, 2025
881c907
Merge remote-tracking branch 'origin/main' into refactor/config-class
koxudaxi Dec 30, 2025
e1232b8
Merge remote-tracking branch 'origin/main' into refactor/config-class
koxudaxi Dec 30, 2025
c308216
Remove WithJsonSchema annotations - automatic handling works
koxudaxi Dec 30, 2025
6d513e8
Add tests to ensure Config and ConfigDict fields/types match
koxudaxi Dec 30, 2025
1aff1b7
Add comprehensive Config/TypedDict compatibility tests
koxudaxi Dec 30, 2025
e44169b
Merge remote-tracking branch 'origin/main' into refactor/config-class
koxudaxi Dec 30, 2025
78e492e
Use defaultdict for extra_template_data to match generate() signature
koxudaxi Dec 30, 2025
1689892
Fix N806 lint: rename UnionMode variable to lowercase
koxudaxi Dec 30, 2025
abfc76c
Simplify type comparison tests for Config/TypedDict
koxudaxi Dec 30, 2025
2104ed0
Add type comparison tests for Config/TypedDict equivalence
koxudaxi Dec 30, 2025
9e89519
Fix formatting
koxudaxi Dec 30, 2025
b786adc
Skip Config/TypedDict tests on Pydantic v1
koxudaxi Dec 30, 2025
6e861cd
Add default value tests for ParserConfig and ParseConfig
koxudaxi Dec 30, 2025
87efda7
Use @PYDANTIC_V2_SKIP decorator for pydantic v2 tests
koxudaxi Dec 30, 2025
fba28c9
Fix type normalization for types.UnionType (Python 3.10+)
koxudaxi Dec 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/config-types.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Check config types

on:
push:
paths:
- 'src/datamodel_code_generator/config.py'
- 'src/datamodel_code_generator/_types/**'
- 'pyproject.toml'
- '.github/workflows/config-types.yaml'
pull_request:
paths:
- 'src/datamodel_code_generator/config.py'
- 'src/datamodel_code_generator/_types/**'
- 'pyproject.toml'
- '.github/workflows/config-types.yaml'

jobs:
config-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- run: uv python install 3.14

- run: uv tool install --python 3.14 tox --with tox-uv

- run: tox -e config-types -- --check
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- run: uvx prek run --all-files --show-diff-on-failure --skip readme
- run: SKIP=readme,config-types uvx prek run --all-files --show-diff-on-failure
- if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository ||
Expand Down
10 changes: 8 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ repos:
rev: 'v0.14.9'
hooks:
- id: ruff-format
exclude: "^tests/data"
exclude: "^tests/data|^src/datamodel_code_generator/_types/"
- id: ruff
exclude: "^tests/data"
exclude: "^tests/data|^src/datamodel_code_generator/_types/"
args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
Expand All @@ -37,3 +37,9 @@ repos:
language: system
files: ^(src/datamodel_code_generator/arguments\.py|README\.md|docs/index\.md)$
pass_filenames: false
- id: config-types
name: Generate config TypedDicts
entry: bash -c 'test -x .tox/dev/bin/python || tox run -e dev --notest -qq; .tox/dev/bin/datamodel-codegen --profile generate-config-dict && .tox/dev/bin/datamodel-codegen --profile parser-config-dict && .tox/dev/bin/datamodel-codegen --profile parse-config-dict'
language: system
files: ^src/datamodel_code_generator/config\.py$
pass_filenames: false
29 changes: 28 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ paths.other = [
"*\\datamodel-code-generator",
]
run.dynamic_context = "none"
run.omit = [ "tests/data/*", "tests/main/test_performance.py" ]
run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*" ]
report.fail_under = 88
run.parallel = true
run.plugins = [
Expand All @@ -258,3 +258,30 @@ reportPrivateImportUsage = false
[tool.pydantic-pycharm-plugin]
ignore-init-method-arguments = true
parsable-types.str = [ "int", "float" ]

[tool.datamodel-codegen.profiles.config-types-base]
enum-field-as-literal = "none"
use-standard-primitive-types = true
disable-warnings = true
disable-timestamp = true
output-model-type = "typing.TypedDict"
formatters = [ "ruff-format", "ruff-check" ]
input-model-ref-strategy = "reuse-foreign"

[tool.datamodel-codegen.profiles.generate-config-dict]
extends = "config-types-base"
input-model = "src/datamodel_code_generator/config.py:GenerateConfig"
output = "src/datamodel_code_generator/_types/generate_config_dict.py"
class-name = "GenerateConfigDict"

[tool.datamodel-codegen.profiles.parser-config-dict]
extends = "config-types-base"
input-model = "src/datamodel_code_generator/config.py:ParserConfig"
output = "src/datamodel_code_generator/_types/parser_config_dict.py"
class-name = "ParserConfigDict"

[tool.datamodel-codegen.profiles.parse-config-dict]
extends = "config-types-base"
input-model = "src/datamodel_code_generator/config.py:ParseConfig"
output = "src/datamodel_code_generator/_types/parse_config_dict.py"
class-name = "ParseConfigDict"
188 changes: 187 additions & 1 deletion src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
if TYPE_CHECKING:
from collections import defaultdict

from datamodel_code_generator.config import GenerateConfig
from datamodel_code_generator.model.pydantic_v2 import UnionMode
from datamodel_code_generator.parser.base import Parser
from datamodel_code_generator.types import StrictTypes
Expand Down Expand Up @@ -451,6 +452,7 @@ def _build_module_content(
def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
input_: Path | str | ParseResult | Mapping[str, Any],
*,
config: GenerateConfig | None = None,
input_filename: str | None = None,
input_file_type: InputFileType = InputFileType.Auto,
output: Path | None = None,
Expand Down Expand Up @@ -512,7 +514,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
model_extra_keys_without_x_prefix: set[str] | None = None,
openapi_scopes: list[OpenAPIScope] | None = None,
include_path_parameters: bool = False,
graphql_scopes: list[GraphQLScope] | None = None, # noqa: ARG001
graphql_scopes: list[GraphQLScope] | None = None,
wrap_string_literal: bool | None = None,
use_title_as_name: bool = False,
use_operation_id_as_name: bool = False,
Expand Down Expand Up @@ -583,6 +585,190 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
- When output is None and multiple modules: GeneratedModules (dict mapping
module path tuples to generated code strings)
"""
if config is not None:
input_filename = config.input_filename if input_filename is None else input_filename
input_file_type = config.input_file_type if input_file_type == InputFileType.Auto else input_file_type
output = config.output if output is None else output
output_model_type = (
config.output_model_type if output_model_type == DataModelType.PydanticBaseModel else output_model_type
)
target_python_version = (
config.target_python_version if target_python_version == PythonVersionMin else target_python_version
)
target_pydantic_version = (
config.target_pydantic_version if target_pydantic_version is None else target_pydantic_version
)
base_class = base_class or config.base_class
base_class_map = config.base_class_map if base_class_map is None else base_class_map
additional_imports = config.additional_imports if additional_imports is None else additional_imports
class_decorators = config.class_decorators if class_decorators is None else class_decorators
custom_template_dir = config.custom_template_dir if custom_template_dir is None else custom_template_dir
if extra_template_data is None and config.extra_template_data is not None:
from collections import defaultdict as _defaultdict # noqa: PLC0415

extra_template_data = _defaultdict(dict, config.extra_template_data)
validation = validation or config.validation
field_constraints = field_constraints or config.field_constraints
snake_case_field = snake_case_field or config.snake_case_field
strip_default_none = strip_default_none or config.strip_default_none
aliases = config.aliases if aliases is None else aliases
disable_timestamp = disable_timestamp or config.disable_timestamp
enable_version_header = enable_version_header or config.enable_version_header
enable_command_header = enable_command_header or config.enable_command_header
command_line = config.command_line if command_line is None else command_line
allow_population_by_field_name = allow_population_by_field_name or config.allow_population_by_field_name
allow_extra_fields = allow_extra_fields or config.allow_extra_fields
extra_fields = config.extra_fields if extra_fields is None else extra_fields
use_generic_base_class = use_generic_base_class or config.use_generic_base_class
apply_default_values_for_required_fields = (
apply_default_values_for_required_fields or config.apply_default_values_for_required_fields
)
force_optional_for_required_fields = (
force_optional_for_required_fields or config.force_optional_for_required_fields
)
class_name = config.class_name if class_name is None else class_name
use_standard_collections = (
config.use_standard_collections if use_standard_collections else use_standard_collections
)
use_schema_description = use_schema_description or config.use_schema_description
use_field_description = use_field_description or config.use_field_description
use_field_description_example = use_field_description_example or config.use_field_description_example
use_attribute_docstrings = use_attribute_docstrings or config.use_attribute_docstrings
use_inline_field_description = use_inline_field_description or config.use_inline_field_description
use_default_kwarg = use_default_kwarg or config.use_default_kwarg
reuse_model = reuse_model or config.reuse_model
reuse_scope = config.reuse_scope if reuse_scope == ReuseScope.Module else reuse_scope
shared_module_name = (
config.shared_module_name if shared_module_name == DEFAULT_SHARED_MODULE_NAME else shared_module_name
)
encoding = config.encoding if encoding == "utf-8" else encoding
enum_field_as_literal = config.enum_field_as_literal if enum_field_as_literal is None else enum_field_as_literal
enum_field_as_literal_map = (
config.enum_field_as_literal_map if enum_field_as_literal_map is None else enum_field_as_literal_map
)
ignore_enum_constraints = ignore_enum_constraints or config.ignore_enum_constraints
use_one_literal_as_default = use_one_literal_as_default or config.use_one_literal_as_default
use_enum_values_in_discriminator = use_enum_values_in_discriminator or config.use_enum_values_in_discriminator
set_default_enum_member = set_default_enum_member or config.set_default_enum_member
use_subclass_enum = use_subclass_enum or config.use_subclass_enum
use_specialized_enum = config.use_specialized_enum if use_specialized_enum else False
strict_nullable = strict_nullable or config.strict_nullable
use_generic_container_types = use_generic_container_types or config.use_generic_container_types
enable_faux_immutability = enable_faux_immutability or config.enable_faux_immutability
disable_appending_item_suffix = disable_appending_item_suffix or config.disable_appending_item_suffix
strict_types = config.strict_types if strict_types is None else strict_types
empty_enum_field_name = config.empty_enum_field_name if empty_enum_field_name is None else empty_enum_field_name
custom_class_name_generator = (
config.custom_class_name_generator if custom_class_name_generator is None else custom_class_name_generator
)
field_extra_keys = config.field_extra_keys if field_extra_keys is None else field_extra_keys
field_include_all_keys = field_include_all_keys or config.field_include_all_keys
field_extra_keys_without_x_prefix = (
config.field_extra_keys_without_x_prefix
if field_extra_keys_without_x_prefix is None
else field_extra_keys_without_x_prefix
)
model_extra_keys = config.model_extra_keys if model_extra_keys is None else model_extra_keys
model_extra_keys_without_x_prefix = (
config.model_extra_keys_without_x_prefix
if model_extra_keys_without_x_prefix is None
else model_extra_keys_without_x_prefix
)
openapi_scopes = config.openapi_scopes if openapi_scopes is None else openapi_scopes
include_path_parameters = include_path_parameters or config.include_path_parameters
graphql_scopes = config.graphql_scopes if graphql_scopes is None else graphql_scopes
Comment thread Dismissed
wrap_string_literal = config.wrap_string_literal if wrap_string_literal is None else wrap_string_literal
use_title_as_name = use_title_as_name or config.use_title_as_name
use_operation_id_as_name = use_operation_id_as_name or config.use_operation_id_as_name
use_unique_items_as_set = use_unique_items_as_set or config.use_unique_items_as_set
use_tuple_for_fixed_items = use_tuple_for_fixed_items or config.use_tuple_for_fixed_items
allof_merge_mode = (
config.allof_merge_mode if allof_merge_mode == AllOfMergeMode.Constraints else allof_merge_mode
)
http_headers = config.http_headers if http_headers is None else http_headers
http_ignore_tls = http_ignore_tls or config.http_ignore_tls
http_timeout = config.http_timeout if http_timeout is None else http_timeout
use_annotated = use_annotated or config.use_annotated
use_serialize_as_any = use_serialize_as_any or config.use_serialize_as_any
use_non_positive_negative_number_constrained_types = (
use_non_positive_negative_number_constrained_types
or config.use_non_positive_negative_number_constrained_types
)
use_decimal_for_multiple_of = use_decimal_for_multiple_of or config.use_decimal_for_multiple_of
original_field_name_delimiter = (
config.original_field_name_delimiter
if original_field_name_delimiter is None
else original_field_name_delimiter
)
use_double_quotes = use_double_quotes or config.use_double_quotes
use_union_operator = config.use_union_operator if use_union_operator else False
collapse_root_models = collapse_root_models or config.collapse_root_models
collapse_root_models_name_strategy = (
config.collapse_root_models_name_strategy
if collapse_root_models_name_strategy is None
else collapse_root_models_name_strategy
)
collapse_reuse_models = collapse_reuse_models or config.collapse_reuse_models
skip_root_model = skip_root_model or config.skip_root_model
use_type_alias = use_type_alias or config.use_type_alias
use_root_model_type_alias = use_root_model_type_alias or config.use_root_model_type_alias
special_field_name_prefix = (
config.special_field_name_prefix if special_field_name_prefix is None else special_field_name_prefix
)
remove_special_field_name_prefix = remove_special_field_name_prefix or config.remove_special_field_name_prefix
capitalise_enum_members = capitalise_enum_members or config.capitalise_enum_members
keep_model_order = keep_model_order or config.keep_model_order
custom_file_header = config.custom_file_header if custom_file_header is None else custom_file_header
custom_file_header_path = (
config.custom_file_header_path if custom_file_header_path is None else custom_file_header_path
)
custom_formatters = config.custom_formatters if custom_formatters is None else custom_formatters
custom_formatters_kwargs = (
config.custom_formatters_kwargs if custom_formatters_kwargs is None else custom_formatters_kwargs
)
use_pendulum = use_pendulum or config.use_pendulum
use_standard_primitive_types = use_standard_primitive_types or config.use_standard_primitive_types
http_query_parameters = config.http_query_parameters if http_query_parameters is None else http_query_parameters
treat_dot_as_module = config.treat_dot_as_module if treat_dot_as_module is None else treat_dot_as_module
use_exact_imports = use_exact_imports or config.use_exact_imports
union_mode = config.union_mode if union_mode is None else union_mode
output_datetime_class = config.output_datetime_class if output_datetime_class is None else output_datetime_class
output_date_class = config.output_date_class if output_date_class is None else output_date_class
keyword_only = keyword_only or config.keyword_only
frozen_dataclasses = frozen_dataclasses or config.frozen_dataclasses
no_alias = no_alias or config.no_alias
use_frozen_field = use_frozen_field or config.use_frozen_field
use_default_factory_for_optional_nested_models = (
use_default_factory_for_optional_nested_models or config.use_default_factory_for_optional_nested_models
)
formatters = config.formatters if formatters == DEFAULT_FORMATTERS else formatters
settings_path = config.settings_path if settings_path is None else settings_path
parent_scoped_naming = parent_scoped_naming or config.parent_scoped_naming
naming_strategy = config.naming_strategy if naming_strategy is None else naming_strategy
duplicate_name_suffix = config.duplicate_name_suffix if duplicate_name_suffix is None else duplicate_name_suffix
dataclass_arguments = config.dataclass_arguments if dataclass_arguments is None else dataclass_arguments
disable_future_imports = disable_future_imports or config.disable_future_imports
type_mappings = config.type_mappings if type_mappings is None else type_mappings
type_overrides = config.type_overrides if type_overrides is None else type_overrides
read_only_write_only_model_type = (
config.read_only_write_only_model_type
if read_only_write_only_model_type is None
else read_only_write_only_model_type
)
use_status_code_in_response_name = use_status_code_in_response_name or config.use_status_code_in_response_name
all_exports_scope = config.all_exports_scope if all_exports_scope is None else all_exports_scope
all_exports_collision_strategy = (
config.all_exports_collision_strategy
if all_exports_collision_strategy is None
else all_exports_collision_strategy
)
field_type_collision_strategy = (
config.field_type_collision_strategy
if field_type_collision_strategy is None
else field_type_collision_strategy
)
module_split_mode = config.module_split_mode if module_split_mode is None else module_split_mode

remote_text_cache: DefaultPutDict[str, str] = DefaultPutDict()
match input_:
case str():
Expand Down
33 changes: 33 additions & 0 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,37 @@ def _filter_defs_by_strategy(
return {**schema, "$defs": new_defs}


def _try_rebuild_model(obj: type) -> None:
"""Try to rebuild a Pydantic model, handling config models specially."""
module = getattr(obj, "__module__", "")
class_name = getattr(obj, "__name__", "")
config_classes = {"GenerateConfig", "ParserConfig", "ParseConfig"}
if module in {"datamodel_code_generator.config", "config"} and class_name in config_classes:
from datamodel_code_generator.model.base import DataModel, DataModelFieldBase # noqa: PLC0415
from datamodel_code_generator.types import DataTypeManager, StrictTypes # noqa: PLC0415

try:
from datamodel_code_generator.model.pydantic_v2 import UnionMode # noqa: PLC0415
except ImportError: # pragma: no cover
from typing import Any # noqa: PLC0415

runtime_union_mode = Any
else:
runtime_union_mode = UnionMode

types_namespace = {
"Path": Path,
"DataModel": DataModel,
"DataModelFieldBase": DataModelFieldBase,
"DataTypeManager": DataTypeManager,
"StrictTypes": StrictTypes,
"UnionMode": runtime_union_mode,
}
obj.model_rebuild(_types_namespace=types_namespace)
else:
obj.model_rebuild()


def _load_model_schema( # noqa: PLR0912, PLR0914, PLR0915
input_model: str,
input_file_type: InputFileType,
Expand Down Expand Up @@ -1199,6 +1230,8 @@ def _load_model_schema( # noqa: PLR0912, PLR0914, PLR0915
if not hasattr(obj, "model_json_schema"):
msg = "--input-model with Pydantic model requires Pydantic v2 runtime. Please upgrade Pydantic to v2."
raise Error(msg)
if hasattr(obj, "model_rebuild"): # pragma: no branch
_try_rebuild_model(obj)
schema_generator = _get_input_model_json_schema_class()
schema = obj.model_json_schema(schema_generator=schema_generator)
schema = _add_python_type_for_unserializable(schema, obj)
Expand Down
Loading
Loading