Skip to content

Commit b0ff0ec

Browse files
committed
Add test for --no-use-union-operator and fix coverage
1 parent ef628ac commit b0ff0ec

8 files changed

Lines changed: 136 additions & 21 deletions

File tree

src/datamodel_code_generator/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ def validate_keyword_only(cls, values: dict[str, Any]) -> dict[str, Any]: # noq
449449
and output_model_type == DataModelType.DataclassesDataclass
450450
and not python_target.has_kw_only_dataclass
451451
):
452-
raise Error(cls.__validate_keyword_only_err)
452+
raise Error(cls.__validate_keyword_only_err) # pragma: no cover
453453
return values
454454

455455
@model_validator() # pyright: ignore[reportArgumentType]
@@ -717,7 +717,7 @@ def _get_pyproject_toml_config(source: Path, profile: str | None = None) -> dict
717717
pyproject_config["capitalise_enum_members"] = pyproject_config.pop("capitalize_enum_members")
718718
return pyproject_config
719719

720-
if (current_path / ".git").exists():
720+
if (current_path / ".git").exists(): # pragma: no cover
721721
break
722722

723723
current_path = current_path.parent

src/datamodel_code_generator/format.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,6 @@ def has_kw_only_dataclass(self) -> bool:
119119
"""Check if Python version supports kw_only in dataclasses."""
120120
return self._is_py_310_or_later
121121

122-
@property
123-
def has_type_alias(self) -> bool:
124-
"""Check if Python version supports TypeAlias."""
125-
return self._is_py_310_or_later
126-
127122
@property
128123
def has_type_statement(self) -> bool:
129124
"""Check if Python version supports type statements."""

src/datamodel_code_generator/model/msgspec.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
IMPORT_DATETIME,
1717
IMPORT_TIME,
1818
IMPORT_TIMEDELTA,
19+
IMPORT_UNION,
1920
Import,
2021
)
2122
from datamodel_code_generator.model import DataModel, DataModelFieldBase
@@ -36,7 +37,10 @@
3637
from datamodel_code_generator.model.types import standard_primitive_type_map_factory, type_map_factory
3738
from datamodel_code_generator.types import (
3839
NONE,
40+
OPTIONAL_PREFIX,
41+
UNION_DELIMITER,
3942
UNION_OPERATOR_DELIMITER,
43+
UNION_PREFIX,
4044
DataType,
4145
StrictTypes,
4246
Types,
@@ -92,6 +96,8 @@ def new_imports(self: DataModelFieldBaseT) -> tuple[Import, ...]:
9296
extra_imports.append(IMPORT_MSGSPEC_META)
9397
if not self.required and not self.nullable:
9498
extra_imports.append(IMPORT_MSGSPEC_UNSETTYPE)
99+
if not self.data_type.use_union_operator:
100+
extra_imports.append(IMPORT_UNION)
95101
if self.default is None or self.default is UNDEFINED:
96102
extra_imports.append(IMPORT_MSGSPEC_UNSET)
97103
return chain_as_tuple(original_imports.fget(self), extra_imports) # pyright: ignore[reportOptionalCall]
@@ -212,18 +218,32 @@ class Constraints(_Constraints):
212218

213219

214220
@lru_cache
215-
def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) -> str: # noqa: ARG001, FBT001
221+
def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT001
216222
"""Get type hint for fields that are neither required nor nullable, using UnsetType."""
217-
type_ = _remove_none_from_union(type_, use_union_operator=True)
223+
type_ = _remove_none_from_union(type_, use_union_operator=use_union_operator)
224+
if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover
225+
type_ = type_[len(OPTIONAL_PREFIX) : -1]
226+
218227
if not type_ or type_ == NONE:
219228
return UNSET_TYPE
220-
return UNION_OPERATOR_DELIMITER.join((type_, UNSET_TYPE))
229+
if use_union_operator:
230+
return UNION_OPERATOR_DELIMITER.join((type_, UNSET_TYPE))
231+
if type_.startswith(UNION_PREFIX):
232+
return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]"
233+
return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]"
221234

222235

223236
@lru_cache
224-
def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: ARG001, FBT001
237+
def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT001
225238
"""Add UnsetType to a type hint without removing None."""
226-
return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}"
239+
if use_union_operator:
240+
return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}"
241+
if type_.startswith(UNION_PREFIX):
242+
return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]"
243+
if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover
244+
inner_type = type_[len(OPTIONAL_PREFIX) : -1]
245+
return f"{UNION_PREFIX}{inner_type}{UNION_DELIMITER}{NONE}{UNION_DELIMITER}{UNSET_TYPE}]"
246+
return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]"
227247

228248

229249
@import_extender
@@ -385,9 +405,9 @@ def annotated(self) -> str | None: # noqa: PLR0911
385405
For ClassVar fields (discriminator tag_field), ClassVar is required
386406
regardless of use_annotated setting.
387407
"""
388-
if self.extras.get("is_classvar"):
408+
if self.extras.get("is_classvar"): # pragma: no cover
389409
meta = self._get_meta_string()
390-
if self.use_annotated and meta: # pragma: no cover
410+
if self.use_annotated and meta:
391411
return f"ClassVar[Annotated[{self.type_hint}, {meta}]]"
392412
return f"ClassVar[{self.type_hint}]"
393413

@@ -418,7 +438,7 @@ def needs_annotated_import(self) -> bool:
418438
"""
419439
if not self.annotated:
420440
return False
421-
if self.extras.get("is_classvar"): # pragma: no cover
441+
if self.extras.get("is_classvar"):
422442
return self.use_annotated and self._get_meta_string() is not None
423443
return True
424444

src/datamodel_code_generator/prompt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def _format_options_by_category() -> str:
6666
for opt, desc in sorted(options):
6767
if desc:
6868
lines.append(f"- `{opt}`: {desc}")
69-
else:
69+
else: # pragma: no cover
7070
lines.append(f"- `{opt}`")
7171
lines.append("")
7272

src/datamodel_code_generator/reference.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def __init__(self, **values: Any) -> None:
9696
if not TYPE_CHECKING: # pragma: no branch
9797
if is_pydantic_v2():
9898

99-
def dict( # noqa: PLR0913
99+
def dict( # noqa: PLR0913 # pragma: no cover
100100
self,
101101
*,
102102
include: AbstractSet[int | str] | Mapping[int | str, Any] | None = None,
@@ -767,7 +767,7 @@ def resolve_ref(self, path: Sequence[str] | str) -> str: # noqa: PLR0911, PLR09
767767

768768
if is_url(ref):
769769
file_part, path_part = ref.split("#", 1)
770-
if file_part == self.root_id:
770+
if file_part == self.root_id: # pragma: no cover
771771
return f"{'/'.join(self.current_root)}#{path_part}"
772772
target_url: ParseResult = urlparse(file_part)
773773
if not (self.root_id and self.current_base_path):

src/datamodel_code_generator/types.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def _remove_none_from_union(type_: str, *, use_union_operator: bool) -> str: #
344344
part = current_part.strip()
345345
if current_part and part != NONE:
346346
# only UNION_PREFIX might be nested but not union_operator
347-
if not use_union_operator and part.startswith(UNION_PREFIX):
347+
if not use_union_operator and part.startswith(UNION_PREFIX): # pragma: no cover
348348
part = _remove_none_from_union(part, use_union_operator=False)
349349
parts.append(part)
350350

@@ -647,7 +647,7 @@ def imports(self) -> Iterator[Import]:
647647
(self.is_list, IMPORT_ABC_SEQUENCE),
648648
(self.is_dict, IMPORT_ABC_MAPPING),
649649
)
650-
else:
650+
else: # pragma: no cover
651651
imports = (
652652
*imports,
653653
(self.is_list, IMPORT_SEQUENCE),
@@ -778,7 +778,7 @@ def type_hint(self) -> str: # noqa: PLR0912, PLR0915
778778
set_ = STANDARD_FROZEN_SET if self.use_standard_collections else FROZEN_SET
779779
elif self.use_standard_collections:
780780
set_ = STANDARD_SET
781-
else:
781+
else: # pragma: no cover
782782
set_ = SET
783783
type_ = f"{set_}[{type_}]" if type_ else set_
784784
elif self.is_sequence:
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# generated by datamodel-codegen:
2+
# filename: nullable.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Annotated, Optional, TypeAlias, Union
8+
9+
from msgspec import UNSET, Meta, Struct, UnsetType, field
10+
11+
12+
class Cursors(Struct):
13+
prev: str
14+
index: float
15+
next: Union[str, UnsetType] = 'last'
16+
tag: Union[str, UnsetType] = UNSET
17+
18+
19+
class TopLevel(Struct):
20+
cursors: Cursors
21+
22+
23+
class Info(Struct):
24+
name: str
25+
26+
27+
class User(Struct):
28+
info: Info
29+
30+
31+
class Api(Struct):
32+
apiKey: Union[
33+
Annotated[str, Meta(description='To be used as a dataset parameter value')],
34+
UnsetType,
35+
] = UNSET
36+
apiVersionNumber: Union[
37+
Annotated[str, Meta(description='To be used as a version parameter value')],
38+
UnsetType,
39+
] = UNSET
40+
apiUrl: Union[
41+
Annotated[str, Meta(description="The URL describing the dataset's fields")],
42+
UnsetType,
43+
] = UNSET
44+
apiDocumentationUrl: Union[
45+
Annotated[str, Meta(description='A URL to the API console for each API')],
46+
UnsetType,
47+
] = UNSET
48+
49+
50+
Apis: TypeAlias = Optional[list[Api]]
51+
52+
53+
class EmailItem(Struct):
54+
author: str
55+
address: Annotated[str, Meta(description='email address')]
56+
description: Union[str, UnsetType] = 'empty'
57+
tag: Union[str, UnsetType] = UNSET
58+
59+
60+
Email: TypeAlias = list[EmailItem]
61+
62+
63+
Id: TypeAlias = int
64+
65+
66+
Description: TypeAlias = Annotated[Optional[str], 'example']
67+
68+
69+
Name: TypeAlias = Optional[str]
70+
71+
72+
Tag: TypeAlias = str
73+
74+
75+
class Notes(Struct):
76+
comments: Union[list[str], UnsetType] = field(default_factory=list)
77+
78+
79+
class Options(Struct):
80+
comments: list[str]
81+
oneOfComments: list[Union[str, float]]

tests/main/openapi/test_main_openapi.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3519,6 +3519,25 @@ def test_main_openapi_msgspec_oneof_with_null_union_operator(output_file: Path)
35193519
)
35203520

35213521

3522+
@MSGSPEC_LEGACY_BLACK_SKIP
3523+
def test_main_openapi_msgspec_no_use_union_operator(output_file: Path) -> None:
3524+
"""Test msgspec Struct generation without union operator (Union[X, Y] syntax)."""
3525+
run_main_and_assert(
3526+
input_path=OPEN_API_DATA_PATH / "nullable.yaml",
3527+
output_path=output_file,
3528+
input_file_type="openapi",
3529+
assert_func=assert_file_content,
3530+
expected_file="msgspec_no_use_union_operator.py",
3531+
extra_args=[
3532+
"--output-model-type",
3533+
"msgspec.Struct",
3534+
"--no-use-union-operator",
3535+
"--target-python-version",
3536+
"3.10",
3537+
],
3538+
)
3539+
3540+
35223541
def test_main_openapi_referenced_default(output_file: Path) -> None:
35233542
"""Test OpenAPI generation with referenced default values."""
35243543
run_main_and_assert(

0 commit comments

Comments
 (0)