Skip to content

codegen: emit guards for every anyOf variant to fix mypy union-attr on array-containing unions#182

Merged
kush-replit merged 1 commit into
mainfrom
fix/anyof-array-last-narrowing
May 22, 2026
Merged

codegen: emit guards for every anyOf variant to fix mypy union-attr on array-containing unions#182
kush-replit merged 1 commit into
mainfrom
fix/anyof-array-last-narrowing

Conversation

@kush-replit
Copy link
Copy Markdown
Contributor

Why

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 it 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)) plus 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 has been blocking ai-infra's codegen-latest-pid2-schema.yml auto-update workflow since 2026-05-04, when repl-it-web#78355 widened agentToolPostgreSQL.executeSqlCommand.params from a flat scalar union to array<scalar | array<scalar>>. Every run since has failed on the regenerated executeSqlCommand.py at the for y in x iteration inside encode_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 of encode_type, emit an explicit isinstance / is None guard for every entry in encoder_parts — including the last one — and append a cast(Any, x) fallback. mypy no longer has to negative-narrow into the iterating branch, so deep unions with an array variant lint cleanly. Any and cast are already part of FILE_HEADER so no import bookkeeping changes.

Concretely, for the failing executeSqlCommand schema, the encoder now ends with:

return (
    x if isinstance(x, str)
    else x if isinstance(x, (int, float))
    else x if isinstance(x, bool)
    else None if x is None
    else [encode_..._AnyOf_4(y) for y in x]
        if isinstance(x, list)
        else cast(Any, x)
)

Test plan

  • Existing tests/v1/codegen/snapshot/test_anyof_mixed.py snapshot updated to show the new if isinstance(x, list) else cast(Any, x) tail on its obj | str | list[str] encoder (the change is additive — the runtime behavior is unchanged).
  • New snapshot test tests/v1/codegen/snapshot/test_anyof_array_in_union.py added with a schema that mirrors executeSqlCommand.params (array<scalar | array<scalar>>) and locks in the fixed output. This is the regression test for ai-infra's CI failure.
  • uv run pytest is green (67 passed, including all v1 and v2 codegen tests).
  • make lint is clean apart from a pre-existing pyright grpc import error in tests/v1/test_communication.py that also fails on main (unrelated).
  • End-to-end verification against ai-infra: pointed ai-infra's ./pkgs/pid2_client/scripts/generate.sh at this branch via RIVER_CODEGEN_PATH=/tmp/opencode/river-python and reran the full lint pipeline that the auto-update workflow runs in CI; [mypy] completed in 15.19s and the script exited OK. instead of the historical union-attr failure.

Once this is released (e.g. v0.17.20) ai-infra can bump replit-river in pkgs/pid2_client/pyproject.toml and the auto-update workflow will start producing green PRs again.

~ written by Zerg 👾 (ascendant-goliath-6d2f)

…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).
@kush-replit kush-replit added the zergling-authored Authored by a zergling agent label May 22, 2026
@kush-replit kush-replit marked this pull request as ready for review May 22, 2026 00:56
@kush-replit kush-replit requested a review from a team as a code owner May 22, 2026 00:56
@kush-replit kush-replit requested review from wernst and removed request for a team May 22, 2026 00:56
@kush-replit kush-replit merged commit c7566b6 into main May 22, 2026
2 checks passed
@kush-replit kush-replit deleted the fix/anyof-array-last-narrowing branch May 22, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

zergling-authored Authored by a zergling agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants