[ty] Extend support for more detailed diagnostics on possibly unbound errors from implicit dunder calls against unions. by lerebear · Pull Request #24676 · astral-sh/ruff (original) (raw)
Bot added the ty
Multi-file analysis & type inference
label
lerebear changed the title
Complete support for more detailed diagnostics on possibly unbound errors from implicit dunder calls against unions. [ty] Complete support for more detailed diagnostics on possibly unbound errors from implicit dunder calls against unions.
lerebear added a commit that referenced this pull request
…m context manager dunder methods invoked on a union. (#24662)
Summary
As part of astral-sh/ty#940, this helps us
emit more specific diagnostics for possibly unbound context manager
dunders (e.g., __enter__, __exit__) invoked on a union type.
Where previously the following snippet would produce just the top-level diagnostic commented below:
class Context:
def __enter__(self): ...
def __exit__(self, *args): ...
class NotContext:
pass
def _(x: Context | NotContext):
# error: [invalid-context-manager] "Object of type `Context | NotContext` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound"
with x:
passWe will now produce two further "info" sub-diagnostics:
info: `NotContext` does not implement `__enter__`
info: `NotContext` does not implement `__exit__`Approach
This implements the approach suggested by
@carljm[here](#20199 (review)) from a previous attempt to address astral-sh/ty#940; it extendsCallDunderError::PossiblyUnboundwith a newunbound_onfield that stores a list of the union members on which a particular dunder is unbound. We create the new, richer error with a newUnionType.try_call_dunder_with_policymethod that looks up the dunder on each member of the union, and then aggregates the results. This is supersedes the previousUnionType.map_with_boundness_and_qualifiersapproach, and allows us to preserve the per-member binding information that we use to produce the more detailed diagnostic.There are two alternatives to this approach that I considered but rejected:
Rebuild the specific union member diagnositic information at each callsite, and only when relevant. This was the approach originally taken by #20199, but I think it will lead to some unnecessary code duplication across callsites (of which there are at least three more).
Refactor such that
UnionType.map_with_boundness_and_qualifierssuch that it no longer loses member-specific binding information when producing its result. This would have required an extension toPlaceAndQualifiers, which would have a large blast radius and also introduce overhead in several cases where member-specific information for unions is not necessary.There are more implicit dunder calls that can benefit from the new shape of
CallDunderError::PossiblyUnbound, but I have intentionally deferred those to a follow-up in order to first collect feedback on a more targeted changeset.The first three commits in this PR (926bcec, 65dc3fb, 988e81d) are "prefactors" that do not change any observable behaviour. The fourth (2422844) actually implements the improvement, and deserves the most scrutiny.
Test Plan
Please see updated mdtests and associated snapshots.
Base automatically changed from lerebear/push-tuskvpoxpzyx to main
lerebear marked this pull request as ready for review
lerebear deleted the lerebear/push-ypypplmwkqpy branch
lerebear changed the title
[ty] Complete support for more detailed diagnostics on possibly unbound errors from implicit dunder calls against unions. [ty] Extend support for more detailed diagnostics on possibly unbound errors from implicit dunder calls against unions.
carljm added a commit to tamird/ruff that referenced this pull request
This was referenced
Apr 27, 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
[ Show hidden characters]({{ revealButtonHref }})