Skip to content

Commit 6ad4fe4

Browse files
authored
fix(types): generate schema enums as StrEnum (#915)
1 parent 9782830 commit 6ad4fe4

408 files changed

Lines changed: 3207 additions & 3050 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/post_generate_fixes.py

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
This script applies necessary modifications to generated files that cannot be
77
handled by datamodel-code-generator directly:
88
9-
1. Adds model_validators to types requiring mutual exclusivity checks
10-
2. Fixes self-referential RootModel type annotations
11-
3. Fixes BrandManifest forward references
12-
4. Adds deprecated=True to fields marked deprecated in JSON schema
13-
5. Unwraps specified RootModel unions to plain Union type aliases (#155)
14-
6. Widens canceled: Literal[True] = True on request types to | None = None (#641)
9+
1. Rewrites generated string enums to StrEnum
10+
2. Adds model_validators to types requiring mutual exclusivity checks
11+
3. Fixes self-referential RootModel type annotations
12+
4. Fixes BrandManifest forward references
13+
5. Adds deprecated=True to fields marked deprecated in JSON schema
14+
6. Unwraps specified RootModel unions to plain Union type aliases (#155)
15+
7. Widens canceled: Literal[True] = True on request types to | None = None (#641)
1516
"""
1617

1718
from __future__ import annotations
@@ -48,6 +49,8 @@ def _load_resolve_bundle_key():
4849

4950
_PROTOCOL_ENVELOPE_IMPORT = "from ..core.protocol_envelope import ProtocolEnvelope\n"
5051
_VERSION_ENVELOPE_IMPORT = "from ..core.version_envelope import AdcpVersionEnvelope\n"
52+
_STR_ENUM_MEMBER_ASSIGNMENT_IGNORE = " # type: ignore[assignment]"
53+
_STR_ATTRIBUTE_NAMES = set(dir(str))
5154

5255

5356
def _sync_protocol_envelope_import(source: str) -> str:
@@ -82,6 +85,90 @@ def add_model_validator_to_product():
8285
print(" product.py validation: no fixes needed (Pydantic handles discriminated unions)")
8386

8487

88+
def _ignore_strenum_member_method_collisions(source: str) -> tuple[str, int]:
89+
"""Suppress mypy for StrEnum members that intentionally shadow str methods."""
90+
lines = source.splitlines()
91+
updated: list[str] = []
92+
class_indent: int | None = None
93+
ignores_added = 0
94+
95+
for line in lines:
96+
stripped = line.lstrip()
97+
indent = len(line) - len(stripped)
98+
99+
class_match = re.match(r"class\s+\w+\(StrEnum\):", stripped)
100+
if class_match is not None:
101+
class_indent = indent
102+
updated.append(line)
103+
continue
104+
105+
if class_indent is not None and stripped and indent <= class_indent:
106+
class_indent = None
107+
108+
if class_indent is not None and indent == class_indent + 4:
109+
assignment_match = re.match(r"([A-Za-z_]\w*)\s*=", stripped)
110+
if (
111+
assignment_match is not None
112+
and assignment_match.group(1) in _STR_ATTRIBUTE_NAMES
113+
and _STR_ENUM_MEMBER_ASSIGNMENT_IGNORE not in line
114+
):
115+
line = f"{line}{_STR_ENUM_MEMBER_ASSIGNMENT_IGNORE}"
116+
ignores_added += 1
117+
118+
updated.append(line)
119+
120+
return "\n".join(updated) + ("\n" if source.endswith("\n") else ""), ignores_added
121+
122+
123+
def rewrite_generated_enums_to_strenum() -> None:
124+
"""Make all generated schema enums inherit from StrEnum.
125+
126+
datamodel-code-generator emits plain ``Enum`` classes for string-valued
127+
JSON Schema enums. The generated enum members should behave like their wire
128+
values for equality, hashing, formatting, and ``str()`` without widening or
129+
narrowing any model field annotations.
130+
"""
131+
files_changed = 0
132+
classes_changed = 0
133+
ignores_added = 0
134+
135+
for path in OUTPUT_DIR.rglob("*.py"):
136+
source = path.read_text()
137+
if (
138+
"(Enum):" not in source
139+
and "from enum import Enum" not in source
140+
and "(StrEnum):" not in source
141+
):
142+
continue
143+
144+
updated = source.replace(
145+
"from enum import Enum, IntEnum\n",
146+
"from enum import IntEnum\nfrom adcp.types._str_enum import StrEnum\n",
147+
)
148+
updated = updated.replace(
149+
"from enum import IntEnum, Enum\n",
150+
"from enum import IntEnum\nfrom adcp.types._str_enum import StrEnum\n",
151+
)
152+
updated = updated.replace(
153+
"from enum import Enum\n",
154+
"from adcp.types._str_enum import StrEnum\n",
155+
)
156+
updated, changed = re.subn(r"\((?:str,\s*)?Enum\):", "(StrEnum):", updated)
157+
updated, file_ignores_added = _ignore_strenum_member_method_collisions(updated)
158+
159+
if updated != source:
160+
path.write_text(updated)
161+
files_changed += 1
162+
classes_changed += changed
163+
ignores_added += file_ignores_added
164+
165+
print(
166+
f" generated enums rewritten to StrEnum "
167+
f"({classes_changed} classes across {files_changed} files, "
168+
f"{ignores_added} member type ignore(s))"
169+
)
170+
171+
85172
def fix_preview_render_self_reference():
86173
"""Fix self-referential RootModel in preview_render.py."""
87174
preview_file = OUTPUT_DIR / "creative" / "preview_render.py"
@@ -3297,6 +3384,7 @@ def main():
32973384
fix_response_payload_jws_required_literals,
32983385
fix_verify_brand_claim_models,
32993386
fix_signal_coverage_forecast_point_types,
3387+
rewrite_generated_enums_to_strenum,
33003388
strip_extra_blank_lines_at_eof,
33013389
]
33023390
for fix in fixes:

src/adcp/types/_str_enum.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Python-version-compatible StrEnum export for generated schema enums."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from enum import Enum
7+
8+
if sys.version_info >= (3, 11):
9+
from enum import StrEnum as StrEnum
10+
else:
11+
12+
class StrEnum(str, Enum):
13+
"""Backport the stdlib StrEnum behavior needed by generated enums."""
14+
15+
def __str__(self) -> str:
16+
return str.__str__(self)
17+
18+
def __format__(self, format_spec: str) -> str:
19+
return str.__format__(self, format_spec)

src/adcp/types/generated_poc/a2ui/si_catalog.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from __future__ import annotations
66

7-
from enum import Enum
7+
from adcp.types._str_enum import StrEnum
88
from typing import Annotated, Literal
99

1010
from adcp.types.base import AdCPBaseModel
@@ -13,7 +13,7 @@
1313
from . import bound_value
1414

1515

16-
class Component(Enum):
16+
class Component(StrEnum):
1717
Text = 'Text'
1818
Button = 'Button'
1919
Link = 'Link'
@@ -36,7 +36,7 @@ class SiComponentCatalog(AdCPBaseModel):
3636
] = None
3737

3838

39-
class Variant(Enum):
39+
class Variant(StrEnum):
4040
body = 'body'
4141
heading = 'heading'
4242
caption = 'caption'
@@ -56,7 +56,7 @@ class Action(AdCPBaseModel):
5656
] = None
5757

5858

59-
class Variant5(Enum):
59+
class Variant5(StrEnum):
6060
primary = 'primary'
6161
secondary = 'secondary'
6262
text = 'text'
@@ -122,7 +122,7 @@ class Template(AdCPBaseModel):
122122
componentId: Annotated[str, Field(description='ID of component to use as template')]
123123

124124

125-
class Layout(Enum):
125+
class Layout(StrEnum):
126126
vertical = 'vertical'
127127
horizontal = 'horizontal'
128128
grid = 'grid'
@@ -137,16 +137,16 @@ class List(AdCPBaseModel):
137137
layout: Layout | None = Layout.vertical
138138

139139

140-
class Align(Enum):
140+
class Align(StrEnum):
141141
start = 'start'
142-
center = 'center'
142+
center = 'center' # type: ignore[assignment]
143143
end = 'end'
144144
stretch = 'stretch'
145145

146146

147-
class Justify(Enum):
147+
class Justify(StrEnum):
148148
start = 'start'
149-
center = 'center'
149+
center = 'center' # type: ignore[assignment]
150150
end = 'end'
151151
between = 'between'
152152
around = 'around'
@@ -165,7 +165,7 @@ class Column(AdCPBaseModel):
165165
align: Align | None = Align.stretch
166166

167167

168-
class Type(Enum):
168+
class Type(StrEnum):
169169
mcp = 'mcp'
170170
a2a = 'a2a'
171171

src/adcp/types/generated_poc/aao/agent_publishers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@
44

55
from __future__ import annotations
66

7-
from enum import Enum
7+
from adcp.types._str_enum import StrEnum
88
from typing import Annotated
99

1010
from adcp.types.base import AdCPBaseModel
1111
from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field
1212

1313

14-
class DiscoveryMethod(Enum):
14+
class DiscoveryMethod(StrEnum):
1515
direct = 'direct'
1616
authoritative_location = 'authoritative_location'
1717
adagents_authoritative = 'adagents_authoritative'
1818
ads_txt_managerdomain = 'ads_txt_managerdomain'
1919

2020

21-
class Status(Enum):
21+
class Status(StrEnum):
2222
authorized = 'authorized'
2323
revoked = 'revoked'
2424

src/adcp/types/generated_poc/account/list_accounts_request.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from __future__ import annotations
66

7-
from enum import Enum
7+
from adcp.types._str_enum import StrEnum
88
from typing import Annotated
99

1010
from pydantic import ConfigDict, Field
@@ -16,7 +16,7 @@
1616
from ..core.version_envelope import AdcpVersionEnvelope
1717

1818

19-
class Status(Enum):
19+
class Status(StrEnum):
2020
active = 'active'
2121
pending_approval = 'pending_approval'
2222
rejected = 'rejected'

src/adcp/types/generated_poc/adagents.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from __future__ import annotations
66

7-
from enum import Enum
7+
from adcp.types._str_enum import StrEnum
88
from typing import Any, Annotated, Literal
99

1010
from adcp.types.base import AdCPBaseModel
@@ -94,7 +94,7 @@ class Contact(AdCPBaseModel):
9494
] = None
9595

9696

97-
class Reason(Enum):
97+
class Reason(StrEnum):
9898
relationship_ended = 'relationship_ended'
9999
compliance_violation = 'compliance_violation'
100100
publisher_request = 'publisher_request'
@@ -144,7 +144,7 @@ class PlacementTags(AdCPBaseModel):
144144
]
145145

146146

147-
class DelegationType(Enum):
147+
class DelegationType(StrEnum):
148148
direct = 'direct'
149149
delegated = 'delegated'
150150
ad_network = 'ad_network'

0 commit comments

Comments
 (0)