Skip to content

fix(codegen): rewrite compact ConfigDict extra in place#935

Merged
bokelley merged 1 commit into
mainfrom
claude/issue-907-compact-configdict
Jun 7, 2026
Merged

fix(codegen): rewrite compact ConfigDict extra in place#935
bokelley merged 1 commit into
mainfrom
claude/issue-907-compact-configdict

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Jun 7, 2026

Summary

Fixes the one remaining gap in #907: _set_class_extra_allow in
scripts/post_generate_fixes.py only matched the multi-line
model_config = ConfigDict(\n ...) form. A compact single-line
model_config = ConfigDict(extra='forbid') did not match, so it fell
through to the else branch that prepends a fresh model_config block —
producing two model_config declarations in the same class, which breaks
Pydantic at import time.

The other #907 sub-items (missing ConfigDict import injection, enum-base
exclusion for the title-less root sentinel, the #902 short-circuit note,
docs follow-ups) are already shipped. This PR closes the compact-ConfigDict
residual only.

Repro (before fix)

A compact ConfigDict(extra='forbid') on a class that should get
extra='allow' produced a duplicate:

class PartnerPayload(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    model_config = ConfigDict(extra='forbid')   # duplicate — last one wins
    name: str

Change

_set_class_extra_allow now matches the compact single-line form
(ConfigDict(<args>) where the open paren is not followed by a newline) and
rewrites it in place:

  • extra='allow' already present → returns "already", no change
  • extra='forbid'/'ignore' → flipped to extra='allow' in place
  • other args, no extra=extra='allow' inserted into the call
  • empty args → extra='allow' inserted

Exactly one model_config is emitted; the multi-line and no-config paths are
unchanged.

Tests

Added test_compact_configdict_forbid_rewritten_in_place to
tests/test_extra_policy.py asserting the compact extra='forbid'
extra='allow' rewrite and that no duplicate model_config is produced.

Verification

  • uv run --extra dev pytest tests/test_extra_policy.py -q → 14 passed
  • make lint → passed
  • make typecheck → passed
  • make validate-generated → passed
  • No regen needed; no generated files changed.

🤖 Generated with Claude Code

`_set_class_extra_allow` only matched the multi-line
`model_config = ConfigDict(\n ...)` form. A compact single-line
`ConfigDict(extra='forbid')` fell through to the else branch, which
prepended a second `model_config` block — two declarations in one class,
breaking Pydantic at import time.

Match the compact single-line form and flip `extra='forbid'`/`'ignore'`
to `extra='allow'` in place (inserting `extra='allow'` when other args or
no args are present), so exactly one `model_config` is emitted.

Addresses the compact-ConfigDict residual of #907.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@aao-ipr-bot aao-ipr-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix. The compact and multi-line forms are now mutually exclusive arms over the same model_config, so exactly one declaration survives — which is the whole point.

Things I checked

  • No multi-line mis-fire. compact_pattern group 2 is [^\n]*? — it can't cross a newline, so a true ConfigDict(\n ... \n) (open paren immediately followed by \n) never matches it. And compact_match is only consumed in the elif, reachable only when config_match is None. The multi-line path always wins when present. scripts/post_generate_fixes.py:516-517, :542.
  • In-place rewrite, single model_config. Reconstruction splices body[:start] + group(1) + new_args + group(3) + body[end:] — rewrite in place, no prepended second block. :556-563. The new test's updated.count("model_config") == 1 locks it.
  • Branch coverage. extra='allow' present → \"already\"; forbid/ignorere.sub count=1; other args → \"extra='allow', \" + args; empty → extra='allow'. All four produce valid Python (ConfigDict(extra='allow', populate_by_name=True), ConfigDict(extra='allow')). :545-554.
  • Quote handling. extra=(['\"])(?:forbid|ignore)\1 matches single or double quotes; replacement normalizes to single-quote extra='allow', same as the existing multi-line arm at :521-527. Consistent.
  • Scope. body is bounded to one class by the (?=^class |\Z) lookahead, so .search only sees the target class's own config. No cross-class bleed.
  • Layering. Build-time codegen post-processor — no src/adcp/ public surface, no wire types, no generated_poc/ hand-edits. code-reviewer: clean, zero findings (Blocker/Major/Minor all none). Commit prefix fix(codegen) is the right semver signal — non-breaking, no public export touched.

Minor nits (non-blocking)

  1. Live test run not executed here. Sandbox blocked pytest/branch checkout, so the rewrite is verified by static analysis of the regex + reconstruction, not a live run. The author reports 14/14 plus lint/typecheck/validate-generated green; CI is the backstop. No concern — noting it for honesty.

LGTM.

@bokelley bokelley merged commit 4d3303f into main Jun 7, 2026
26 checks passed
@bokelley bokelley deleted the claude/issue-907-compact-configdict branch June 7, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant