Make __exit__ return types of generator context managers specifiable#14439
Make __exit__ return types of generator context managers specifiable#14439johnslavik wants to merge 10 commits intopython:mainfrom johnslavik:contextdecorator-call-ret-type
__exit__ return types of generator context managers specifiable#14439Conversation
|
I think we can rename the new typevar to |
|
This PR so far has a significant drawback, as it assumes all Therefore for from collections.abc import Generator
@contextmanager
def not_suppressing() -> Generator[None]:
yield
@not_suppressing()
def foo(x: int, y: int) -> int:
return x // yit causes Ideally, whether suppression happens or not could be inferred by the type checker. |
This comment has been minimized.
This comment has been minimized.
jax errorIt's because I believe that in this sense prefect error
openlibrary errorSimilarly for openlibrary, |
|
This PR is blocked until I figure out a way to make this edge case an opt-in. That might either be via type checker special casing: def example() -> int:
with suppress():
return 1for both mypy and pyright, type checking this code errors out (although type checkers could be smarter in this exact case :-)): from contextlib import suppress
def example() -> int:
# [pyright] error: Function with declared return type "int" must return value on all code paths
# [mypy] error: Missing return statement [return]
with suppress():
return 1and this doesn't: from contextlib import nullcontext
def example() -> int:
with nullcontext():
return 1So it definitely is possible to infer what @contextmanager
def example() -> Generator[None]:
# type checker should flag this as suppressing context manager
# therefore `example` becomes a `Callable[_P, _GeneratorContextManager[..., None]]`
with suppress():
yield@contextmanager
def example() -> Generator[None]:
# normal path, this becomes a `Callable[_P, _GeneratorContextManager[..., Never]]`
with nullcontext():
yield |
|
Oh, although that doesn't solve the problem with jax. That shouldn't really be a huge issue if the annotation can be corrected downstream, question is how. |
|
I exposed |
This comment has been minimized.
This comment has been minimized.
|
OK, this looks good. |
None to [Async]ContextDecorator.__call__ return type when context managers suppress exceptionsContextDecorator with None
ContextDecorator with NoneContextDecorator with None
|
This would not solve a related problem though: from contextlib import contextmanager, suppress
from collections.abc import Generator
@contextmanager
def ignore() -> Generator[None]:
with suppress(ZeroDivisionError):
yield
def div(a: int, b: int) -> float:
with ignore():
return a / bThat should cause in the same error as in from contextlib import suppress
def div(a: int, b: int) -> float:
with suppress(ZeroDivisionError):
return a / bWhich signalizes that |
|
This led me to seeing a more general problem consisting of at least these 3 issues with how type checkers (don't) handle generator-based context managers:
In my understanding, there are only two correct bindings of
Their individual upper bounds would remain the same, but there should be some way to enforce that logic. This sheds new light on this issue as a much more general problem that needs discussion and new sections in the typing specification. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Detailspwndbg (https://github.com/pwndbg/pwndbg)
+ pwndbg/dbg/gdb/__init__.py: note: In member "evaluate_expression" of class "GDBFrame":
+ pwndbg/dbg/gdb/__init__.py:141: error: "ABC" has no attribute "__enter__" [attr-defined]
+ pwndbg/dbg/gdb/__init__.py:141: error: "ABC" has no attribute "__exit__" [attr-defined]
sphinx (https://github.com/sphinx-doc/sphinx)
+ sphinx/builders/__init__.py: note: In member "build" of class "Builder":
+ sphinx/builders/__init__.py:405:13: error: "ABC" has no attribute "__enter__" [attr-defined]
+ sphinx/builders/__init__.py:405:13: error: "ABC" has no attribute "__exit__" [attr-defined]
+ sphinx/builders/__init__.py: note: In member "_write_serial" of class "Builder":
+ sphinx/builders/__init__.py:773:13: error: "ABC" has no attribute "__enter__" [attr-defined]
+ sphinx/builders/__init__.py:773:13: error: "ABC" has no attribute "__exit__" [attr-defined]
core (https://github.com/home-assistant/core)
+ homeassistant/components/qnap/coordinator.py:74: error: "ABC" has no attribute "__enter__" [attr-defined]
+ homeassistant/components/qnap/coordinator.py:74: error: "ABC" has no attribute "__exit__" [attr-defined]This is odd. Does mypy think that the common base of |
|
Oh, this used to pass on mypy 1.16.1 but it doesn't on mypy 1.17.1... Looks like I spotted a regression in the meantime... |
|
Anyways, I'm pretty confident that this is correct. Ready for review. |
ContextDecorator with None__exit__ return types of generator context managers specifiable
Perhaps it's because of disjoint bases? I'll investigate soon. |
|
|
||
| # __exit__ can suppress exceptions by returning a true value. | ||
| # _ExcReturnT describes function's return type after an exception occurs: | ||
| # - Never (default, the context manager never suppresses exceptions) |
There was a problem hiding this comment.
I'm not sure we can really use Never to say "may raise an exception". Every function can potentially raise an exception – for example KeyboardInterrupt. As Never is the bottom type, X | Never is equivalent to just X:
from typing import Never
def foo() -> Never | int:
return 3
reveal_type(foo()) # Revealed type is "builtins.int"
foo() + 5 # legalThere was a problem hiding this comment.
X | Neveris equivalent to justX
Yes, that's the entire point of this solution!
We don't use Never to signal something may raise an exception, but to not extend the return type with None when no suppression happens. This way we leave the return type unchanged, because we union it with Never.
It's an intentional design that's exactly based on what stems from your explanation of Never :-)
There was a problem hiding this comment.
I think we can make this comment a little clearer, FWIW. Would you like me to do that? @srittau
|
Diff from mypy_primer, showing the effect of this PR on open source code: pwndbg (https://github.com/pwndbg/pwndbg)
+ pwndbg/dbg/gdb/__init__.py: note: In member "evaluate_expression" of class "GDBFrame":
+ pwndbg/dbg/gdb/__init__.py:141: error: "ABC" has no attribute "__enter__" [attr-defined]
+ pwndbg/dbg/gdb/__init__.py:141: error: "ABC" has no attribute "__exit__" [attr-defined]
sphinx (https://github.com/sphinx-doc/sphinx)
+ sphinx/builders/__init__.py: note: In member "build" of class "Builder":
+ sphinx/builders/__init__.py:405:13: error: "ABC" has no attribute "__enter__" [attr-defined]
+ sphinx/builders/__init__.py:405:13: error: "ABC" has no attribute "__exit__" [attr-defined]
+ sphinx/builders/__init__.py: note: In member "_write_serial" of class "Builder":
+ sphinx/builders/__init__.py:773:13: error: "ABC" has no attribute "__enter__" [attr-defined]
+ sphinx/builders/__init__.py:773:13: error: "ABC" has no attribute "__exit__" [attr-defined]
core (https://github.com/home-assistant/core)
+ homeassistant/components/qnap/coordinator.py:74: error: "ABC" has no attribute "__enter__" [attr-defined]
+ homeassistant/components/qnap/coordinator.py:74: error: "ABC" has no attribute "__exit__" [attr-defined]
jax (https://github.com/google/jax)
+ jax/_src/api.py:3094: error: Incompatible return value type (got "Callable[[VarArg(Any), KwArg(Any)], Any]", expected "F") [return-value]
pytest (https://github.com/pytest-dev/pytest)
+ src/_pytest/logging.py:931: error: "ABC" has no attribute "__enter__" [attr-defined]
+ src/_pytest/logging.py:931: error: "ABC" has no attribute "__exit__" [attr-defined]
cki-lib (https://gitlab.com/cki-project/cki-lib)
- tests/test_metrics.py:75: error: Call to untyped function "dummy_function" in typed context [no-untyped-call]
|
Closes #13512
Please see https://discuss.python.org/t/support-suppressing-generator-context-managers/103615 first!