fix(narrow): narrow TypedDict unions on subscript membership tests#3606
fix(narrow): narrow TypedDict unions on subscript membership tests#3606mikeleppane wants to merge 2 commits into
Conversation
A discriminated TypedDict union tested with `event[key] in (...)` was left as the full union. `atomic_narrow_for_facet` eliminated members for `==`/`is` discriminant checks but did nothing for `in`/`not in`, so impossible members survived the branch and downstream uses failed to type-check. Narrow each union member by running its discriminant facet through the same `atomic_narrow` already used for the facet value, then drop any member whose facet collapses to `Never` — a member that cannot satisfy the test is impossible in that branch. Sharing one narrowing routine keeps the facet-value and parent narrowing consistent and inherits the existing literal, enum, and class-object membership handling for free.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
a2979ed eliminates a union member when its `x.facet in c` discriminant narrows to Never. For a general mapping, atomic_narrow intersects the facet with the key type, yielding Never for disjoint types — which collapsed the entire base, even a non-union one. pandera hit this: `arg in param_dict` made annot_info Never, so a downstream subscript became Optional[Never]. A mapping key type is not a closed value set, so an empty intersection does not prove a member impossible. Restrict member elimination to enumerable literal containers and skip recording an impossible facet narrow for non-enumerable membership. Literal/enum discriminants are unaffected, preserving the win from a2979ed.
|
Diff from mypy_primer, showing the effect of this PR on open source code: bokeh (https://github.com/bokeh/bokeh)
- ERROR src/bokeh/models/layouts.py:332:20-31: Returned type `Nullable[int]` is not assignable to declared return type `int | None` [bad-return]
pydantic (https://github.com/pydantic/pydantic)
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `AfterValidatorFunctionSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `AnySchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `ArgumentsSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `ArgumentsV3Schema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `BeforeValidatorFunctionSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `BoolSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `BytesSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `CallSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `CallableSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `ChainSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `ComplexSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `CustomErrorSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DataclassArgsSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DataclassSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DateSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DatetimeSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DecimalSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DefinitionReferenceSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DefinitionsSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `DictSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `EnumSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `FloatSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `FrozenSetSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `GeneratorSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `IntSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `InvalidSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `IsInstanceSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `IsSubclassSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `JsonOrPythonSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `JsonSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `LaxOrStrictSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `ListSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `LiteralSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `MissingSentinelSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `ModelFieldsSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `ModelSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `NoneSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `NullableSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `PlainValidatorFunctionSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `SetSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `StringSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `TaggedUnionSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `TimeSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `TimedeltaSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `TupleSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `TypedDictSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `UnionSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `UuidSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `WithDefaultSchema` [unsupported-operation]
- ERROR pydantic/networks.py:122:13-45: Cannot set item in `WrapValidatorFunctionSchema` [unsupported-operation]
|
Primer Diff Classification✅ 2 improvement(s) | 2 project(s) total | -2 errors 2 improvement(s) across bokeh, pydantic.
Detailed analysis✅ Improvement (2)bokeh (-1)
pydantic (-1)
Was this helpful? React with 👍 or 👎 Classification by primer-classifier (2 LLM) |
Summary
What
Narrow discriminated
TypedDictunions when the discriminant is tested with a membership check (event[key] in (...) / not in (...)), not just with equality/identity.Given a union where each member pins a shared key to a distinct literal:
ProcessMeta (ph: Literal["M"]) is now correctly dropped inside the branch, and the union is fully restored in the else.
Why
Subscript narrowing does two jobs: it narrows the facet value (
event["ph"]), and it narrows the parent by eliminating union members whose discriminant can't match. The parent step (atomic_narrow_for_facet) handled ==/!=/is/is not but left in/not in to narrow nothing. So equality discriminants worked while membership discriminants silently left the full union in place - impossible members survived the branch, and downstream assignments (list.append, returns, etc.) failed to type-check even though the code is sound.How
The in/not in arm now narrows each union member's discriminant facet through the same
atomic_narrowroutine already used for the facet value, and drops any member whose facet collapses toNever- a member that can't satisfy the test is impossible in that branch.Fixes #3603
Test Plan
cargo test -p pyrefly test::narrow- full narrowing module greenpython3 test.py --no-test --no-conformance --no-jsonschema- cargo fmt + clippy clean