Skip to content

Commit 00cde07

Browse files
committed
Use semantic None default checks
1 parent 9745f5b commit 00cde07

8 files changed

Lines changed: 94 additions & 12 deletions

File tree

src/datamodel_code_generator/model/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,14 @@ def _process_const_as_literal(self) -> None:
315315
if self.default is None and not self.has_default:
316316
self.default = const
317317

318+
def should_strip_default_none(self, *, keep_optional: bool = False) -> bool:
319+
"""Return whether an actual None default should be omitted."""
320+
match (self.strip_default_none, keep_optional and self.data_type.is_optional):
321+
case (False, _) | (_, True):
322+
return False
323+
324+
return self.default is None
325+
318326
def self_reference(self) -> bool:
319327
"""Check if field references its parent model.
320328

src/datamodel_code_generator/model/dataclass.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@
3434
def has_field_assignment(field: DataModelFieldBase) -> bool:
3535
"""Check if a dataclass field renders with an assignment or default value."""
3636
return (bool(field.field) and not field.use_annotated) or not (
37-
(field.required and not field.use_default_with_required)
38-
or (field.represented_default == "None" and field.strip_default_none)
37+
(field.required and not field.use_default_with_required) or field.should_strip_default_none()
3938
)
4039

4140

src/datamodel_code_generator/model/msgspec.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ def __str__(self) -> str:
6060

6161
def has_field_assignment(field: DataModelFieldBase) -> bool:
6262
"""Return whether a msgspec field renders with a default assignment."""
63-
return field.use_default_with_required or not (
64-
field.required or (field.represented_default == "None" and field.strip_default_none)
65-
)
63+
return field.use_default_with_required or not (field.required or field.should_strip_default_none())
6664

6765

6866
DataModelFieldBaseT = TypeVar("DataModelFieldBaseT", bound=DataModelFieldBase)

src/datamodel_code_generator/model/pydantic_base.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,6 @@ def _process_data_in_str(self, data: dict[str, Any]) -> None: # pragma: no cove
235235
def _process_annotated_field_arguments(self, field_arguments: list[str]) -> list[str]: # noqa: PLR6301
236236
return field_arguments
237237

238-
def _should_strip_default_none_from_field(self) -> bool:
239-
if self.data_type.is_optional:
240-
return False
241-
242-
return self.default is None
243-
244238
def _get_field_data_and_default_factory(self) -> tuple[dict[str, Any], Any]:
245239
"""Build Field() keyword data and the effective default_factory."""
246240
data: dict[str, Any] = {k: v for k, v in self.extras.items() if k not in self._EXCLUDE_FIELD_KEYS}
@@ -315,7 +309,7 @@ def __str__(self) -> str:
315309
elif (
316310
not default_factory
317311
and (default_repr := represent_python_value(self.default))
318-
and (not self.strip_default_none or not self._should_strip_default_none_from_field())
312+
and not self.should_strip_default_none(keep_optional=True)
319313
):
320314
field_arguments = [default_repr, *field_arguments]
321315

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: strip_default_none_semantic.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass
8+
9+
10+
@dataclass
11+
class SemanticNoneModel:
12+
implicitNone: str | None
13+
stringNoneDefault: str | None = 'None'
14+
explicitDefault: str | None = 'active'
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: strip_default_none_semantic.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from msgspec import UNSET, Struct, UnsetType
8+
9+
10+
class SemanticNoneModel(Struct):
11+
implicitNone: str | UnsetType = UNSET
12+
stringNoneDefault: str | UnsetType = 'None'
13+
explicitDefault: str | UnsetType = 'active'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
openapi: 3.0.3
2+
info:
3+
title: strip default none semantic
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
SemanticNoneModel:
9+
type: object
10+
properties:
11+
implicitNone:
12+
type: string
13+
stringNoneDefault:
14+
type: string
15+
default: "None"
16+
explicitDefault:
17+
type: string
18+
default: active

tests/main/openapi/test_main_openapi.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,44 @@ def test_main_openapi_pydantic_v2_strip_default_none_field_metadata(output_file:
13191319
)
13201320

13211321

1322+
@pytest.mark.parametrize(
1323+
("output_model_type", "target_python_version", "expected_file"),
1324+
[
1325+
(
1326+
"dataclasses.dataclass",
1327+
"3.10",
1328+
"strip_default_none_semantic_dataclass.py",
1329+
),
1330+
(
1331+
"msgspec.Struct",
1332+
"3.10",
1333+
"strip_default_none_semantic_msgspec.py",
1334+
),
1335+
],
1336+
)
1337+
def test_main_openapi_strip_default_none_semantic_default(
1338+
output_model_type: str,
1339+
target_python_version: str,
1340+
expected_file: str,
1341+
output_file: Path,
1342+
) -> None:
1343+
"""Test strip-default-none treats only actual None defaults as None."""
1344+
run_main_and_assert(
1345+
input_path=OPEN_API_DATA_PATH / "strip_default_none_semantic.yaml",
1346+
output_path=output_file,
1347+
input_file_type="openapi",
1348+
assert_func=assert_file_content,
1349+
expected_file=expected_file,
1350+
extra_args=[
1351+
"--strip-default-none",
1352+
"--output-model-type",
1353+
output_model_type,
1354+
"--target-python-version",
1355+
target_python_version,
1356+
],
1357+
)
1358+
1359+
13221360
def test_disable_timestamp(output_file: Path) -> None:
13231361
"""Test OpenAPI generation with timestamp disabled."""
13241362
run_main_and_assert(

0 commit comments

Comments
 (0)