Skip to content

Commit 332c034

Browse files
authored
Merge pull request #46 from devhelmhq/fix/alerting-null-wall-integration-library
fix: relax response-DTO models from extra='forbid' to extra='ignore'
2 parents d7a3516 + ca1ff6c commit 332c034

3 files changed

Lines changed: 217 additions & 196 deletions

File tree

‎scripts/inject_strict_config.py‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,24 @@
5050
# `root-model-extra`), so skip them. Their behavior is governed by the
5151
# inner type, which on its own enforces strict validation.
5252
CLASS_RE = re.compile(r"^class\s+([A-Za-z_][\w]*)\s*\(\s*(BaseModel)\s*\)\s*:\s*$")
53-
CONFIG_LINE = " model_config = ConfigDict(extra='forbid', populate_by_name=True)"
53+
CONFIG_LINE_STRICT = " model_config = ConfigDict(extra='forbid', populate_by_name=True)"
54+
CONFIG_LINE_TOLERANT = " model_config = ConfigDict(extra='ignore', populate_by_name=True)"
55+
56+
57+
def _is_response_shape(class_name: str) -> bool:
58+
"""Response-shape classes tolerate unknown fields (Postel's Law)."""
59+
if class_name[0].islower():
60+
return False
61+
if class_name.endswith(("Request", "Params")):
62+
return False
63+
return bool(
64+
class_name.endswith(("Dto", "Response"))
65+
or class_name.startswith(("SingleValueResponse", "TableValueResult", "CursorPage"))
66+
)
67+
68+
69+
# Keep the old name for backward compat in case anything imports it
70+
CONFIG_LINE = CONFIG_LINE_STRICT
5471

5572
# Doc-banner injections keyed by class name. Inserted as a leading docstring
5673
# inside the target class so the note shows up in IDE hovers and stays put
@@ -130,6 +147,7 @@ def inject(source: str) -> tuple[str, int]:
130147
i += 1
131148
continue
132149
class_name = m.group(1)
150+
config_line = CONFIG_LINE_TOLERANT if _is_response_shape(class_name) else CONFIG_LINE_STRICT
133151
# Look at the very next line. If it's already model_config or pass,
134152
# leave the class alone (idempotency / empty class).
135153
next_idx = i + 1
@@ -141,10 +159,10 @@ def inject(source: str) -> tuple[str, int]:
141159
out.append(f' """{banner}"""\n')
142160
modified += 1
143161
if "model_config" in next_line:
144-
# Upgrade the existing config line to include populate_by_name=True
145-
# if it isn't already there. Idempotent across re-runs.
146-
if "populate_by_name" not in next_line:
147-
out.append(CONFIG_LINE + "\n")
162+
# Replace the existing config line if it doesn't match the
163+
# desired strictness or is missing populate_by_name.
164+
if next_line.strip() != config_line.strip():
165+
out.append(config_line + "\n")
148166
i += 2 # replace the existing model_config line
149167
modified += 1
150168
continue
@@ -154,11 +172,11 @@ def inject(source: str) -> tuple[str, int]:
154172
# exact match (NOT startswith) — fields like `passed: Annotated[...]`
155173
# also start with "pass" but are not empty class markers.
156174
if next_line.strip() in ("pass", "pass\n"):
157-
out.append(CONFIG_LINE + "\n")
175+
out.append(config_line + "\n")
158176
i += 2 # skip the pass
159177
modified += 1
160178
continue
161-
out.append(CONFIG_LINE + "\n")
179+
out.append(config_line + "\n")
162180
modified += 1
163181
i += 1
164182
return "".join(out), modified

0 commit comments

Comments
 (0)