From 4485283f740a4220268fbf382fa7356f91eb14e6 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 18 Dec 2025 15:38:59 +0000 Subject: [PATCH 1/7] feat: Add module split mode for generating separate files per model class --- src/datamodel_code_generator/__init__.py | 12 ++++ src/datamodel_code_generator/__main__.py | 3 + src/datamodel_code_generator/arguments.py | 7 +++ src/datamodel_code_generator/cli_options.py | 1 + src/datamodel_code_generator/parser/base.py | 58 +++++++++++++------ .../module_split_single/__init__.py | 14 +++++ .../jsonschema/module_split_single/model.py | 12 ++++ .../jsonschema/module_split_single/order.py | 15 +++++ .../jsonschema/module_split_single/user.py | 13 +++++ .../jsonschema/module_split_single/input.json | 19 ++++++ tests/main/test_main_general.py | 23 ++++++++ 11 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 tests/data/expected/main/jsonschema/module_split_single/__init__.py create mode 100644 tests/data/expected/main/jsonschema/module_split_single/model.py create mode 100644 tests/data/expected/main/jsonschema/module_split_single/order.py create mode 100644 tests/data/expected/main/jsonschema/module_split_single/user.py create mode 100644 tests/data/jsonschema/module_split_single/input.json diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index 15a9a9b3e..4225538b6 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -300,6 +300,15 @@ class ReadOnlyWriteOnlyModelType(Enum): All = "all" +class ModuleSplitMode(Enum): + """Mode for splitting generated models into separate files. + + Single: Generate one file per model class. + """ + + Single = "single" + + class Error(Exception): """Base exception for datamodel-code-generator errors.""" @@ -468,6 +477,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915 read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None, all_exports_scope: AllExportsScope | None = None, all_exports_collision_strategy: AllExportsCollisionStrategy | None = None, + module_split_mode: ModuleSplitMode | None = None, ) -> None: """Generate Python data models from schema definitions or structured data. @@ -716,6 +726,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]: disable_future_imports=disable_future_imports, all_exports_scope=all_exports_scope, all_exports_collision_strategy=all_exports_collision_strategy, + module_split_mode=module_split_mode, ) if not input_filename: # pragma: no cover if isinstance(input_, str): @@ -852,6 +863,7 @@ def infer_input_type(text: str) -> InputFileType: "InputFileType", "InvalidClassNameError", "LiteralType", + "ModuleSplitMode", "PythonVersion", "ReadOnlyWriteOnlyModelType", "generate", diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index 79068d97c..93fd66e88 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -30,6 +30,7 @@ Error, InputFileType, InvalidClassNameError, + ModuleSplitMode, OpenAPIScope, ReadOnlyWriteOnlyModelType, ReuseScope, @@ -460,6 +461,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict read_only_write_only_model_type: Optional[ReadOnlyWriteOnlyModelType] = None # noqa: UP045 all_exports_scope: Optional[AllExportsScope] = None # noqa: UP045 all_exports_collision_strategy: Optional[AllExportsCollisionStrategy] = None # noqa: UP045 + module_split_mode: Optional[ModuleSplitMode] = None # noqa: UP045 def merge_args(self, args: Namespace) -> None: """Merge command-line arguments into config.""" @@ -909,6 +911,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912, read_only_write_only_model_type=config.read_only_write_only_model_type, all_exports_scope=config.all_exports_scope, all_exports_collision_strategy=config.all_exports_collision_strategy, + module_split_mode=config.module_split_mode, ) except InvalidClassNameError as e: print(f"{e} You have to set `--class-name` option", file=sys.stderr) # noqa: T201 diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py index 696153047..47438f7bb 100644 --- a/src/datamodel_code_generator/arguments.py +++ b/src/datamodel_code_generator/arguments.py @@ -22,6 +22,7 @@ DataclassArguments, DataModelType, InputFileType, + ModuleSplitMode, OpenAPIScope, ReadOnlyWriteOnlyModelType, ReuseScope, @@ -317,6 +318,12 @@ def start_section(self, heading: str | None) -> None: choices=[s.value for s in AllExportsCollisionStrategy], default=None, ) +model_options.add_argument( + "--module-split-mode", + help="Split generated models into separate files. 'single': generate one file per model class.", + choices=[m.value for m in ModuleSplitMode], + default=None, +) # ====================================================================================== # Typing options for generated models diff --git a/src/datamodel_code_generator/cli_options.py b/src/datamodel_code_generator/cli_options.py index 503b55d16..1830fc955 100644 --- a/src/datamodel_code_generator/cli_options.py +++ b/src/datamodel_code_generator/cli_options.py @@ -205,6 +205,7 @@ class CLIOptionMeta: "--all-exports-collision-strategy": CLIOptionMeta( name="--all-exports-collision-strategy", category=OptionCategory.GENERAL ), + "--module-split-mode": CLIOptionMeta(name="--module-split-mode", category=OptionCategory.GENERAL), "--disable-warnings": CLIOptionMeta(name="--disable-warnings", category=OptionCategory.GENERAL), } diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index ecf3d9b38..2664260fb 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -29,6 +29,7 @@ AllExportsScope, AllOfMergeMode, Error, + ModuleSplitMode, ReadOnlyWriteOnlyModelType, ReuseScope, ) @@ -66,7 +67,7 @@ from datamodel_code_generator.parser import DefaultPutDict, LiteralType 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.reference import ModelResolver, ModelType, Reference, camel_to_snake from datamodel_code_generator.types import DataType, DataTypeManager, StrictTypes if TYPE_CHECKING: @@ -1069,43 +1070,53 @@ def __change_from_import( *, init: bool, internal_modules: set[tuple[str, ...]] | None = None, + model_path_to_module_name: dict[str, str] | None = None, ) -> None: model_paths = {model.path for model in models} internal_modules = internal_modules or set() + model_path_to_module_name = model_path_to_module_name or {} for model in models: scoped_model_resolver.add([model.path], model.class_name) for model in models: before_import = model.imports imports.append(before_import) + current_module_name = model_path_to_module_name.get(model.path, model.module_name) for data_type in model.all_data_types: - # To change from/import - if not data_type.reference or data_type.reference.path in model_paths: - # No need to import non-reference model. - # Or, Referenced model is in the same file. we don't need to import the model continue + ref_module_name = model_path_to_module_name.get( + data_type.reference.path, + data_type.full_name.rsplit(".", 1)[0] if "." in data_type.full_name else "", + ) + target_full_name = ( + f"{ref_module_name}.{data_type.reference.short_name}" + if ref_module_name + else data_type.reference.short_name + ) + if isinstance(data_type, BaseClassDataType): - left, right = relative(model.module_name, data_type.full_name) - is_ancestor = is_ancestor_package_reference(model.module_name, data_type.full_name) + left, right = relative(current_module_name, target_full_name) + is_ancestor = is_ancestor_package_reference(current_module_name, target_full_name) from_ = left if is_ancestor else (f"{left}{right}" if left.endswith(".") else f"{left}.{right}") import_ = data_type.reference.short_name full_path = from_, import_ else: - from_, import_ = full_path = relative(model.module_name, data_type.full_name) - if imports.use_exact: # pragma: no cover + from_, import_ = full_path = relative(current_module_name, target_full_name) + if imports.use_exact: from_, import_ = exact_import(from_, import_, data_type.reference.short_name) import_ = import_.replace("-", "_") - if ( # pragma: no cover - len(model.module_path) > 1 - and model.module_path[-1].count(".") > 0 + current_module_path = tuple(current_module_name.split(".")) if current_module_name else () + if ( + len(current_module_path) > 1 + and current_module_path[-1].count(".") > 0 and not self.treat_dot_as_module ): - rel_path_depth = model.module_path[-1].count(".") + rel_path_depth = current_module_path[-1].count(".") from_ = from_[rel_path_depth:] - ref_module = tuple(data_type.full_name.split(".")[:-1]) + ref_module = tuple(target_full_name.split(".")[:-1]) is_module_class_collision = ( ref_module and import_ == data_type.reference.short_name and ref_module[-1] == import_ @@ -1122,7 +1133,7 @@ def __change_from_import( if from_ and import_ and alias != name: data_type.alias = alias if data_type.reference.short_name == import_ else f"{alias}.{name}" - if init and not data_type.full_name.startswith(model.module_name + "."): + if init and not target_full_name.startswith(current_module_name + "."): from_ = "." + from_ imports.append( Import( @@ -2404,6 +2415,7 @@ def parse( # noqa: PLR0912, PLR0913, PLR0914, PLR0915, PLR0917 disable_future_imports: bool = False, # noqa: FBT001, FBT002 all_exports_scope: AllExportsScope | None = None, all_exports_collision_strategy: AllExportsCollisionStrategy | None = None, + module_split_mode: ModuleSplitMode | None = None, ) -> str | dict[tuple[str, ...], Result]: """Parse schema and generate code, returning single file or module dict.""" self.parse_raw() @@ -2439,10 +2451,14 @@ def parse( # noqa: PLR0912, PLR0913, PLR0914, PLR0915, PLR0917 results: dict[tuple[str, ...], Result] = {} def module_key(data_model: DataModel) -> tuple[str, ...]: + if module_split_mode == ModuleSplitMode.Single: + file_name = camel_to_snake(data_model.class_name) + return (*data_model.module_path, file_name) return tuple(data_model.module_path) def sort_key(data_model: DataModel) -> tuple[int, tuple[str, ...]]: - return (len(data_model.module_path), tuple(data_model.module_path)) + key = module_key(data_model) + return (len(key), key) # process in reverse order to correctly establish module levels grouped_models = groupby( @@ -2454,11 +2470,14 @@ def sort_key(data_model: DataModel) -> tuple[int, tuple[str, ...]]: unused_models: list[DataModel] = [] model_to_module_models: dict[DataModel, tuple[tuple[str, ...], list[DataModel]]] = {} module_to_import: dict[tuple[str, ...], Imports] = {} + model_path_to_module_name: dict[str, str] = {} previous_module: tuple[str, ...] = () for module, models in ((k, [*v]) for k, v in grouped_models): for model in models: model_to_module_models[model] = module, models + if module_split_mode == ModuleSplitMode.Single: + model_path_to_module_name[model.path] = ".".join(module) self.__delete_duplicate_models(models) self.__replace_duplicate_name_in_module(models) if len(previous_module) - len(module) > 1: @@ -2524,7 +2543,12 @@ class Processed(NamedTuple): self.__override_required_field(models) self.__replace_unique_list_to_set(models) self.__change_from_import( - models, imports, scoped_model_resolver, init=init, internal_modules=internal_modules + models, + imports, + scoped_model_resolver, + init=init, + internal_modules=internal_modules, + model_path_to_module_name=model_path_to_module_name, ) self.__extract_inherited_enum(models) self.__set_reference_default_value_to_field(models) diff --git a/tests/data/expected/main/jsonschema/module_split_single/__init__.py b/tests/data/expected/main/jsonschema/module_split_single/__init__.py new file mode 100644 index 000000000..8e24fcbf8 --- /dev/null +++ b/tests/data/expected/main/jsonschema/module_split_single/__init__.py @@ -0,0 +1,14 @@ +# generated by datamodel-codegen: +# filename: input.json + +from __future__ import annotations + +from .model import Model +from .order import Order +from .user import User + +__all__ = [ + "Model", + "Order", + "User", +] diff --git a/tests/data/expected/main/jsonschema/module_split_single/model.py b/tests/data/expected/main/jsonschema/module_split_single/model.py new file mode 100644 index 000000000..2763c9910 --- /dev/null +++ b/tests/data/expected/main/jsonschema/module_split_single/model.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: input.json + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel + + +class Model(BaseModel): + __root__: Any diff --git a/tests/data/expected/main/jsonschema/module_split_single/order.py b/tests/data/expected/main/jsonschema/module_split_single/order.py new file mode 100644 index 000000000..71235e6ac --- /dev/null +++ b/tests/data/expected/main/jsonschema/module_split_single/order.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: input.json + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel + +from . import user as user_1 + + +class Order(BaseModel): + id: Optional[int] = None + user: Optional[user_1.User] = None diff --git a/tests/data/expected/main/jsonschema/module_split_single/user.py b/tests/data/expected/main/jsonschema/module_split_single/user.py new file mode 100644 index 000000000..069afc65a --- /dev/null +++ b/tests/data/expected/main/jsonschema/module_split_single/user.py @@ -0,0 +1,13 @@ +# generated by datamodel-codegen: +# filename: input.json + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel + + +class User(BaseModel): + id: Optional[int] = None + name: Optional[str] = None diff --git a/tests/data/jsonschema/module_split_single/input.json b/tests/data/jsonschema/module_split_single/input.json new file mode 100644 index 000000000..653718448 --- /dev/null +++ b/tests/data/jsonschema/module_split_single/input.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "User": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"} + } + }, + "Order": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "user": {"$ref": "#/definitions/User"} + } + } + } +} diff --git a/tests/main/test_main_general.py b/tests/main/test_main_general.py index ea9c38f98..81edd6088 100644 --- a/tests/main/test_main_general.py +++ b/tests/main/test_main_general.py @@ -1027,3 +1027,26 @@ def test_use_specialized_enum_pyproject_override_with_cli(output_file: Path, tmp assert_func=assert_file_content, expected_file="no_use_specialized_enum.py", ) + + +@pytest.mark.cli_doc( + options=["--module-split-mode"], + input_schema="jsonschema/module_split_single/input.json", + cli_args=["--module-split-mode", "single", "--all-exports-scope", "recursive"], + golden_output="jsonschema/module_split_single", + related_options=["--all-exports-scope"], +) +def test_module_split_mode_single(output_dir: Path) -> None: + """Split generated models into separate files, one per model class. + + The `--module-split-mode=single` flag generates each model class in its own file, + named after the class in snake_case. Use with `--all-exports-scope=recursive` to + create an __init__.py that re-exports all models for convenient imports. + """ + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "module_split_single" / "input.json", + output_path=output_dir, + input_file_type="jsonschema", + extra_args=["--disable-timestamp", "--module-split-mode", "single", "--all-exports-scope", "recursive"], + expected_directory=EXPECTED_MAIN_PATH / "jsonschema" / "module_split_single", + ) From dd6ff12f868dca629db4b1fef3afe1edb1100938 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 15:39:51 +0000 Subject: [PATCH 2/7] docs: update CLI reference documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated by GitHub Actions --- docs/cli-reference/general-options.md | 111 ++++++++++++++++++++++++++ docs/cli-reference/index.md | 8 +- docs/cli-reference/quick-reference.md | 2 + 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/docs/cli-reference/general-options.md b/docs/cli-reference/general-options.md index 8668eb3f2..a58e8b1d7 100644 --- a/docs/cli-reference/general-options.md +++ b/docs/cli-reference/general-options.md @@ -14,6 +14,7 @@ | [`--http-ignore-tls`](#http-ignore-tls) | Disable TLS certificate verification for HTTPS requests. | | [`--http-query-parameters`](#http-query-parameters) | Add query parameters to HTTP requests for remote schemas. | | [`--ignore-pyproject`](#ignore-pyproject) | Ignore pyproject.toml configuration file. | +| [`--module-split-mode`](#module-split-mode) | Split generated models into separate files, one per model cl... | | [`--shared-module-name`](#shared-module-name) | Customize the name of the shared module for deduplicated mod... | --- @@ -1670,6 +1671,116 @@ testing without project configuration. --- +## `--module-split-mode` {#module-split-mode} + +Split generated models into separate files, one per model class. + +The `--module-split-mode=single` flag generates each model class in its own file, +named after the class in snake_case. Use with `--all-exports-scope=recursive` to +create an __init__.py that re-exports all models for convenient imports. + +**Related:** [`--all-exports-scope`](general-options.md#all-exports-scope) + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --module-split-mode single --all-exports-scope recursive # (1)! + ``` + + 1. :material-arrow-left: `--module-split-mode` - the option documented here + +??? example "Input Schema" + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "User": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"} + } + }, + "Order": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "user": {"$ref": "#/definitions/User"} + } + } + } + } + ``` + +??? example "Output" + + ```python + # __init__.py + # generated by datamodel-codegen: + # filename: input.json + + from __future__ import annotations + + from .model import Model + from .order import Order + from .user import User + + __all__ = [ + "Model", + "Order", + "User", + ] + + # model.py + # generated by datamodel-codegen: + # filename: input.json + + from __future__ import annotations + + from typing import Any + + from pydantic import BaseModel + + + class Model(BaseModel): + __root__: Any + + # order.py + # generated by datamodel-codegen: + # filename: input.json + + from __future__ import annotations + + from typing import Optional + + from pydantic import BaseModel + + from . import user as user_1 + + + class Order(BaseModel): + id: Optional[int] = None + user: Optional[user_1.User] = None + + # user.py + # generated by datamodel-codegen: + # filename: input.json + + from __future__ import annotations + + from typing import Optional + + from pydantic import BaseModel + + + class User(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + ``` + +--- + ## `--shared-module-name` {#shared-module-name} Customize the name of the shared module for deduplicated models. diff --git a/docs/cli-reference/index.md b/docs/cli-reference/index.md index 87bee0599..21289106d 100644 --- a/docs/cli-reference/index.md +++ b/docs/cli-reference/index.md @@ -14,12 +14,12 @@ This documentation is auto-generated from test cases. | ๐Ÿ—๏ธ [Model Customization](model-customization.md) | 26 | Model generation behavior | | ๐ŸŽจ [Template Customization](template-customization.md) | 15 | Output formatting and custom rendering | | ๐Ÿ“˜ [OpenAPI-only Options](openapi-only-options.md) | 5 | OpenAPI-specific features | -| โš™๏ธ [General Options](general-options.md) | 11 | Utilities and meta options | +| โš™๏ธ [General Options](general-options.md) | 12 | Utilities and meta options | | ๐Ÿ“ [Utility Options](utility-options.md) | 5 | Help, version, debug options | ## All Options -**Jump to:** [A](#a) ยท [B](#b) ยท [C](#c) ยท [D](#d) ยท [E](#e) ยท [F](#f) ยท [G](#g) ยท [H](#h) ยท [I](#i) ยท [K](#k) ยท [N](#n) ยท [O](#o) ยท [P](#p) ยท [R](#r) ยท [S](#s) ยท [T](#t) ยท [U](#u) ยท [V](#v) ยท [W](#w) +**Jump to:** [A](#a) ยท [B](#b) ยท [C](#c) ยท [D](#d) ยท [E](#e) ยท [F](#f) ยท [G](#g) ยท [H](#h) ยท [I](#i) ยท [K](#k) ยท [M](#m) ยท [N](#n) ยท [O](#o) ยท [P](#p) ยท [R](#r) ยท [S](#s) ยท [T](#t) ยท [U](#u) ยท [V](#v) ยท [W](#w) ### A {#a} @@ -101,6 +101,10 @@ This documentation is auto-generated from test cases. - [`--keep-model-order`](model-customization.md#keep-model-order) - [`--keyword-only`](model-customization.md#keyword-only) +### M {#m} + +- [`--module-split-mode`](general-options.md#module-split-mode) + ### N {#n} - [`--no-alias`](field-customization.md#no-alias) diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index 8cbe59359..965e577fa 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -143,6 +143,7 @@ datamodel-codegen [OPTIONS] | [`--http-ignore-tls`](general-options.md#http-ignore-tls) | Disable TLS certificate verification for HTTPS requests. | | [`--http-query-parameters`](general-options.md#http-query-parameters) | Add query parameters to HTTP requests for remote schemas. | | [`--ignore-pyproject`](general-options.md#ignore-pyproject) | Ignore pyproject.toml configuration file. | +| [`--module-split-mode`](general-options.md#module-split-mode) | Split generated models into separate files, one per model class. | | [`--shared-module-name`](general-options.md#shared-module-name) | Customize the name of the shared module for deduplicated models. | ### ๐Ÿ“ Utility Options @@ -210,6 +211,7 @@ All options sorted alphabetically: - [`--input-file-type`](base-options.md#input-file-type) - Specify the input file type for code generation. - [`--keep-model-order`](model-customization.md#keep-model-order) - Keep model definition order as specified in schema. - [`--keyword-only`](model-customization.md#keyword-only) - Generate dataclasses with keyword-only fields (Python 3.10+)... +- [`--module-split-mode`](general-options.md#module-split-mode) - Split generated models into separate files, one per model cl... - [`--no-alias`](field-customization.md#no-alias) - Disable Field alias generation for non-Python-safe property ... - [`--no-color`](utility-options.md#no-color) - Disable colorized output - [`--no-use-specialized-enum`](typing-customization.md#no-use-specialized-enum) - Disable specialized Enum classes for Python 3.11+ code gener... From 9cc606b9a0f92e3f94814eb1d559915a42e27606 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 18 Dec 2025 15:44:04 +0000 Subject: [PATCH 3/7] refactor: Update __change_from_import method signature for clarity --- src/datamodel_code_generator/parser/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 2664260fb..ddca3c97a 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -1062,7 +1062,7 @@ def __replace_duplicate_name_in_module(cls, models: list[DataModel]) -> None: model.class_name = duplicate_name model_names[duplicate_name] = model - def __change_from_import( + def __change_from_import( # noqa: PLR0913, PLR0914 self, models: list[DataModel], imports: Imports, From df192e514520156a2f191793354eaab7fd288932 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 18 Dec 2025 16:00:04 +0000 Subject: [PATCH 4/7] refactor: Improve readability of conditional statement in base.py --- src/datamodel_code_generator/parser/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index ddca3c97a..f8cb4262c 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -1108,7 +1108,7 @@ def __change_from_import( # noqa: PLR0913, PLR0914 from_, import_ = exact_import(from_, import_, data_type.reference.short_name) import_ = import_.replace("-", "_") current_module_path = tuple(current_module_name.split(".")) if current_module_name else () - if ( + if ( # pragma: no cover len(current_module_path) > 1 and current_module_path[-1].count(".") > 0 and not self.treat_dot_as_module From 456dab08852e35d63e9a0004e2634c07bcafd9bc Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 18 Dec 2025 16:06:15 +0000 Subject: [PATCH 5/7] refactor: Move camel_to_snake to util.py to fix cyclic import --- src/datamodel_code_generator/parser/base.py | 3 ++- src/datamodel_code_generator/reference.py | 13 +------------ src/datamodel_code_generator/util.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index f8cb4262c..5ba2791bb 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -67,8 +67,9 @@ from datamodel_code_generator.parser import DefaultPutDict, LiteralType 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, camel_to_snake +from datamodel_code_generator.reference import ModelResolver, ModelType, Reference from datamodel_code_generator.types import DataType, DataTypeManager, StrictTypes +from datamodel_code_generator.util import camel_to_snake if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping, Sequence diff --git a/src/datamodel_code_generator/reference.py b/src/datamodel_code_generator/reference.py index 63c696969..06e002395 100644 --- a/src/datamodel_code_generator/reference.py +++ b/src/datamodel_code_generator/reference.py @@ -36,7 +36,7 @@ from typing_extensions import TypeIs from datamodel_code_generator import Error -from datamodel_code_generator.util import PYDANTIC_V2, ConfigDict, model_validator +from datamodel_code_generator.util import PYDANTIC_V2, ConfigDict, camel_to_snake, model_validator if TYPE_CHECKING: from collections.abc import Generator, Iterator, Mapping, Sequence @@ -221,17 +221,6 @@ def context_variable(setter: Callable[[T], None], current_value: T, new_value: T setter(previous_value) -_UNDER_SCORE_1: Pattern[str] = re.compile(r"([^_])([A-Z][a-z]+)") -_UNDER_SCORE_2: Pattern[str] = re.compile(r"([a-z0-9])([A-Z])") - - -@lru_cache -def camel_to_snake(string: str) -> str: - """Convert camelCase or PascalCase to snake_case.""" - subbed = _UNDER_SCORE_1.sub(r"\1_\2", string) - return _UNDER_SCORE_2.sub(r"\1_\2", subbed).lower() - - class FieldNameResolver: """Converts schema field names to valid Python identifiers.""" diff --git a/src/datamodel_code_generator/util.py b/src/datamodel_code_generator/util.py index 2b231c656..ada27bcae 100644 --- a/src/datamodel_code_generator/util.py +++ b/src/datamodel_code_generator/util.py @@ -7,6 +7,9 @@ from __future__ import annotations import copy +import re +from functools import lru_cache +from re import Pattern from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload import pydantic @@ -140,3 +143,14 @@ class BaseModel(_BaseModel): if PYDANTIC_V2: model_config = ConfigDict(strict=False) # pyright: ignore[reportAssignmentType] + + +_UNDER_SCORE_1: Pattern[str] = re.compile(r"([^_])([A-Z][a-z]+)") +_UNDER_SCORE_2: Pattern[str] = re.compile(r"([a-z0-9])([A-Z])") + + +@lru_cache +def camel_to_snake(string: str) -> str: + """Convert camelCase or PascalCase to snake_case.""" + subbed = _UNDER_SCORE_1.sub(r"\1_\2", string) + return _UNDER_SCORE_2.sub(r"\1_\2", subbed).lower() From 3ae6c7cd6efcf7c5732d120237e7392183702671 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 18 Dec 2025 16:16:08 +0000 Subject: [PATCH 6/7] refactor: Update imports in order.py and test_main_general.py for clarity and consistency --- src/datamodel_code_generator/util.py | 5 ++--- .../main/jsonschema/module_split_single/order.py | 4 ++-- tests/main/test_main_general.py | 13 ++++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/datamodel_code_generator/util.py b/src/datamodel_code_generator/util.py index ada27bcae..c0129b159 100644 --- a/src/datamodel_code_generator/util.py +++ b/src/datamodel_code_generator/util.py @@ -9,7 +9,6 @@ import copy import re from functools import lru_cache -from re import Pattern from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload import pydantic @@ -145,8 +144,8 @@ class BaseModel(_BaseModel): model_config = ConfigDict(strict=False) # pyright: ignore[reportAssignmentType] -_UNDER_SCORE_1: Pattern[str] = re.compile(r"([^_])([A-Z][a-z]+)") -_UNDER_SCORE_2: Pattern[str] = re.compile(r"([a-z0-9])([A-Z])") +_UNDER_SCORE_1: re.Pattern[str] = re.compile(r"([^_])([A-Z][a-z]+)") +_UNDER_SCORE_2: re.Pattern[str] = re.compile(r"([a-z0-9])([A-Z])") @lru_cache diff --git a/tests/data/expected/main/jsonschema/module_split_single/order.py b/tests/data/expected/main/jsonschema/module_split_single/order.py index 71235e6ac..c958b40ac 100644 --- a/tests/data/expected/main/jsonschema/module_split_single/order.py +++ b/tests/data/expected/main/jsonschema/module_split_single/order.py @@ -7,9 +7,9 @@ from pydantic import BaseModel -from . import user as user_1 +from .user import User class Order(BaseModel): id: Optional[int] = None - user: Optional[user_1.User] = None + user: Optional[User] = None diff --git a/tests/main/test_main_general.py b/tests/main/test_main_general.py index 81edd6088..d8035e23d 100644 --- a/tests/main/test_main_general.py +++ b/tests/main/test_main_general.py @@ -1032,9 +1032,9 @@ def test_use_specialized_enum_pyproject_override_with_cli(output_file: Path, tmp @pytest.mark.cli_doc( options=["--module-split-mode"], input_schema="jsonschema/module_split_single/input.json", - cli_args=["--module-split-mode", "single", "--all-exports-scope", "recursive"], + cli_args=["--module-split-mode", "single", "--all-exports-scope", "recursive", "--use-exact-imports"], golden_output="jsonschema/module_split_single", - related_options=["--all-exports-scope"], + related_options=["--all-exports-scope", "--use-exact-imports"], ) def test_module_split_mode_single(output_dir: Path) -> None: """Split generated models into separate files, one per model class. @@ -1047,6 +1047,13 @@ def test_module_split_mode_single(output_dir: Path) -> None: input_path=JSON_SCHEMA_DATA_PATH / "module_split_single" / "input.json", output_path=output_dir, input_file_type="jsonschema", - extra_args=["--disable-timestamp", "--module-split-mode", "single", "--all-exports-scope", "recursive"], + extra_args=[ + "--disable-timestamp", + "--module-split-mode", + "single", + "--all-exports-scope", + "recursive", + "--use-exact-imports", + ], expected_directory=EXPECTED_MAIN_PATH / "jsonschema" / "module_split_single", ) From abedc4dcd9591d61469a566c304100a81cd9b128 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 16:16:33 +0000 Subject: [PATCH 7/7] docs: update CLI reference documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated by GitHub Actions --- docs/cli-reference/general-options.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cli-reference/general-options.md b/docs/cli-reference/general-options.md index a58e8b1d7..2bfa33682 100644 --- a/docs/cli-reference/general-options.md +++ b/docs/cli-reference/general-options.md @@ -1679,12 +1679,12 @@ The `--module-split-mode=single` flag generates each model class in its own file named after the class in snake_case. Use with `--all-exports-scope=recursive` to create an __init__.py that re-exports all models for convenient imports. -**Related:** [`--all-exports-scope`](general-options.md#all-exports-scope) +**Related:** [`--all-exports-scope`](general-options.md#all-exports-scope), [`--use-exact-imports`](template-customization.md#use-exact-imports) !!! tip "Usage" ```bash - datamodel-codegen --input schema.json --module-split-mode single --all-exports-scope recursive # (1)! + datamodel-codegen --input schema.json --module-split-mode single --all-exports-scope recursive --use-exact-imports # (1)! ``` 1. :material-arrow-left: `--module-split-mode` - the option documented here @@ -1756,12 +1756,12 @@ create an __init__.py that re-exports all models for convenient imports. from pydantic import BaseModel - from . import user as user_1 + from .user import User class Order(BaseModel): id: Optional[int] = None - user: Optional[user_1.User] = None + user: Optional[User] = None # user.py # generated by datamodel-codegen: