Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 15 additions & 7 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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] = []
Expand Down
4 changes: 4 additions & 0 deletions tests/data/expected/main/cli_fast_paths/empty_formatters.txt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions tests/data/expected/main/generate_with_empty_formatters.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
37 changes: 37 additions & 0 deletions tests/main/test_cli_fast_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import pytest

from tests.conftest import assert_output

ROOT = Path(__file__).parents[2]
SRC = ROOT / "src"
MISSING = object()
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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."""
Expand Down
103 changes: 103 additions & 0 deletions tests/main/test_main_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Loading