feat(types): widen extension-point list[X] to Sequence[X] (#624)#640
feat(types): widen extension-point list[X] to Sequence[X] (#624)#640
Conversation
… inheritance Closes #624. Adopters who follow Critical Pattern #1 (subclass a library response type and override a parent's `list[X]` field with `list[ChildX]`) hit `# type: ignore[assignment]` on every override under mypy --strict — list is invariant in its element type. Sequence is covariant, so a Sequence[Parent] parent permits list[Child] override (where Child <: Parent) cleanly. This PR adds a post-codegen processor (`widen_extension_point_lists_to_sequence` in `scripts/post_generate_fixes.py`) that rewrites annotations on a small allowlist of fields documented as extension points. The allowlist is keyed on (file, class, field) so it survives codegen reformatting and field-order drift. The current allowlist contains one confirmed entry — `UpdateMediaBuyResponse.affected_packages` (matches salesagent `_base.py:360`) — across the two emitted variants (`media_buy/` and `bundled/media_buy/`). Nine TODO entries are placeholders pending the salesagent `# type: ignore[assignment]` line list — fill in as they're mapped. `tests/type_checks/extend_response_with_sequence.py` is the regression gate: subclasses `UpdateMediaBuyResponse1` with an overridden `affected_packages: list[_InternalPackage]` and asserts mypy --strict accepts the override with zero ignores. The Pydantic plugin behavior was validated in a 2-file spike before this PR — see the comment on #624 for the validation result. BREAKING CHANGE: callers passing `affected_packages` into a function typed `def f(x: list[Package])` will see a mypy error and need to migrate to `Sequence[Package]`. Runtime behavior is unchanged; the change is annotation-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor allowlist from (file, class, field) to (class, field) tuples and walk every generated .py file. datamodel-codegen emits bundled response files that each inline copies of subordinate types (Placement, TargetingOverlay, etc.); the walker rewrites every emission so all paths stay consistent. Allowlist now covers all 15 Category A entries: - Response: UpdateMediaBuyResponse.affected_packages, GetMediaBuyDeliveryResponse.media_buy_deliveries, GetCreativeDeliveryResponse.creatives, Signal.deployments, GetSignalsResponse.signals, GetMediaBuysResponse.media_buys, ListCreativesResponse.creatives - Request: PackageRequest.creatives, CreateMediaBuyRequest.packages, UpdateMediaBuyRequest.packages - Cross-cutting: Placement.format_ids, TargetingOverlay.geo_*_exclude (4) Result: 47 fields widened across 21 generated files. Pairs that match zero files emit a WARN so allowlist drift surfaces fast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allowlist expanded to all 15 Category A entriesRefactored the rewriter from `(file, class, field)` to `(class, field)` and made it walk every generated `.py` file. `datamodel-codegen` emits bundled response files that each inline copies of subordinate types (`Placement`, `TargetingOverlay`, etc.) — every emission gets rewritten so all paths stay consistent. 47 fields widened across 21 generated files.
Local checks: `mypy src/adcp/` clean (784 source files), `mypy --strict tests/type_checks/` clean (9 source files), test sanity sweep on `test_decisioning_dispatch` + `test_pre_validation_hooks` 63 passed. The walker also emits a `WARN` for any `(class, field)` pair that doesn't match anywhere in the tree, so future allowlist drift (renamed/removed fields) surfaces fast on regen. CI rolling now. |
|
Noted — scope expansion looks solid. The Generated by Claude Code |
…silent idempotency, document scope Three fixes from expert review: 1. Regex now bounded to current class. _widen_field_annotation slices region up to next ^class before running the field search. Latent bug fix — current corpus has no collisions, but prevents future codegen changes from silently triggering it. 2. Idempotent re-runs are silent. New _field_already_widened helper tracks "already Sequence" separately from "no list found"; WARN fires only when neither is found anywhere (genuine drift). 3. Regression test scope honestly documented. Required-field overrides surface a separate codegen multi-emission issue (filed as #642): datamodel-codegen emits Creative/Package/etc. per response file, so public alias resolves to one emission while parent field references another. Test docstring documents the scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Fold candidate from #642: Issue #642 (codegen type-identity mismatch — Generated by Claude Code |
|
Investigated the fold. The two
Approach B as written (replace local re-emission with an import from the canonical module) would wire the delivery-metrics Recommendation: proceed with #640 as-is (Sequence widening still eliminates the variance half of the friction) and leave #642 open as a separate track requiring either a codegen rename ( Generated by Claude Code |
|
Understood — the investigation confirms these are genuinely distinct schemas, not an identity mismatch, so the fold would have wired the wrong type. #640 ships the Sequence widening as intended; #642 stays open as a separate codegen-rename track ( Triaged by Claude Code. Session: https://claude.ai/code/session_01FGyjS99jvAz6F58M9qpH35 Generated by Claude Code |
Closes #624.
Summary
Adopters following the documented Critical Pattern #1 (subclass a library response type, override a parent's `list[X]` field with `list[ChildX]` carrying internal-only fields) carry `# type: ignore[assignment]` on every override under mypy --strict. `list[T]` is invariant in T; `Sequence[T]` is covariant, so a `Sequence[Parent]` parent permits `list[Child]` override cleanly.
This PR adds a post-codegen processor that rewrites `list[X]` → `Sequence[X]` for fields on a small allowlist of documented extension points.
What's in the diff
Allowlist status
Confirmed (1):
Placeholders (9 TODOs):
Blocker on filling these in: I need salesagent's actual `# type: ignore[assignment]` line list to map them accurately. Once the list is dropped, expanding the allowlist is a 5-line change per entry.
Validation
Breaking change
Callers passing `affected_packages` into a function typed `def f(x: list[Package])` will see a mypy error and need to migrate to `Sequence[Package]`. Runtime behavior is unchanged — the change is annotation-only.
Open questions for review