diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index babc261df..9569f8f43 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -3938,7 +3938,7 @@ def _reset_local_source_cache(self) -> None: self._cache_local_sources = False self._local_source_cache = None - def parse( # noqa: PLR0913, PLR0914, PLR0917 + def parse( # noqa: PLR0912, PLR0913, PLR0914, PLR0917 self, with_import: bool | None = True, # noqa: FBT001, FBT002 format_: bool | None = True, # noqa: FBT001, FBT002 @@ -3980,12 +3980,20 @@ def parse( # noqa: PLR0913, PLR0914, PLR0917 ) = self._build_module_structure(sorted_data_models, require_update_action_models, module_split_mode) if format_: - config = config._replace( - code_formatter=self._build_code_formatter( - settings_path, - is_multi_module_output=self.defer_formatting or len(module_models) > 1, - ) - ) + match self.formatters: + case [] if ( + not self.custom_formatter + and "_build_code_formatter" not in self.__dict__ + and type(self)._build_code_formatter is Parser._build_code_formatter # noqa: SLF001 + ): + pass + case _: + config = config._replace( + code_formatter=self._build_code_formatter( + settings_path, + is_multi_module_output=self.defer_formatting or len(module_models) > 1, + ), + ) results: dict[ModulePath, Result] = {} unused_models: list[DataModel] = [] diff --git a/tests/data/expected/main/cli_fast_paths/empty_formatters.txt b/tests/data/expected/main/cli_fast_paths/empty_formatters.txt new file mode 100644 index 000000000..72c43e16b --- /dev/null +++ b/tests/data/expected/main/cli_fast_paths/empty_formatters.txt @@ -0,0 +1,4 @@ +{ + "generated": "# generated by datamodel-codegen:\n# filename: person.json\n\nfrom __future__ import annotations\nfrom pydantic import BaseModel, Field, conint\nfrom typing import Any\n\n\nclass Person(BaseModel):\n firstName: str | None = Field(None, description=\"The person's first name.\")\n lastName: str | None = Field(None, description=\"The person's last name.\")\n age: conint(ge=0) | None = Field(None, description='Age in years which must be equal to or greater than zero.')\n friends: list[Any] | None = None\n comment: None = Field(None)", + "imported_format": false +} diff --git a/tests/data/expected/main/generate_with_custom_formatter_and_empty_formatters.py b/tests/data/expected/main/generate_with_custom_formatter_and_empty_formatters.py new file mode 100644 index 000000000..beb8758c7 --- /dev/null +++ b/tests/data/expected/main/generate_with_custom_formatter_and_empty_formatters.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: person.json + +# a comment +from __future__ import annotations +from pydantic import BaseModel, Field, conint +from typing import Any + + +class Person(BaseModel): + firstName: str | None = Field(None, description="The person's first name.") + lastName: str | None = Field(None, description="The person's last name.") + age: conint(ge=0) | None = Field(None, description='Age in years which must be equal to or greater than zero.') + friends: list[Any] | None = None + comment: None = Field(None) diff --git a/tests/data/expected/main/generate_with_empty_formatters.py b/tests/data/expected/main/generate_with_empty_formatters.py new file mode 100644 index 000000000..649e66eaf --- /dev/null +++ b/tests/data/expected/main/generate_with_empty_formatters.py @@ -0,0 +1,14 @@ +# generated by datamodel-codegen: +# filename: person.json + +from __future__ import annotations +from pydantic import BaseModel, Field, conint +from typing import Any + + +class Person(BaseModel): + firstName: str | None = Field(None, description="The person's first name.") + lastName: str | None = Field(None, description="The person's last name.") + age: conint(ge=0) | None = Field(None, description='Age in years which must be equal to or greater than zero.') + friends: list[Any] | None = None + comment: None = Field(None) diff --git a/tests/data/expected/main/parser_formatter_builder_override_with_empty_formatters.py b/tests/data/expected/main/parser_formatter_builder_override_with_empty_formatters.py new file mode 100644 index 000000000..50a5b5f01 --- /dev/null +++ b/tests/data/expected/main/parser_formatter_builder_override_with_empty_formatters.py @@ -0,0 +1,12 @@ +# a comment +from __future__ import annotations +from typing import Any, List, Optional +from pydantic import BaseModel, Field, conint + + +class Person(BaseModel): + firstName: Optional[str] = Field(None, description="The person's first name.") + lastName: Optional[str] = Field(None, description="The person's last name.") + age: Optional[conint(ge=0)] = Field(None, description='Age in years which must be equal to or greater than zero.') + friends: Optional[List[Any]] = None + comment: None = Field(None) diff --git a/tests/data/expected/main/parser_formatter_builder_override_with_empty_formatters_calls.txt b/tests/data/expected/main/parser_formatter_builder_override_with_empty_formatters_calls.txt new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/tests/data/expected/main/parser_formatter_builder_override_with_empty_formatters_calls.txt @@ -0,0 +1 @@ +1 diff --git a/tests/main/test_cli_fast_paths.py b/tests/main/test_cli_fast_paths.py index f46455566..d3f9b8614 100644 --- a/tests/main/test_cli_fast_paths.py +++ b/tests/main/test_cli_fast_paths.py @@ -16,6 +16,8 @@ import pytest +from tests.conftest import assert_output + ROOT = Path(__file__).parents[2] SRC = ROOT / "src" MISSING = object() @@ -170,6 +172,31 @@ def _run_main_import_probe() -> dict[str, Any]: ) +def _run_no_formatter_generation_probe() -> dict[str, Any]: + return _run_probe( + textwrap.dedent( + """ + import json + import sys + from pathlib import Path + + from datamodel_code_generator import InputFileType, generate + + generated = generate( + Path("tests/data/jsonschema/person.json"), + input_file_type=InputFileType.JsonSchema, + disable_timestamp=True, + formatters=[], + ) + print(json.dumps({ + "generated": generated, + "imported_format": "datamodel_code_generator.format" in sys.modules, + }, indent=2, sort_keys=True)) + """ + ) + ) + + def _run_invalid_args_probe() -> dict[str, Any]: return _run_probe( textwrap.dedent( @@ -371,6 +398,16 @@ def test_main_import_skips_formatter_runtime() -> None: assert imported["imported_validators"] is False +def test_empty_formatters_skip_formatter_runtime() -> None: + """Explicit empty formatters keep the formatter runtime out of a fresh process.""" + result = _run_no_formatter_generation_probe() + + assert_output( + f"{json.dumps(result, indent=2, sort_keys=True)}\n", + ROOT / "tests/data/expected/main/cli_fast_paths/empty_formatters.txt", + ) + + @pytest.mark.allow_direct_assert def test_invalid_args_skip_pydantic_import() -> None: """Argparse errors exit before loading CLI Config or Pydantic.""" diff --git a/tests/main/test_main_general.py b/tests/main/test_main_general.py index 7c8c449f7..c2842840c 100644 --- a/tests/main/test_main_general.py +++ b/tests/main/test_main_general.py @@ -55,6 +55,7 @@ assert_generated_modules_output, assert_httpx_get_kwargs, assert_no_uncommented_generated_code, + assert_output, assert_runtime_import_package, assert_warnings_contain, assert_warnings_do_not_contain, @@ -2944,6 +2945,108 @@ def test_generate_accepts_path_input(output_file: Path) -> None: ) +@pytest.mark.parametrize("custom_formatters", [None, []], ids=["custom-unset", "custom-empty"]) +def test_generate_with_empty_formatters(output_file: Path, custom_formatters: list[str] | None) -> None: + """Skip formatter work when the explicit formatter list is empty.""" + run_generate_file_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "person.json", + output_path=output_file, + input_file_type=InputFileType.JsonSchema, + disable_timestamp=True, + formatters=[], + custom_formatters=custom_formatters, + assert_func=assert_file_content, + expected_file="generate_with_empty_formatters.py", + ) + + +def test_generate_with_custom_formatter_and_empty_formatters(output_file: Path) -> None: + """Keep custom formatting when the built-in formatter list is empty.""" + run_generate_file_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "person.json", + output_path=output_file, + input_file_type=InputFileType.JsonSchema, + disable_timestamp=True, + formatters=[], + custom_formatters=["tests.data.python.custom_formatters.add_comment"], + assert_func=assert_file_content, + expected_file="generate_with_custom_formatter_and_empty_formatters.py", + ) + + +def test_parser_formatter_builder_override_with_empty_formatters() -> None: + """Keep subclass formatter-builder hooks active with an empty formatter list.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + from tests.data.python.custom_formatters.add_comment import CodeFormatter as AddCommentFormatter + + class ConfiguringJsonSchemaParser(JsonSchemaParser): + code_formatter_build_count = 0 + + def _build_code_formatter( + self, + settings_path: Path | None, + *, + is_multi_module_output: bool, + ) -> CodeFormatter: + code_formatter = super()._build_code_formatter( + settings_path, + is_multi_module_output=is_multi_module_output, + ) + code_formatter.custom_formatters.append(AddCommentFormatter(formatter_kwargs={})) + self.code_formatter_build_count += 1 + return code_formatter + + parser = ConfiguringJsonSchemaParser( + source=(JSON_SCHEMA_DATA_PATH / "person.json").resolve(), + formatters=[], + ) + assert_output( + f"{parser.parse()}\n", + EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters.py", + ) + assert_output( + f"{parser.code_formatter_build_count}\n", + EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters_calls.txt", + ) + + +def test_parser_instance_formatter_builder_with_empty_formatters() -> None: + """Keep an instance-injected formatter builder active with an empty formatter list.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + from tests.data.python.custom_formatters.add_comment import CodeFormatter as AddCommentFormatter + + parser = JsonSchemaParser( + source=(JSON_SCHEMA_DATA_PATH / "person.json").resolve(), + formatters=[], + ) + default_builder = parser._build_code_formatter + code_formatter_build_count = 0 + + def build_code_formatter( + settings_path: Path | None, + *, + is_multi_module_output: bool, + ) -> CodeFormatter: + nonlocal code_formatter_build_count + code_formatter = default_builder( + settings_path, + is_multi_module_output=is_multi_module_output, + ) + code_formatter.custom_formatters.append(AddCommentFormatter(formatter_kwargs={})) + code_formatter_build_count += 1 + return code_formatter + + parser._build_code_formatter = build_code_formatter # type: ignore[method-assign] + assert_output( + f"{parser.parse()}\n", + EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters.py", + ) + assert_output( + f"{code_formatter_build_count}\n", + EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters_calls.txt", + ) + + def test_generate_keeps_existing_path_string_input() -> None: """Test generate() keeps existing path strings as inline source text.""" run_generate_and_assert(