Skip to content

Commit 29a6f5b

Browse files
authored
feat: Add --enum-field-as-literal none option and respect user settings for TypedDict (#2691)
* feat: Add 'none' option for enum_field_as_literal and update related logic * test: Skip tests for TypedDict enum_field_as_literal on incompatible black version
1 parent 38b953f commit 29a6f5b

10 files changed

Lines changed: 156 additions & 10 deletions

File tree

src/datamodel_code_generator/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -660,9 +660,9 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
660660
reuse_model=reuse_model,
661661
reuse_scope=reuse_scope,
662662
shared_module_name=shared_module_name,
663-
enum_field_as_literal=LiteralType.All
664-
if output_model_type == DataModelType.TypingTypedDict
665-
else enum_field_as_literal,
663+
enum_field_as_literal=enum_field_as_literal
664+
if enum_field_as_literal is not None
665+
else (LiteralType.All if output_model_type == DataModelType.TypingTypedDict else None),
666666
use_one_literal_as_default=use_one_literal_as_default,
667667
use_enum_values_in_discriminator=use_enum_values_in_discriminator,
668668
set_default_enum_member=True

src/datamodel_code_generator/arguments.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ def start_section(self, heading: str | None) -> None:
343343
"--enum-field-as-literal",
344344
help="Parse enum field as literal. "
345345
"all: all enum field type are Literal. "
346-
"one: field type is Literal when an enum has only one possible value",
346+
"one: field type is Literal when an enum has only one possible value. "
347+
"none: always use Enum class (never convert to Literal)",
347348
choices=[lt.value for lt in LiteralType],
348349
default=None,
349350
)

src/datamodel_code_generator/parser/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class LiteralType(Enum):
1919

2020
All = "all"
2121
One = "one"
22+
Off = "none"
2223

2324

2425
class DefaultPutDict(UserDict[TK, TV]):

src/datamodel_code_generator/parser/graphql.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,11 @@ def parse_scalar(self, scalar_graphql_object: graphql.GraphQLScalarType) -> None
406406

407407
def should_parse_enum_as_literal(self, obj: graphql.GraphQLEnumType) -> bool:
408408
"""Determine if an enum should be parsed as a literal type."""
409-
return self.enum_field_as_literal == LiteralType.All or (
410-
self.enum_field_as_literal == LiteralType.One and len(obj.values) == 1
411-
)
409+
if self.enum_field_as_literal == LiteralType.All:
410+
return True
411+
if self.enum_field_as_literal == LiteralType.One:
412+
return len(obj.values) == 1
413+
return False
412414

413415
def parse_enum(self, enum_object: graphql.GraphQLEnumType) -> None:
414416
"""Parse a GraphQL enum type and add it to results."""

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -767,9 +767,11 @@ def root_id(self, value: str | None) -> None:
767767

768768
def should_parse_enum_as_literal(self, obj: JsonSchemaObject) -> bool:
769769
"""Determine if an enum should be parsed as a literal type."""
770-
return self.enum_field_as_literal == LiteralType.All or (
771-
self.enum_field_as_literal == LiteralType.One and len(obj.enum) == 1
772-
)
770+
if self.enum_field_as_literal == LiteralType.All:
771+
return True
772+
if self.enum_field_as_literal == LiteralType.One:
773+
return len(obj.enum) == 1
774+
return False
773775

774776
@classmethod
775777
def _extract_const_enum_from_combined( # noqa: PLR0912
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# generated by datamodel-codegen:
2+
# filename: enum_literal_typed_dict.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal, NotRequired, TypedDict
8+
9+
10+
class EnumLiteralTypedDict(TypedDict):
11+
status: NotRequired[Literal['active', 'inactive', 'pending']]
12+
priority: NotRequired[Literal['high']]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# generated by datamodel-codegen:
2+
# filename: enum_literal_typed_dict.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import StrEnum
8+
from typing import NotRequired, TypedDict
9+
10+
11+
class Status(StrEnum):
12+
active = 'active'
13+
inactive = 'inactive'
14+
pending = 'pending'
15+
16+
17+
class Priority(StrEnum):
18+
high = 'high'
19+
20+
21+
class EnumLiteralTypedDict(TypedDict):
22+
status: NotRequired[Status]
23+
priority: NotRequired[Priority]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# generated by datamodel-codegen:
2+
# filename: enum_literal_typed_dict.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import StrEnum
8+
from typing import Literal, NotRequired, TypedDict
9+
10+
11+
class Status(StrEnum):
12+
active = 'active'
13+
inactive = 'inactive'
14+
pending = 'pending'
15+
16+
17+
class EnumLiteralTypedDict(TypedDict):
18+
status: NotRequired[Status]
19+
priority: NotRequired[Literal['high']]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "EnumLiteralTypedDict",
4+
"type": "object",
5+
"properties": {
6+
"status": {
7+
"title": "Status",
8+
"type": "string",
9+
"enum": ["active", "inactive", "pending"]
10+
},
11+
"priority": {
12+
"title": "Priority",
13+
"type": "string",
14+
"enum": ["high"]
15+
}
16+
}
17+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2841,6 +2841,75 @@ def test_main_typed_dict_additional_properties(output_file: Path) -> None:
28412841
)
28422842

28432843

2844+
@pytest.mark.skipif(
2845+
black.__version__.split(".")[0] == "22",
2846+
reason="Installed black doesn't support Python version 3.11",
2847+
)
2848+
def test_main_typed_dict_enum_field_as_literal_none(output_file: Path) -> None:
2849+
"""Test TypedDict with enum_field_as_literal=none."""
2850+
run_main_and_assert(
2851+
input_path=JSON_SCHEMA_DATA_PATH / "enum_literal_typed_dict.json",
2852+
output_path=output_file,
2853+
input_file_type=None,
2854+
assert_func=assert_file_content,
2855+
expected_file="typed_dict_enum_literal_none.py",
2856+
extra_args=[
2857+
"--output-model-type",
2858+
"typing.TypedDict",
2859+
"--enum-field-as-literal",
2860+
"none",
2861+
"--target-python-version",
2862+
"3.11",
2863+
],
2864+
)
2865+
2866+
2867+
@pytest.mark.skipif(
2868+
black.__version__.split(".")[0] == "22",
2869+
reason="Installed black doesn't support Python version 3.11",
2870+
)
2871+
def test_main_typed_dict_enum_field_as_literal_one(output_file: Path) -> None:
2872+
"""Test TypedDict with enum_field_as_literal=one."""
2873+
run_main_and_assert(
2874+
input_path=JSON_SCHEMA_DATA_PATH / "enum_literal_typed_dict.json",
2875+
output_path=output_file,
2876+
input_file_type=None,
2877+
assert_func=assert_file_content,
2878+
expected_file="typed_dict_enum_literal_one.py",
2879+
extra_args=[
2880+
"--output-model-type",
2881+
"typing.TypedDict",
2882+
"--enum-field-as-literal",
2883+
"one",
2884+
"--target-python-version",
2885+
"3.11",
2886+
],
2887+
)
2888+
2889+
2890+
@pytest.mark.skipif(
2891+
black.__version__.split(".")[0] == "22",
2892+
reason="Installed black doesn't support Python version 3.11",
2893+
)
2894+
def test_main_typed_dict_enum_field_as_literal_all(output_file: Path) -> None:
2895+
"""Test TypedDict with enum_field_as_literal=all."""
2896+
run_main_and_assert(
2897+
input_path=JSON_SCHEMA_DATA_PATH / "enum_literal_typed_dict.json",
2898+
output_path=output_file,
2899+
input_file_type=None,
2900+
assert_func=assert_file_content,
2901+
expected_file="typed_dict_enum_literal_all.py",
2902+
extra_args=[
2903+
"--output-model-type",
2904+
"typing.TypedDict",
2905+
"--enum-field-as-literal",
2906+
"all",
2907+
"--target-python-version",
2908+
"3.11",
2909+
],
2910+
)
2911+
2912+
28442913
def test_main_dataclass_const(output_file: Path) -> None:
28452914
"""Test main function writing to dataclass with const fields."""
28462915
run_main_and_assert(

0 commit comments

Comments
 (0)