Skip to content

Commit 7e8b04e

Browse files
authored
Skip unused formatter construction (#3599)
* Skip unused formatter construction * Preserve formatter builder fallback
1 parent 01303c3 commit 7e8b04e

8 files changed

Lines changed: 201 additions & 7 deletions

src/datamodel_code_generator/parser/base.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3938,7 +3938,7 @@ def _reset_local_source_cache(self) -> None:
39383938
self._cache_local_sources = False
39393939
self._local_source_cache = None
39403940

3941-
def parse( # noqa: PLR0913, PLR0914, PLR0917
3941+
def parse( # noqa: PLR0912, PLR0913, PLR0914, PLR0917
39423942
self,
39433943
with_import: bool | None = True, # noqa: FBT001, FBT002
39443944
format_: bool | None = True, # noqa: FBT001, FBT002
@@ -3980,12 +3980,20 @@ def parse( # noqa: PLR0913, PLR0914, PLR0917
39803980
) = self._build_module_structure(sorted_data_models, require_update_action_models, module_split_mode)
39813981

39823982
if format_:
3983-
config = config._replace(
3984-
code_formatter=self._build_code_formatter(
3985-
settings_path,
3986-
is_multi_module_output=self.defer_formatting or len(module_models) > 1,
3987-
)
3988-
)
3983+
match self.formatters:
3984+
case [] if (
3985+
not self.custom_formatter
3986+
and "_build_code_formatter" not in self.__dict__
3987+
and type(self)._build_code_formatter is Parser._build_code_formatter # noqa: SLF001
3988+
):
3989+
pass
3990+
case _:
3991+
config = config._replace(
3992+
code_formatter=self._build_code_formatter(
3993+
settings_path,
3994+
is_multi_module_output=self.defer_formatting or len(module_models) > 1,
3995+
),
3996+
)
39893997

39903998
results: dict[ModulePath, Result] = {}
39913999
unused_models: list[DataModel] = []
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"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)",
3+
"imported_format": false
4+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# generated by datamodel-codegen:
2+
# filename: person.json
3+
4+
# a comment
5+
from __future__ import annotations
6+
from pydantic import BaseModel, Field, conint
7+
from typing import Any
8+
9+
10+
class Person(BaseModel):
11+
firstName: str | None = Field(None, description="The person's first name.")
12+
lastName: str | None = Field(None, description="The person's last name.")
13+
age: conint(ge=0) | None = Field(None, description='Age in years which must be equal to or greater than zero.')
14+
friends: list[Any] | None = None
15+
comment: None = Field(None)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# generated by datamodel-codegen:
2+
# filename: person.json
3+
4+
from __future__ import annotations
5+
from pydantic import BaseModel, Field, conint
6+
from typing import Any
7+
8+
9+
class Person(BaseModel):
10+
firstName: str | None = Field(None, description="The person's first name.")
11+
lastName: str | None = Field(None, description="The person's last name.")
12+
age: conint(ge=0) | None = Field(None, description='Age in years which must be equal to or greater than zero.')
13+
friends: list[Any] | None = None
14+
comment: None = Field(None)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# a comment
2+
from __future__ import annotations
3+
from typing import Any, List, Optional
4+
from pydantic import BaseModel, Field, conint
5+
6+
7+
class Person(BaseModel):
8+
firstName: Optional[str] = Field(None, description="The person's first name.")
9+
lastName: Optional[str] = Field(None, description="The person's last name.")
10+
age: Optional[conint(ge=0)] = Field(None, description='Age in years which must be equal to or greater than zero.')
11+
friends: Optional[List[Any]] = None
12+
comment: None = Field(None)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1

tests/main/test_cli_fast_paths.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import pytest
1818

19+
from tests.conftest import assert_output
20+
1921
ROOT = Path(__file__).parents[2]
2022
SRC = ROOT / "src"
2123
MISSING = object()
@@ -170,6 +172,31 @@ def _run_main_import_probe() -> dict[str, Any]:
170172
)
171173

172174

175+
def _run_no_formatter_generation_probe() -> dict[str, Any]:
176+
return _run_probe(
177+
textwrap.dedent(
178+
"""
179+
import json
180+
import sys
181+
from pathlib import Path
182+
183+
from datamodel_code_generator import InputFileType, generate
184+
185+
generated = generate(
186+
Path("tests/data/jsonschema/person.json"),
187+
input_file_type=InputFileType.JsonSchema,
188+
disable_timestamp=True,
189+
formatters=[],
190+
)
191+
print(json.dumps({
192+
"generated": generated,
193+
"imported_format": "datamodel_code_generator.format" in sys.modules,
194+
}, indent=2, sort_keys=True))
195+
"""
196+
)
197+
)
198+
199+
173200
def _run_invalid_args_probe() -> dict[str, Any]:
174201
return _run_probe(
175202
textwrap.dedent(
@@ -371,6 +398,16 @@ def test_main_import_skips_formatter_runtime() -> None:
371398
assert imported["imported_validators"] is False
372399

373400

401+
def test_empty_formatters_skip_formatter_runtime() -> None:
402+
"""Explicit empty formatters keep the formatter runtime out of a fresh process."""
403+
result = _run_no_formatter_generation_probe()
404+
405+
assert_output(
406+
f"{json.dumps(result, indent=2, sort_keys=True)}\n",
407+
ROOT / "tests/data/expected/main/cli_fast_paths/empty_formatters.txt",
408+
)
409+
410+
374411
@pytest.mark.allow_direct_assert
375412
def test_invalid_args_skip_pydantic_import() -> None:
376413
"""Argparse errors exit before loading CLI Config or Pydantic."""

tests/main/test_main_general.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
assert_generated_modules_output,
5656
assert_httpx_get_kwargs,
5757
assert_no_uncommented_generated_code,
58+
assert_output,
5859
assert_runtime_import_package,
5960
assert_warnings_contain,
6061
assert_warnings_do_not_contain,
@@ -2944,6 +2945,108 @@ def test_generate_accepts_path_input(output_file: Path) -> None:
29442945
)
29452946

29462947

2948+
@pytest.mark.parametrize("custom_formatters", [None, []], ids=["custom-unset", "custom-empty"])
2949+
def test_generate_with_empty_formatters(output_file: Path, custom_formatters: list[str] | None) -> None:
2950+
"""Skip formatter work when the explicit formatter list is empty."""
2951+
run_generate_file_and_assert(
2952+
input_path=JSON_SCHEMA_DATA_PATH / "person.json",
2953+
output_path=output_file,
2954+
input_file_type=InputFileType.JsonSchema,
2955+
disable_timestamp=True,
2956+
formatters=[],
2957+
custom_formatters=custom_formatters,
2958+
assert_func=assert_file_content,
2959+
expected_file="generate_with_empty_formatters.py",
2960+
)
2961+
2962+
2963+
def test_generate_with_custom_formatter_and_empty_formatters(output_file: Path) -> None:
2964+
"""Keep custom formatting when the built-in formatter list is empty."""
2965+
run_generate_file_and_assert(
2966+
input_path=JSON_SCHEMA_DATA_PATH / "person.json",
2967+
output_path=output_file,
2968+
input_file_type=InputFileType.JsonSchema,
2969+
disable_timestamp=True,
2970+
formatters=[],
2971+
custom_formatters=["tests.data.python.custom_formatters.add_comment"],
2972+
assert_func=assert_file_content,
2973+
expected_file="generate_with_custom_formatter_and_empty_formatters.py",
2974+
)
2975+
2976+
2977+
def test_parser_formatter_builder_override_with_empty_formatters() -> None:
2978+
"""Keep subclass formatter-builder hooks active with an empty formatter list."""
2979+
from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
2980+
from tests.data.python.custom_formatters.add_comment import CodeFormatter as AddCommentFormatter
2981+
2982+
class ConfiguringJsonSchemaParser(JsonSchemaParser):
2983+
code_formatter_build_count = 0
2984+
2985+
def _build_code_formatter(
2986+
self,
2987+
settings_path: Path | None,
2988+
*,
2989+
is_multi_module_output: bool,
2990+
) -> CodeFormatter:
2991+
code_formatter = super()._build_code_formatter(
2992+
settings_path,
2993+
is_multi_module_output=is_multi_module_output,
2994+
)
2995+
code_formatter.custom_formatters.append(AddCommentFormatter(formatter_kwargs={}))
2996+
self.code_formatter_build_count += 1
2997+
return code_formatter
2998+
2999+
parser = ConfiguringJsonSchemaParser(
3000+
source=(JSON_SCHEMA_DATA_PATH / "person.json").resolve(),
3001+
formatters=[],
3002+
)
3003+
assert_output(
3004+
f"{parser.parse()}\n",
3005+
EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters.py",
3006+
)
3007+
assert_output(
3008+
f"{parser.code_formatter_build_count}\n",
3009+
EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters_calls.txt",
3010+
)
3011+
3012+
3013+
def test_parser_instance_formatter_builder_with_empty_formatters() -> None:
3014+
"""Keep an instance-injected formatter builder active with an empty formatter list."""
3015+
from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
3016+
from tests.data.python.custom_formatters.add_comment import CodeFormatter as AddCommentFormatter
3017+
3018+
parser = JsonSchemaParser(
3019+
source=(JSON_SCHEMA_DATA_PATH / "person.json").resolve(),
3020+
formatters=[],
3021+
)
3022+
default_builder = parser._build_code_formatter
3023+
code_formatter_build_count = 0
3024+
3025+
def build_code_formatter(
3026+
settings_path: Path | None,
3027+
*,
3028+
is_multi_module_output: bool,
3029+
) -> CodeFormatter:
3030+
nonlocal code_formatter_build_count
3031+
code_formatter = default_builder(
3032+
settings_path,
3033+
is_multi_module_output=is_multi_module_output,
3034+
)
3035+
code_formatter.custom_formatters.append(AddCommentFormatter(formatter_kwargs={}))
3036+
code_formatter_build_count += 1
3037+
return code_formatter
3038+
3039+
parser._build_code_formatter = build_code_formatter # type: ignore[method-assign]
3040+
assert_output(
3041+
f"{parser.parse()}\n",
3042+
EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters.py",
3043+
)
3044+
assert_output(
3045+
f"{code_formatter_build_count}\n",
3046+
EXPECTED_MAIN_PATH / "parser_formatter_builder_override_with_empty_formatters_calls.txt",
3047+
)
3048+
3049+
29473050
def test_generate_keeps_existing_path_string_input() -> None:
29483051
"""Test generate() keeps existing path strings as inline source text."""
29493052
run_generate_and_assert(

0 commit comments

Comments
 (0)