From 72721768a5c3923dc79b43d5f69902a4426952f8 Mon Sep 17 00:00:00 2001 From: DetachHead Date: Sun, 25 Feb 2024 15:42:21 +1000 Subject: [PATCH] fix `contextmanager` decorator treating all context managers as if they can suppress exceptions when that's no longer the case since python 3.7 --- .../src/tests/samples/withBased.py | 25 +++++++++++++------ .../typeshed-fallback/stdlib/contextlib.pyi | 19 ++++++++++++-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/pyright-internal/src/tests/samples/withBased.py b/packages/pyright-internal/src/tests/samples/withBased.py index 282992e117..a55b0330b4 100644 --- a/packages/pyright-internal/src/tests/samples/withBased.py +++ b/packages/pyright-internal/src/tests/samples/withBased.py @@ -1,10 +1,10 @@ -import contextlib +from contextlib import AbstractContextManager, contextmanager from types import TracebackType -from typing import Iterator, Literal +from typing import Iterator, Literal, Iterator from typing_extensions import assert_never -class BoolOrNone(contextlib.AbstractContextManager[None]): +class BoolOrNone(AbstractContextManager[None]): def __exit__( self, __exc_type: type[BaseException] | None, @@ -18,7 +18,7 @@ def _(): raise Exception print(1) # reachable -class TrueOrNone(contextlib.AbstractContextManager[None]): +class TrueOrNone(AbstractContextManager[None]): def __exit__( self, __exc_type: type[BaseException] | None, @@ -33,7 +33,7 @@ def _(): print(1) # reachable -class FalseOrNone(contextlib.AbstractContextManager[None]): +class FalseOrNone(AbstractContextManager[None]): def __exit__( self, __exc_type: type[BaseException] | None, @@ -48,7 +48,7 @@ def _(): print(1) # unreachable -class OnlyNone(contextlib.AbstractContextManager[None]): +class OnlyNone(AbstractContextManager[None]): def __exit__( self, __exc_type: type[BaseException] | None, @@ -60,4 +60,15 @@ def __exit__( def _(): with OnlyNone(): raise Exception - print(1) # unreachable \ No newline at end of file + print(1) # unreachable + + +@contextmanager +def foo() -> Iterator[None]: ... + + +with foo(): + a = 1 + +# no reportPossiblyUnboundVariable since _GeneratorContextManager cannot suppress exceptions anymore as of python 3.7 +print(a) diff --git a/packages/pyright-internal/typeshed-fallback/stdlib/contextlib.pyi b/packages/pyright-internal/typeshed-fallback/stdlib/contextlib.pyi index eb4e95b335..0e6bedd02f 100644 --- a/packages/pyright-internal/typeshed-fallback/stdlib/contextlib.pyi +++ b/packages/pyright-internal/typeshed-fallback/stdlib/contextlib.pyi @@ -4,7 +4,7 @@ from _typeshed import FileDescriptorOrPath, Unused from abc import abstractmethod from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator from types import TracebackType -from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable +from typing import IO, Any, Generic, Literal, Protocol, TypeVar, overload, runtime_checkable from typing_extensions import ParamSpec, Self, TypeAlias __all__ = [ @@ -67,8 +67,23 @@ class _GeneratorContextManager(AbstractContextManager[_T_co], ContextDecorator): if sys.version_info >= (3, 9): def __exit__( self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> bool | None: ... + ) -> Literal[False]: ... + elif sys.version_info >= (3, 7): + def __exit__( + self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> Literal[False]: ... else: + # python 3.7 fixes an issue where generators could incorrectly suppress StopIteration exceptions + # (see https://peps.python.org/pep-0479/). while it's still technically possible to craft a generator that + # does, it's an extreme edge case that you need to go out of your way to cause. so we change the return type + # to `Literal[False]` to prevent it from assuming every context manager can suppress exceptions. the only + # reason this isn't an issue in upstream pyright/mypy is because they both "fix" this problem in the + # stupidest way imaginable, by special-casing `bool | None` to mean `Literal[False]` instead. + + # the tradeoff with my fix is that python <=3.7 users will experience plenty of annoying errors caused by + # basedpyright being overly strict and assuming variables set inside context managers can be unbound in case + # the exception gets suppressed. i could do some special casing to address this, but python 3.6 is deprecated + # anyway and you should just update to a supported version of python instead. def __exit__( self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None ) -> bool | None: ...