Skip to content

Commit 7272176

Browse files
committed
fix contextmanager decorator treating all context managers as if they can suppress exceptions when that's no longer the case since python 3.7
1 parent 75f969a commit 7272176

File tree

2 files changed

+35
-9
lines changed

2 files changed

+35
-9
lines changed

packages/pyright-internal/src/tests/samples/withBased.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import contextlib
1+
from contextlib import AbstractContextManager, contextmanager
22
from types import TracebackType
3-
from typing import Iterator, Literal
3+
from typing import Iterator, Literal, Iterator
44

55
from typing_extensions import assert_never
66

7-
class BoolOrNone(contextlib.AbstractContextManager[None]):
7+
class BoolOrNone(AbstractContextManager[None]):
88
def __exit__(
99
self,
1010
__exc_type: type[BaseException] | None,
@@ -18,7 +18,7 @@ def _():
1818
raise Exception
1919
print(1) # reachable
2020

21-
class TrueOrNone(contextlib.AbstractContextManager[None]):
21+
class TrueOrNone(AbstractContextManager[None]):
2222
def __exit__(
2323
self,
2424
__exc_type: type[BaseException] | None,
@@ -33,7 +33,7 @@ def _():
3333
print(1) # reachable
3434

3535

36-
class FalseOrNone(contextlib.AbstractContextManager[None]):
36+
class FalseOrNone(AbstractContextManager[None]):
3737
def __exit__(
3838
self,
3939
__exc_type: type[BaseException] | None,
@@ -48,7 +48,7 @@ def _():
4848
print(1) # unreachable
4949

5050

51-
class OnlyNone(contextlib.AbstractContextManager[None]):
51+
class OnlyNone(AbstractContextManager[None]):
5252
def __exit__(
5353
self,
5454
__exc_type: type[BaseException] | None,
@@ -60,4 +60,15 @@ def __exit__(
6060
def _():
6161
with OnlyNone():
6262
raise Exception
63-
print(1) # unreachable
63+
print(1) # unreachable
64+
65+
66+
@contextmanager
67+
def foo() -> Iterator[None]: ...
68+
69+
70+
with foo():
71+
a = 1
72+
73+
# no reportPossiblyUnboundVariable since _GeneratorContextManager cannot suppress exceptions anymore as of python 3.7
74+
print(a)

packages/pyright-internal/typeshed-fallback/stdlib/contextlib.pyi

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ from _typeshed import FileDescriptorOrPath, Unused
44
from abc import abstractmethod
55
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator
66
from types import TracebackType
7-
from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable
7+
from typing import IO, Any, Generic, Literal, Protocol, TypeVar, overload, runtime_checkable
88
from typing_extensions import ParamSpec, Self, TypeAlias
99

1010
__all__ = [
@@ -67,8 +67,23 @@ class _GeneratorContextManager(AbstractContextManager[_T_co], ContextDecorator):
6767
if sys.version_info >= (3, 9):
6868
def __exit__(
6969
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
70-
) -> bool | None: ...
70+
) -> Literal[False]: ...
71+
elif sys.version_info >= (3, 7):
72+
def __exit__(
73+
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
74+
) -> Literal[False]: ...
7175
else:
76+
# python 3.7 fixes an issue where generators could incorrectly suppress StopIteration exceptions
77+
# (see https://peps.python.org/pep-0479/). while it's still technically possible to craft a generator that
78+
# does, it's an extreme edge case that you need to go out of your way to cause. so we change the return type
79+
# to `Literal[False]` to prevent it from assuming every context manager can suppress exceptions. the only
80+
# reason this isn't an issue in upstream pyright/mypy is because they both "fix" this problem in the
81+
# stupidest way imaginable, by special-casing `bool | None` to mean `Literal[False]` instead.
82+
83+
# the tradeoff with my fix is that python <=3.7 users will experience plenty of annoying errors caused by
84+
# basedpyright being overly strict and assuming variables set inside context managers can be unbound in case
85+
# the exception gets suppressed. i could do some special casing to address this, but python 3.6 is deprecated
86+
# anyway and you should just update to a supported version of python instead.
7287
def __exit__(
7388
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
7489
) -> bool | None: ...

0 commit comments

Comments
 (0)