codegen: emit guards for every anyOf variant to fix mypy union-attr on array-containing unions#182
Merged
Merged
Conversation
…rray variants
The encoder generator for non-discriminated anyOf unions emits a chain
of ternary expressions, with the last variant historically rendered as
the unguarded `else` branch. That works for simple unions like
`object | str | list` (mypy can negative-narrow `x` to `list` in the
final branch), but breaks for deeper unions where the array variant is
last, e.g.
str | float | bool | list[scalar] | None
When mypy fails to fully narrow `x` to `list[...]` through the prior
`isinstance` checks (`isinstance(x, (int, float))` + `bool` subclassing
`int` make this tricky), it complains that scalar items of the union
have no `__iter__` attribute:
error: Item "float" of "str | float | bool | None | list[...]"
has no attribute "__iter__" (not iterable) [union-attr]
error: Item "bool" of ... has no attribute "__iter__" [union-attr]
error: Item "object" of ... has no attribute "__iter__" [union-attr]
This is the exact failure that's been blocking the ai-infra
auto-update job (`.github/workflows/codegen-latest-pid2-schema.yml`)
since 2026-05-04, when repl-it-web#78355 widened
`agentToolPostgreSQL.executeSqlCommand.params` to allow array values.
The fix emits an explicit `isinstance`/`is None` guard for every
`encoder_parts` entry, including the last one, and appends a
`cast(Any, x)` fallback so mypy never has to negative-narrow into the
iterating branch. `Any` and `cast` are already part of the standard
generated-file imports.
Tests
-----
- Existing `test_anyof_mixed` snapshot updated to show the new
`isinstance(x, list) else cast(Any, x)` tail on the
`obj | str | list[str]` encoder.
- New `test_anyof_array_in_union` snapshot test added that mirrors
the `executeSqlCommand.params` schema (`array<scalar | array<scalar>>`)
and locks in the fixed output.
- Full pytest suite passes (67 tests, including v1 and v2 codegen).
- `make lint` clean apart from a pre-existing pyright `grpc` import
error in `tests/v1/test_communication.py` (unrelated).
jackyzha0
approved these changes
May 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The encoder generator for non-discriminated anyOf unions emits a chain of ternary expressions, with the last variant historically rendered as the unguarded
elsebranch. That works for simple unions likeobject | str | list(mypy can negative-narrowxtolistin the final branch), but it breaks for deeper unions where the array variant is last, e.g.When mypy fails to fully narrow
xtolist[...]through the priorisinstancechecks (isinstance(x, (int, float))plusboolsubclassingintmake this tricky), it complains that scalar items of the union have no__iter__attribute:This is the exact failure that has been blocking ai-infra's
codegen-latest-pid2-schema.ymlauto-update workflow since 2026-05-04, when repl-it-web#78355 widenedagentToolPostgreSQL.executeSqlCommand.paramsfrom a flat scalar union toarray<scalar | array<scalar>>. Every run since has failed on the regeneratedexecuteSqlCommand.pyat thefor y in xiteration insideencode_ExecutesqlcommandInputParams. The committed pid2 client in ai-infra has been kept current by hand (see replit/ai-infra#12813), but the bot has been red for ~2.5 weeks.What changed
src/replit_river/codegen/client.py: in the non-discriminated-anyOf branch ofencode_type, emit an explicitisinstance/is Noneguard for every entry inencoder_parts— including the last one — and append acast(Any, x)fallback. mypy no longer has to negative-narrow into the iterating branch, so deep unions with an array variant lint cleanly.Anyandcastare already part ofFILE_HEADERso no import bookkeeping changes.Concretely, for the failing executeSqlCommand schema, the encoder now ends with:
Test plan
tests/v1/codegen/snapshot/test_anyof_mixed.pysnapshot updated to show the newif isinstance(x, list) else cast(Any, x)tail on itsobj | str | list[str]encoder (the change is additive — the runtime behavior is unchanged).tests/v1/codegen/snapshot/test_anyof_array_in_union.pyadded with a schema that mirrorsexecuteSqlCommand.params(array<scalar | array<scalar>>) and locks in the fixed output. This is the regression test for ai-infra's CI failure.uv run pytestis green (67 passed, including all v1 and v2 codegen tests).make lintis clean apart from a pre-existingpyrightgrpcimport error intests/v1/test_communication.pythat also fails onmain(unrelated)../pkgs/pid2_client/scripts/generate.shat this branch viaRIVER_CODEGEN_PATH=/tmp/opencode/river-pythonand reran the full lint pipeline that the auto-update workflow runs in CI;[mypy] completed in 15.19sand the script exitedOK.instead of the historicalunion-attrfailure.Once this is released (e.g.
v0.17.20) ai-infra can bumpreplit-riverinpkgs/pid2_client/pyproject.tomland the auto-update workflow will start producing green PRs again.~ written by Zerg 👾 (ascendant-goliath-6d2f)