Skip to content

[ty] chore: ContextManagerError message update#20199

Closed
Y3K wants to merge 3 commits intoastral-sh:mainfrom
Y3K:chore/ContextManageError_message_update
Closed

[ty] chore: ContextManagerError message update#20199
Y3K wants to merge 3 commits intoastral-sh:mainfrom
Y3K:chore/ContextManageError_message_update

Conversation

@Y3K
Copy link
Copy Markdown

@Y3K Y3K commented Sep 1, 2025

Summary

Fixes astral-sh/ty#940

Test Plan

class Bound:
    def __enter__(self): ...

    def __exit__(self, *args): ...


class EnterUnbound:
    def __exit__(self): ...


class ExitUnbound:
    def __enter__(self): ...


class Unbound: ...


def bound(x: Bound):
    with x: ...


def enter_unbound(x: EnterUnbound):
    with x: ...


def exit_unbound(x: ExitUnbound):
    with x: ...


def unbound(x: Unbound):
    with x: ...


def union_enter_unbound(x: Bound | EnterUnbound):
    with x: ...


def union_exit_unbound(x: Bound | ExitUnbound):
    with x: ...


def union_unbound(x: Bound | Unbound):
    with x: ...


def multiple(x: Bound | EnterUnbound | ExitUnbound):
    with x: ...
$ cargo run --bin ty -- check --project ../pydummy

error[invalid-context-manager]: Object of type `EnterUnbound` cannot be used with `with` because it does not implement `__enter__`, and it does not correctly implement `__exit__`
  --> ../pydummy/main.py:23:10
   |
22 | def enter_unbound(x: EnterUnbound):
23 |     with x: ...
   |          ^
   |
info: rule `invalid-context-manager` is enabled by default

error[invalid-context-manager]: Object of type `ExitUnbound` cannot be used with `with` because it does not implement `__exit__`
  --> ../pydummy/main.py:27:10
   |
26 | def exit_unbound(x: ExitUnbound):
27 |     with x: ...
   |          ^
   |
info: rule `invalid-context-manager` is enabled by default

error[invalid-context-manager]: Object of type `Unbound` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
  --> ../pydummy/main.py:31:10
   |
30 | def unbound(x: Unbound):
31 |     with x: ...
   |          ^
   |
info: rule `invalid-context-manager` is enabled by default

error[invalid-context-manager]: Object of type `Bound | EnterUnbound` cannot be used with `with` because the method `__enter__` of `EnterUnbound` is possibly unbound
  --> ../pydummy/main.py:35:10
   |
34 | def union_enter_unbound(x: Bound | EnterUnbound):
35 |     with x: ...
   |          ^
   |
info: rule `invalid-context-manager` is enabled by default

error[invalid-context-manager]: Object of type `Bound | ExitUnbound` cannot be used with `with` because the method `__exit__` of `ExitUnbound` is possibly unbound
  --> ../pydummy/main.py:39:10
   |
38 | def union_exit_unbound(x: Bound | ExitUnbound):
39 |     with x: ...
   |          ^
   |
info: rule `invalid-context-manager` is enabled by default

error[invalid-context-manager]: Object of type `Bound | Unbound` cannot be used with `with` because the methods `__enter__` and `__exit__` of `Unbound` are possibly unbound
  --> ../pydummy/main.py:43:10
   |
42 | def union_unbound(x: Bound | Unbound):
43 |     with x: ...
   |          ^
   |
info: rule `invalid-context-manager` is enabled by default

error[invalid-context-manager]: Object of type `Bound | EnterUnbound | ExitUnbound` cannot be used with `with` because the method `__enter__` of `EnterUnbound` is possibly unbound, and the method `__exit__` of `ExitUnbound` is possibly unbound
  --> ../pydummy/main.py:47:10
   |
46 | def multiple(x: Bound | EnterUnbound | ExitUnbound):
47 |     with x: ...
   |          ^
   |
info: rule `invalid-context-manager` is enabled by default

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Sep 1, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Sep 1, 2025

mypy_primer results

Changes were detected when running on open source projects
prefect (https://github.com/PrefectHQ/prefect)
- src/prefect/artifacts.py:675:16: error[invalid-context-manager] Object of type `nullcontext[PrefectClient & ~AlwaysFalsy] | PrefectClient` cannot be used with `async with` because the methods `__aenter__` and `__aexit__` are possibly unbound
+ src/prefect/artifacts.py:675:16: error[invalid-context-manager] Object of type `nullcontext[PrefectClient & ~AlwaysFalsy] | PrefectClient` cannot be used with `async with` because the methods `__aenter__` and `__aexit__` of `nullcontext[PrefectClient & ~AlwaysFalsy]` are possibly unbound

xarray (https://github.com/pydata/xarray)
- xarray/tests/test_distributed.py:316:14: error[invalid-context-manager] Object of type `Unknown | None` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ xarray/tests/test_distributed.py:316:14: error[invalid-context-manager] Object of type `Unknown | None` cannot be used with `with` because the methods `__enter__` and `__exit__` of `None` are possibly unbound

pandas (https://github.com/pandas-dev/pandas)
- pandas/tests/io/test_stata.py:1302:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1302:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1347:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1347:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1351:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1351:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1355:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1355:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1359:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1359:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1364:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1364:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1400:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1400:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1427:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1427:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:1692:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:1692:14: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
- pandas/tests/io/test_stata.py:2351:10: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound
+ pandas/tests/io/test_stata.py:2351:10: error[invalid-context-manager] Object of type `DataFrame | StataReader` cannot be used with `with` because the methods `__enter__` and `__exit__` of `DataFrame` are possibly unbound
No memory usage changes detected ✅

@Y3K Y3K force-pushed the chore/ContextManageError_message_update branch from 06686e9 to 0fdf81e Compare September 1, 2025 19:54
@Y3K Y3K changed the title [ty] chore: ContextManagerError message update [ty] chore: ContextManagerError message update Sep 1, 2025
@Y3K Y3K force-pushed the chore/ContextManageError_message_update branch from 0fdf81e to 9733d41 Compare September 1, 2025 21:49
@ntBre ntBre added the ty Multi-file analysis & type inference label Sep 2, 2025
Copy link
Copy Markdown
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pull request! This is definitely an improvement.

My idea when I filed the issue was that we would bubble up the information about which types had the dunder unbound or possibly-unbound as part of the CallDunderError, but looking at it now I see this would probably require special-casing Type::Union in try_call_dunder (because member_lookup_with_policy squashes union information into a single PlaceAndQualifiers). I still think this might make sense, but I also think it's OK to just re-create the information once we know there's a problem.

Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment thread crates/ty_python_semantic/src/types.rs Outdated
```

## Union context managers with specific member issues

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can snapshot diagnostics for these tests, which is an easier way to see the full resulting diagnostic, instead of putting full error messages directly in the mdtest. This is usually preferred for tests focused on diagnostic rendering, and is especially useful when the diagnostic is not all crammed into a single sentence, as I suggest below.

Suggested change
<!-- snapshot-diagnostics -->

Then you use cargo insta test -p ty_python_semantic to generate new snapshots to review with cargo insta review and then commit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the rookie question.

Can I get a really brief example of how snapshots work?

e.g. should be snapshotted in main or post changes? What if the message changes do I need to update snaps? Are all the snaps run when I do cargo nextest run?


context_expr = Bound() if flag else EnterUnbound()

# error: [invalid-context-manager] "Object of type `Bound | EnterUnbound` cannot be used with `async with` because the method `__aenter__` of `EnterUnbound` is possibly unbound"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. if we snapshot diagnostics, then we can just do this here:

Suggested change
# error: [invalid-context-manager] "Object of type `Bound | EnterUnbound` cannot be used with `async with` because the method `__aenter__` of `EnterUnbound` is possibly unbound"
# error: [invalid-context-manager]

Comment thread crates/ty_python_semantic/src/types.rs
@sharkdp sharkdp removed their request for review September 3, 2025 07:22
@Y3K
Copy link
Copy Markdown
Author

Y3K commented Sep 3, 2025

Thank you for your review @carljm!

I'm quite rookie with Rust and ty therefore the changes requested still are a bit confussing to me.

I'll read each suggestion carefully and work on a new approach, will ping you once it's ready.

Thanks!

@carljm
Copy link
Copy Markdown
Contributor

carljm commented Sep 3, 2025

@Y3K happy to offer clarification on any comments. I think for purposes of this PR you can ignore the comment about "bubbling up the information through CallDunderError", and you can ignore one other comment I'll mention inline. The other ones I think don't require an entirely "new approach" to address, just some updates to the current approach.

@Y3K
Copy link
Copy Markdown
Author

Y3K commented Sep 3, 2025

Thank you again, @carljm !

I don't want to waste a lot of your time, but I do accept any extra help you want and can provide.

Will address all your feedback within this PR 🙌🏼

@Y3K Y3K marked this pull request as draft September 3, 2025 17:33
@Y3K Y3K force-pushed the chore/ContextManageError_message_update branch from aee5519 to 67846c0 Compare September 3, 2025 17:34
@sharkdp
Copy link
Copy Markdown
Contributor

sharkdp commented Feb 3, 2026

I'm closing this due to inactivity. Please feel free to comment in case it should be re-opened.

I'll add a comment to the linked ticket that this is useful as a resource if someone picks that task up again.

@sharkdp sharkdp closed this Feb 3, 2026
lerebear added a commit that referenced this pull request Apr 20, 2026
…m context manager dunder methods invoked on a union. (#24662)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->

## 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:

```py
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:
        pass
```

We 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 extends
`CallDunderError::PossiblyUnbound` with a new `unbound_on` field that
stores a list of the union members on which a particular dunder is
unbound. We create the new, richer error with a new
`UnionType.try_call_dunder_with_policy` method that looks up the dunder
on each member of the union, and then aggregates the results. This is
supersedes the previous `UnionType.map_with_boundness_and_qualifiers`
approach, 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_qualifiers` such
that it no longer loses member-specific binding information when
producing its result. This would have required an extension to
`PlaceAndQualifiers`, 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](#24676) 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

improve diagnostics for possibly-unbound call-dunder with union

4 participants