diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 6368a9379..ce83fad6d 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -126,9 +126,22 @@ def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any return value return int(value) - def _get_default_as_pydantic_model(self) -> str | None: + def _get_default_as_pydantic_model(self) -> str | None: # noqa: PLR0911, PLR0912 if isinstance(self.default, WrappedDefault): return f"lambda :{self.default!r}" + if self.data_type.is_list and len(self.data_type.data_types) == 1: + data_type_child = self.data_type.data_types[0] + if ( + data_type_child.reference + and isinstance(data_type_child.reference.source, BaseModelBase) + and isinstance(self.default, list) + ): + if not self.default: + return STANDARD_LIST + return ( # pragma: no cover + f"lambda :[{data_type_child.alias or data_type_child.reference.source.class_name}." + f"{self._PARSE_METHOD}(v) for v in {self.default!r}]" + ) for data_type in self.data_type.data_types or (self.data_type,): # TODO: Check nested data_types if data_type.is_dict: @@ -220,7 +233,7 @@ def __str__(self) -> str: # noqa: PLR0912 elif isinstance(discriminator, dict): # pragma: no cover data["discriminator"] = discriminator["propertyName"] - if self.required: + if self.required and not self.has_default: default_factory = None elif self.default is not UNDEFINED and self.default is not None and "default_factory" not in data: default_factory = self._get_default_as_pydantic_model() @@ -249,7 +262,7 @@ def __str__(self) -> str: # noqa: PLR0912 if self.use_annotated: field_arguments = self._process_annotated_field_arguments(field_arguments) - elif self.required: + elif self.required and not default_factory: field_arguments = ["...", *field_arguments] elif not default_factory: default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default) diff --git a/tests/data/expected/main/graphql/empty_list_default.py b/tests/data/expected/main/graphql/empty_list_default.py new file mode 100644 index 000000000..8def8a752 --- /dev/null +++ b/tests/data/expected/main/graphql/empty_list_default.py @@ -0,0 +1,34 @@ +# generated by datamodel-codegen: +# filename: empty_list_default.graphql +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field + +type Boolean = bool +""" +The `Boolean` scalar type represents `true` or `false`. +""" + + +type String = str +""" +The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. +""" + + +class Container(BaseModel): + name: String + typename__: Literal['Container'] | None = Field('Container', alias='__typename') + + +class PodSpec(BaseModel): + container_list: list[Container] = Field(default_factory=list) + container_list_or_none: list[Container | None] = Field(default_factory=list) + container_or_none_list_or_none: list[Container | None] | None = Field( + default_factory=list + ) + typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') diff --git a/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py new file mode 100644 index 000000000..8def8a752 --- /dev/null +++ b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py @@ -0,0 +1,34 @@ +# generated by datamodel-codegen: +# filename: empty_list_default.graphql +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field + +type Boolean = bool +""" +The `Boolean` scalar type represents `true` or `false`. +""" + + +type String = str +""" +The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. +""" + + +class Container(BaseModel): + name: String + typename__: Literal['Container'] | None = Field('Container', alias='__typename') + + +class PodSpec(BaseModel): + container_list: list[Container] = Field(default_factory=list) + container_list_or_none: list[Container | None] = Field(default_factory=list) + container_or_none_list_or_none: list[Container | None] | None = Field( + default_factory=list + ) + typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') diff --git a/tests/data/expected/main/openapi/empty_list_default.py b/tests/data/expected/main/openapi/empty_list_default.py index 34ef0c865..4678aeb8d 100644 --- a/tests/data/expected/main/openapi/empty_list_default.py +++ b/tests/data/expected/main/openapi/empty_list_default.py @@ -4,14 +4,12 @@ from __future__ import annotations -from typing import List, Optional - from pydantic import BaseModel, Field class Container(BaseModel): - name: Optional[str] = None + name: str | None = None class PodSpec(BaseModel): - containers: Optional[List[Container]] = Field(default_factory=list) + containers: list[Container] | None = Field(default_factory=list) diff --git a/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py b/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py index 34ef0c865..4678aeb8d 100644 --- a/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py +++ b/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py @@ -4,14 +4,12 @@ from __future__ import annotations -from typing import List, Optional - from pydantic import BaseModel, Field class Container(BaseModel): - name: Optional[str] = None + name: str | None = None class PodSpec(BaseModel): - containers: Optional[List[Container]] = Field(default_factory=list) + containers: list[Container] | None = Field(default_factory=list) diff --git a/tests/data/graphql/empty_list_default.graphql b/tests/data/graphql/empty_list_default.graphql new file mode 100644 index 000000000..c561d3dc4 --- /dev/null +++ b/tests/data/graphql/empty_list_default.graphql @@ -0,0 +1,10 @@ + +input Container { + name: String! +} + +input PodSpec { + container_list: [Container!]! = [] + container_list_or_none: [Container]! = [] + container_or_none_list_or_none: [Container] = [] +} diff --git a/tests/main/graphql/test_main_graphql.py b/tests/main/graphql/test_main_graphql.py index 14b2ae52a..61fdc7846 100644 --- a/tests/main/graphql/test_main_graphql.py +++ b/tests/main/graphql/test_main_graphql.py @@ -7,7 +7,13 @@ import black import pytest -from tests.main.conftest import DEFAULT_VALUES_DATA_PATH, GRAPHQL_DATA_PATH, LEGACY_BLACK_SKIP, run_main_and_assert +from tests.main.conftest import ( + DEFAULT_VALUES_DATA_PATH, + EXPECTED_GRAPHQL_PATH, + GRAPHQL_DATA_PATH, + LEGACY_BLACK_SKIP, + run_main_and_assert, +) from tests.main.graphql.conftest import assert_file_content if TYPE_CHECKING: @@ -104,6 +110,40 @@ def test_main_use_default_kwarg(output_file: Path) -> None: ) +@pytest.mark.parametrize( + ("output_model", "expected_output"), + [ + ( + "pydantic.BaseModel", + "empty_list_default.py", + ), + ( + "pydantic_v2.BaseModel", + "pydantic_v2_empty_list_default.py", + ), + ], +) +@pytest.mark.skipif( + black.__version__.split(".")[0] in {"19", "22"}, + reason="Installed black doesn't support Python 3.12 target version", +) +def test_main_graphql_empty_list_default(output_model: str, expected_output: str, output_file: Path) -> None: + """Test GraphQL generation with empty list default values.""" + run_main_and_assert( + input_path=GRAPHQL_DATA_PATH / "empty_list_default.graphql", + output_path=output_file, + assert_func=assert_file_content, + expected_file=EXPECTED_GRAPHQL_PATH / expected_output, + input_file_type="graphql", + extra_args=[ + "--output-model-type", + output_model, + "--target-python-version", + "3.12", + ], + ) + + @pytest.mark.skipif( black.__version__.split(".")[0] == "19", reason="Installed black doesn't support the old style", diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 495c2309b..0c8decff1 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -2808,6 +2808,7 @@ def test_main_openapi_empty_list_default(output_model: str, expected_output: str input_path=OPEN_API_DATA_PATH / "empty_list_default.yaml", output_path=output_file, expected_file=EXPECTED_OPENAPI_PATH / expected_output, + assert_func=assert_file_content, input_file_type="openapi", extra_args=[ "--output-model-type",