|
6 | 6 | This script applies necessary modifications to generated files that cannot be |
7 | 7 | handled by datamodel-code-generator directly: |
8 | 8 |
|
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) |
15 | 16 | """ |
16 | 17 |
|
17 | 18 | from __future__ import annotations |
@@ -48,6 +49,8 @@ def _load_resolve_bundle_key(): |
48 | 49 |
|
49 | 50 | _PROTOCOL_ENVELOPE_IMPORT = "from ..core.protocol_envelope import ProtocolEnvelope\n" |
50 | 51 | _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)) |
51 | 54 |
|
52 | 55 |
|
53 | 56 | def _sync_protocol_envelope_import(source: str) -> str: |
@@ -82,6 +85,90 @@ def add_model_validator_to_product(): |
82 | 85 | print(" product.py validation: no fixes needed (Pydantic handles discriminated unions)") |
83 | 86 |
|
84 | 87 |
|
| 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 | + |
85 | 172 | def fix_preview_render_self_reference(): |
86 | 173 | """Fix self-referential RootModel in preview_render.py.""" |
87 | 174 | preview_file = OUTPUT_DIR / "creative" / "preview_render.py" |
@@ -3297,6 +3384,7 @@ def main(): |
3297 | 3384 | fix_response_payload_jws_required_literals, |
3298 | 3385 | fix_verify_brand_claim_models, |
3299 | 3386 | fix_signal_coverage_forecast_point_types, |
| 3387 | + rewrite_generated_enums_to_strenum, |
3300 | 3388 | strip_extra_blank_lines_at_eof, |
3301 | 3389 | ] |
3302 | 3390 | for fix in fixes: |
|
0 commit comments