From 90b105ca341b791500cc12dac5b437ab0abd003e Mon Sep 17 00:00:00 2001 From: Daverball Date: Mon, 20 Nov 2023 23:24:52 +0100 Subject: [PATCH 01/14] Looks at impact of making `ExitStack` generic. See https://discuss.python.org/t/add-an-else-clause-to-with-statements-to-detect-early-termination/38031 for related discussion. --- stdlib/contextlib.pyi | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/stdlib/contextlib.pyi b/stdlib/contextlib.pyi index ce46d0d39830..872528b2a436 100644 --- a/stdlib/contextlib.pyi +++ b/stdlib/contextlib.pyi @@ -31,6 +31,7 @@ if sys.version_info >= (3, 11): _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) _T_io = TypeVar("_T_io", bound=IO[str] | None) +_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None) _F = TypeVar("_F", bound=Callable[..., Any]) _P = ParamSpec("_P") @@ -45,6 +46,16 @@ class AbstractContextManager(Protocol[_T_co]): self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None ) -> bool | None: ... +# this is a version of AbstractContextManager which has a second TypeVar for the return +# type of __exit__ since the return type has an impact on reachability analysis. When +# PEP696 becomes a thing we probably want to merge this with AbstractContextManager and +# provide a default for _ExitT_co instead. +class _AbstractContextManager(Protocol[_T_co, _ExitT_co]): + def __enter__(self) -> _T_co: ... + def __exit__( + self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None + ) -> _ExitT_co: ... + @runtime_checkable class AbstractAsyncContextManager(Protocol[_T_co]): async def __aenter__(self) -> _T_co: ... @@ -53,6 +64,13 @@ class AbstractAsyncContextManager(Protocol[_T_co]): self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None ) -> bool | None: ... +# same thing as with _AbstractContextManager +class _AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): + async def __aenter__(self) -> _T_co: ... + async def __exit__( + self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None + ) -> _ExitT_co: ... + class ContextDecorator: def __call__(self, func: _F) -> _F: ... @@ -141,8 +159,8 @@ class redirect_stderr(_RedirectStream[_T_io]): ... # In reality this is a subclass of `AbstractContextManager`; # see #7961 for why we don't do that in the stub -class ExitStack(metaclass=abc.ABCMeta): - def enter_context(self, cm: AbstractContextManager[_T]) -> _T: ... +class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): + def enter_context(self, cm: _AbstractContextManager[_T, _ExitT_co]) -> _T: ... def push(self, exit: _CM_EF) -> _CM_EF: ... def callback(self, __callback: Callable[_P, _T], *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... def pop_all(self) -> Self: ... @@ -150,7 +168,7 @@ class ExitStack(metaclass=abc.ABCMeta): def __enter__(self) -> Self: ... def __exit__( self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None - ) -> bool: ... + ) -> _ExitT_co: ... _ExitCoroFunc: TypeAlias = Callable[ [type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None] @@ -159,9 +177,9 @@ _ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any] | _ExitCoroF # In reality this is a subclass of `AbstractAsyncContextManager`; # see #7961 for why we don't do that in the stub -class AsyncExitStack(metaclass=abc.ABCMeta): - def enter_context(self, cm: AbstractContextManager[_T]) -> _T: ... - async def enter_async_context(self, cm: AbstractAsyncContextManager[_T]) -> _T: ... +class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): + def enter_context(self, cm: _AbstractContextManager[_T, _ExitT_co]) -> _T: ... + async def enter_async_context(self, cm: _AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ... def push(self, exit: _CM_EF) -> _CM_EF: ... def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ... def callback(self, __callback: Callable[_P, _T], *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... From 8bb852ef2109925e12ed10d5043485dce25f94df Mon Sep 17 00:00:00 2001 From: Daverball Date: Mon, 20 Nov 2023 23:32:52 +0100 Subject: [PATCH 02/14] Fixes `ExitStack` test cases --- test_cases/stdlib/check_contextlib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_cases/stdlib/check_contextlib.py b/test_cases/stdlib/check_contextlib.py index 648661bca856..b39e84ec3ce9 100644 --- a/test_cases/stdlib/check_contextlib.py +++ b/test_cases/stdlib/check_contextlib.py @@ -5,16 +5,16 @@ # See issue #7961 -class Thing(ExitStack): +class Thing(ExitStack[bool | None]): pass -stack = ExitStack() +stack: ExitStack[bool | None] = ExitStack() thing = Thing() assert_type(stack.enter_context(Thing()), Thing) -assert_type(thing.enter_context(ExitStack()), ExitStack) +assert_type(thing.enter_context(ExitStack()), ExitStack[bool | None]) with stack as cm: - assert_type(cm, ExitStack) + assert_type(cm, ExitStack[bool | None]) with thing as cm2: assert_type(cm2, Thing) From 083e471707b41d4bcbf21642d271ef153e5a6c06 Mon Sep 17 00:00:00 2001 From: Daverball Date: Mon, 20 Nov 2023 23:38:24 +0100 Subject: [PATCH 03/14] Fixes use of union syntax in test cases --- test_cases/stdlib/check_contextlib.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test_cases/stdlib/check_contextlib.py b/test_cases/stdlib/check_contextlib.py index b39e84ec3ce9..bf7d687c6fb9 100644 --- a/test_cases/stdlib/check_contextlib.py +++ b/test_cases/stdlib/check_contextlib.py @@ -1,20 +1,21 @@ from __future__ import annotations from contextlib import ExitStack +from typing import Optional from typing_extensions import assert_type # See issue #7961 -class Thing(ExitStack[bool | None]): +class Thing(ExitStack[Optional[bool]]): pass -stack: ExitStack[bool | None] = ExitStack() +stack: ExitStack[Optional[bool]] = ExitStack() thing = Thing() assert_type(stack.enter_context(Thing()), Thing) -assert_type(thing.enter_context(ExitStack()), ExitStack[bool | None]) +assert_type(thing.enter_context(ExitStack()), ExitStack[Optional[bool]]) with stack as cm: - assert_type(cm, ExitStack[bool | None]) + assert_type(cm, ExitStack[Optional[bool]]) with thing as cm2: assert_type(cm2, Thing) From df11be55fb696ab8a867ad49892396f73e6ec094 Mon Sep 17 00:00:00 2001 From: Daverball Date: Mon, 20 Nov 2023 23:44:25 +0100 Subject: [PATCH 04/14] Makes ruff happy. --- test_cases/stdlib/check_contextlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_cases/stdlib/check_contextlib.py b/test_cases/stdlib/check_contextlib.py index bf7d687c6fb9..eb8cca151e40 100644 --- a/test_cases/stdlib/check_contextlib.py +++ b/test_cases/stdlib/check_contextlib.py @@ -10,7 +10,7 @@ class Thing(ExitStack[Optional[bool]]): pass -stack: ExitStack[Optional[bool]] = ExitStack() +stack: ExitStack[bool | None] = ExitStack() thing = Thing() assert_type(stack.enter_context(Thing()), Thing) assert_type(thing.enter_context(ExitStack()), ExitStack[Optional[bool]]) From 70e21b2432aedf6f57cc1ed4bc2819aa6c8268dc Mon Sep 17 00:00:00 2001 From: Daverball Date: Fri, 15 Mar 2024 22:14:52 +0100 Subject: [PATCH 05/14] Let's see how mypy_primer looks after adding a default --- stdlib/contextlib.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/contextlib.pyi b/stdlib/contextlib.pyi index f208b836da7a..3736e50048e5 100644 --- a/stdlib/contextlib.pyi +++ b/stdlib/contextlib.pyi @@ -31,7 +31,7 @@ if sys.version_info >= (3, 11): _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) _T_io = TypeVar("_T_io", bound=IO[str] | None) -_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None) +_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None) _F = TypeVar("_F", bound=Callable[..., Any]) _P = ParamSpec("_P") From afe0f91d82787daa3ae504faff85e1d4cd740fd8 Mon Sep 17 00:00:00 2001 From: Daverball Date: Fri, 15 Mar 2024 22:20:46 +0100 Subject: [PATCH 06/14] Reverts changes to test_cases, since they're no longer necessary We could consider adding new ones, to test the functionality though --- test_cases/stdlib/check_contextlib.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test_cases/stdlib/check_contextlib.py b/test_cases/stdlib/check_contextlib.py index eb8cca151e40..648661bca856 100644 --- a/test_cases/stdlib/check_contextlib.py +++ b/test_cases/stdlib/check_contextlib.py @@ -1,21 +1,20 @@ from __future__ import annotations from contextlib import ExitStack -from typing import Optional from typing_extensions import assert_type # See issue #7961 -class Thing(ExitStack[Optional[bool]]): +class Thing(ExitStack): pass -stack: ExitStack[bool | None] = ExitStack() +stack = ExitStack() thing = Thing() assert_type(stack.enter_context(Thing()), Thing) -assert_type(thing.enter_context(ExitStack()), ExitStack[Optional[bool]]) +assert_type(thing.enter_context(ExitStack()), ExitStack) with stack as cm: - assert_type(cm, ExitStack[Optional[bool]]) + assert_type(cm, ExitStack) with thing as cm2: assert_type(cm2, Thing) From e9b9b7a72e53a266569a924ed92e220cb55e8c9e Mon Sep 17 00:00:00 2001 From: Daverball Date: Fri, 15 Mar 2024 22:40:48 +0100 Subject: [PATCH 07/14] Takes things one step further, to see how this would look --- stdlib/contextlib.pyi | 45 ++++++++++++++----------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/stdlib/contextlib.pyi b/stdlib/contextlib.pyi index 3736e50048e5..be161f6863b4 100644 --- a/stdlib/contextlib.pyi +++ b/stdlib/contextlib.pyi @@ -36,45 +36,28 @@ _F = TypeVar("_F", bound=Callable[..., Any]) _P = ParamSpec("_P") _ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None] -_CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any] | _ExitFunc) +_CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc) @runtime_checkable -class AbstractContextManager(Protocol[_T_co]): +class AbstractContextManager(Protocol[_T_co, _ExitT_co]): def __enter__(self) -> _T_co: ... @abstractmethod def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / ) -> bool | None: ... -# this is a version of AbstractContextManager which has a second TypeVar for the return -# type of __exit__ since the return type has an impact on reachability analysis. When -# PEP696 becomes a thing we probably want to merge this with AbstractContextManager and -# provide a default for _ExitT_co instead. -class _AbstractContextManager(Protocol[_T_co, _ExitT_co]): - def __enter__(self) -> _T_co: ... - def __exit__( - self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / - ) -> _ExitT_co: ... - @runtime_checkable -class AbstractAsyncContextManager(Protocol[_T_co]): +class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): async def __aenter__(self) -> _T_co: ... @abstractmethod async def __aexit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / ) -> bool | None: ... -# same thing as with _AbstractContextManager -class _AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): - async def __aenter__(self) -> _T_co: ... - async def __exit__( - self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / - ) -> _ExitT_co: ... - class ContextDecorator: def __call__(self, func: _F) -> _F: ... -class _GeneratorContextManager(AbstractContextManager[_T_co], ContextDecorator): +class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], ContextDecorator): # __init__ and all instance attributes are actually inherited from _GeneratorContextManagerBase # _GeneratorContextManagerBase is more trouble than it's worth to include in the stub; see #6676 def __init__(self, func: Callable[..., Iterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... @@ -99,7 +82,7 @@ if sys.version_info >= (3, 10): class AsyncContextDecorator: def __call__(self, func: _AF) -> _AF: ... - class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co], AsyncContextDecorator): + class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None], AsyncContextDecorator): # __init__ and these attributes are actually defined in the base class _GeneratorContextManagerBase, # which is more trouble than it's worth to include in the stub (see #6676) def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... @@ -112,7 +95,7 @@ if sys.version_info >= (3, 10): ) -> bool | None: ... else: - class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co]): + class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]): def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... gen: AsyncGenerator[_T_co, Any] func: Callable[..., AsyncGenerator[_T_co, Any]] @@ -129,7 +112,7 @@ class _SupportsClose(Protocol): _SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose) -class closing(AbstractContextManager[_SupportsCloseT]): +class closing(AbstractContextManager[_SupportsCloseT, None]): def __init__(self, thing: _SupportsCloseT) -> None: ... def __exit__(self, *exc_info: Unused) -> None: ... @@ -143,13 +126,13 @@ if sys.version_info >= (3, 10): def __init__(self, thing: _SupportsAcloseT) -> None: ... async def __aexit__(self, *exc_info: Unused) -> None: ... -class suppress(AbstractContextManager[None]): +class suppress(AbstractContextManager[None, bool]): def __init__(self, *exceptions: type[BaseException]) -> None: ... def __exit__( self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None ) -> bool: ... -class _RedirectStream(AbstractContextManager[_T_io]): +class _RedirectStream(AbstractContextManager[_T_io, None]): def __init__(self, new_target: _T_io) -> None: ... def __exit__( self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None @@ -161,7 +144,7 @@ class redirect_stderr(_RedirectStream[_T_io]): ... # In reality this is a subclass of `AbstractContextManager`; # see #7961 for why we don't do that in the stub class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): - def enter_context(self, cm: _AbstractContextManager[_T, _ExitT_co]) -> _T: ... + def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... def push(self, exit: _CM_EF) -> _CM_EF: ... def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... def pop_all(self) -> Self: ... @@ -179,8 +162,8 @@ _ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any] | _ExitCoroF # In reality this is a subclass of `AbstractAsyncContextManager`; # see #7961 for why we don't do that in the stub class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): - def enter_context(self, cm: _AbstractContextManager[_T, _ExitT_co]) -> _T: ... - async def enter_async_context(self, cm: _AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ... + def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... + async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ... def push(self, exit: _CM_EF) -> _CM_EF: ... def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ... def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... @@ -195,7 +178,7 @@ class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): ) -> bool: ... if sys.version_info >= (3, 10): - class nullcontext(AbstractContextManager[_T], AbstractAsyncContextManager[_T]): + class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): enter_result: _T @overload def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... @@ -207,7 +190,7 @@ if sys.version_info >= (3, 10): async def __aexit__(self, *exctype: Unused) -> None: ... else: - class nullcontext(AbstractContextManager[_T]): + class nullcontext(AbstractContextManager[_T, None]): enter_result: _T @overload def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... From 457768f25ec0436e387d4bb8b7c5a86c38d02219 Mon Sep 17 00:00:00 2001 From: Daverball Date: Fri, 15 Mar 2024 22:49:46 +0100 Subject: [PATCH 08/14] Fixes missing type parameters --- stdlib/contextlib.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stdlib/contextlib.pyi b/stdlib/contextlib.pyi index be161f6863b4..4873abad9869 100644 --- a/stdlib/contextlib.pyi +++ b/stdlib/contextlib.pyi @@ -122,7 +122,7 @@ if sys.version_info >= (3, 10): _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) - class aclosing(AbstractAsyncContextManager[_SupportsAcloseT]): + class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]): def __init__(self, thing: _SupportsAcloseT) -> None: ... async def __aexit__(self, *exc_info: Unused) -> None: ... @@ -157,7 +157,7 @@ class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): _ExitCoroFunc: TypeAlias = Callable[ [type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None] ] -_ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any] | _ExitCoroFunc) +_ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any, Any] | _ExitCoroFunc) # In reality this is a subclass of `AbstractAsyncContextManager`; # see #7961 for why we don't do that in the stub @@ -202,7 +202,7 @@ else: if sys.version_info >= (3, 11): _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) - class chdir(AbstractContextManager[None], Generic[_T_fd_or_any_path]): + class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): path: _T_fd_or_any_path def __init__(self, path: _T_fd_or_any_path) -> None: ... def __enter__(self) -> None: ... From 9595dc3aaaefde7d9b02d0611cb50010b972d7eb Mon Sep 17 00:00:00 2001 From: Daverball Date: Fri, 15 Mar 2024 23:02:36 +0100 Subject: [PATCH 09/14] Adds missing param for some obvious cases in stdlib --- stdlib/multiprocessing/synchronize.pyi | 4 ++-- stdlib/os/__init__.pyi | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stdlib/multiprocessing/synchronize.pyi b/stdlib/multiprocessing/synchronize.pyi index 048c6fe8d891..b417925fb17b 100644 --- a/stdlib/multiprocessing/synchronize.pyi +++ b/stdlib/multiprocessing/synchronize.pyi @@ -14,7 +14,7 @@ class Barrier(threading.Barrier): self, parties: int, action: Callable[[], object] | None = None, timeout: float | None = None, *ctx: BaseContext ) -> None: ... -class Condition(AbstractContextManager[bool]): +class Condition(AbstractContextManager[bool, None]): def __init__(self, lock: _LockLike | None = None, *, ctx: BaseContext) -> None: ... def notify(self, n: int = 1) -> None: ... def notify_all(self) -> None: ... @@ -34,7 +34,7 @@ class Event: def wait(self, timeout: float | None = None) -> bool: ... # Not part of public API -class SemLock(AbstractContextManager[bool]): +class SemLock(AbstractContextManager[bool, None]): def acquire(self, block: bool = ..., timeout: float | None = ...) -> bool: ... def release(self) -> None: ... def __exit__( diff --git a/stdlib/os/__init__.pyi b/stdlib/os/__init__.pyi index 89d906d4edfc..5ddf0b2dcaa9 100644 --- a/stdlib/os/__init__.pyi +++ b/stdlib/os/__init__.pyi @@ -794,7 +794,7 @@ def replace( ) -> None: ... def rmdir(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... -class _ScandirIterator(Iterator[DirEntry[AnyStr]], AbstractContextManager[_ScandirIterator[AnyStr]]): +class _ScandirIterator(Iterator[DirEntry[AnyStr]], AbstractContextManager[_ScandirIterator[AnyStr], None]): def __next__(self) -> DirEntry[AnyStr]: ... def __exit__(self, *args: Unused) -> None: ... def close(self) -> None: ... From 0e4280c437d10a01612775dd51d306f17170d813 Mon Sep 17 00:00:00 2001 From: Daverball Date: Thu, 21 Mar 2024 17:54:43 +0100 Subject: [PATCH 10/14] Avoids runtime type errors for `ContextManager`/`AsyncContextManager` --- stdlib/typing.pyi | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index be0c29c89f8d..a103c5059136 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -22,6 +22,7 @@ from types import ( TracebackType, WrapperDescriptorType, ) +from typing import Protocol from typing_extensions import Never as _Never, ParamSpec as _ParamSpec if sys.version_info >= (3, 10): @@ -129,8 +130,8 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 12): __all__ += ["TypeAliasType", "override"] -ContextManager = AbstractContextManager -AsyncContextManager = AbstractAsyncContextManager +class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ... +class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ... # This itself is only available during type checking def type_check_only(func_or_cls: _F) -> _F: ... From c7b422ffcd17eb2618382b84a6be40c8a45d4aca Mon Sep 17 00:00:00 2001 From: Daverball Date: Thu, 21 Mar 2024 18:03:41 +0100 Subject: [PATCH 11/14] Fixes oopsie and adds comment --- stdlib/typing.pyi | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index a103c5059136..a15d057b6e0c 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -22,7 +22,6 @@ from types import ( TracebackType, WrapperDescriptorType, ) -from typing import Protocol from typing_extensions import Never as _Never, ParamSpec as _ParamSpec if sys.version_info >= (3, 10): @@ -130,9 +129,6 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 12): __all__ += ["TypeAliasType", "override"] -class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ... -class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ... - # This itself is only available during type checking def type_check_only(func_or_cls: _F) -> _F: ... @@ -390,6 +386,16 @@ class Hashable(Protocol, metaclass=ABCMeta): @abstractmethod def __hash__(self) -> int: ... +# TODO: Eventually this should be able to take two arguments as well, but this can happen +# no sooner than Python 3.13 +@runtime_checkable +class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co], metaclass=ABCMeta): ... + +# TODO: Eventually this should be able to take two arguments as well, but this can happen +# no sooner than Python 3.13 +@runtime_checkable +class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co], metaclass=ABCMeta): ... + @runtime_checkable class Iterable(Protocol[_T_co]): @abstractmethod From 0ec04a727395ef4d21bee02e252b561f52148002 Mon Sep 17 00:00:00 2001 From: Daverball Date: Thu, 21 Mar 2024 18:21:10 +0100 Subject: [PATCH 12/14] Reverts changes --- stdlib/contextlib.pyi | 4 ++-- stdlib/typing.pyi | 13 +++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/stdlib/contextlib.pyi b/stdlib/contextlib.pyi index 4873abad9869..c5881a9dbd02 100644 --- a/stdlib/contextlib.pyi +++ b/stdlib/contextlib.pyi @@ -44,7 +44,7 @@ class AbstractContextManager(Protocol[_T_co, _ExitT_co]): @abstractmethod def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / - ) -> bool | None: ... + ) -> _ExitT_co: ... @runtime_checkable class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): @@ -52,7 +52,7 @@ class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): @abstractmethod async def __aexit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / - ) -> bool | None: ... + ) -> _ExitT_co: ... class ContextDecorator: def __call__(self, func: _F) -> _F: ... diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index a15d057b6e0c..be0c29c89f8d 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -129,6 +129,9 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 12): __all__ += ["TypeAliasType", "override"] +ContextManager = AbstractContextManager +AsyncContextManager = AbstractAsyncContextManager + # This itself is only available during type checking def type_check_only(func_or_cls: _F) -> _F: ... @@ -386,16 +389,6 @@ class Hashable(Protocol, metaclass=ABCMeta): @abstractmethod def __hash__(self) -> int: ... -# TODO: Eventually this should be able to take two arguments as well, but this can happen -# no sooner than Python 3.13 -@runtime_checkable -class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co], metaclass=ABCMeta): ... - -# TODO: Eventually this should be able to take two arguments as well, but this can happen -# no sooner than Python 3.13 -@runtime_checkable -class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co], metaclass=ABCMeta): ... - @runtime_checkable class Iterable(Protocol[_T_co]): @abstractmethod From d030e802ba047b6eac46f6c756f8e480f2063849 Mon Sep 17 00:00:00 2001 From: Daverball Date: Thu, 21 Mar 2024 18:35:49 +0100 Subject: [PATCH 13/14] Tries to go the type alias route without explicit `TypeAlias` --- stdlib/typing.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index be0c29c89f8d..1c4910734dcb 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -129,8 +129,8 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 12): __all__ += ["TypeAliasType", "override"] -ContextManager = AbstractContextManager -AsyncContextManager = AbstractAsyncContextManager +ContextManager = AbstractContextManager[_T_co, bool | None] # noqa: Y026 +AsyncContextManager = AbstractAsyncContextManager[_T_co, bool | None] # noqa: Y026 # This itself is only available during type checking def type_check_only(func_or_cls: _F) -> _F: ... From ef826648efb733e85d3b30c6be4d963fcf57dfd6 Mon Sep 17 00:00:00 2001 From: Daverball Date: Sat, 23 Mar 2024 16:02:28 +0100 Subject: [PATCH 14/14] Uses a more sane workaround for the `typing` aliases. --- stdlib/typing.pyi | 15 ++++++++++++--- tests/stubtest_allowlists/py3_common.txt | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index 1c4910734dcb..98b0a38cd1a5 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -129,9 +129,6 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 12): __all__ += ["TypeAliasType", "override"] -ContextManager = AbstractContextManager[_T_co, bool | None] # noqa: Y026 -AsyncContextManager = AbstractAsyncContextManager[_T_co, bool | None] # noqa: Y026 - # This itself is only available during type checking def type_check_only(func_or_cls: _F) -> _F: ... @@ -432,6 +429,18 @@ class Generator(Iterator[_YieldT_co], Generic[_YieldT_co, _SendT_contra, _Return @property def gi_yieldfrom(self) -> Generator[Any, Any, Any] | None: ... +# NOTE: Technically we would like this to be able to accept a second parameter as well, just +# like it's counterpart in contextlib, however `typing._SpecialGenericAlias` enforces the +# correct number of arguments at runtime, so we would be hiding runtime errors. +@runtime_checkable +class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ... + +# NOTE: Technically we would like this to be able to accept a second parameter as well, just +# like it's counterpart in contextlib, however `typing._SpecialGenericAlias` enforces the +# correct number of arguments at runtime, so we would be hiding runtime errors. +@runtime_checkable +class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ... + @runtime_checkable class Awaitable(Protocol[_T_co]): @abstractmethod diff --git a/tests/stubtest_allowlists/py3_common.txt b/tests/stubtest_allowlists/py3_common.txt index 03c83f55d718..199d8bf736be 100644 --- a/tests/stubtest_allowlists/py3_common.txt +++ b/tests/stubtest_allowlists/py3_common.txt @@ -533,6 +533,10 @@ typing(_extensions)?\.TextIO\.errors typing(_extensions)?\.TextIO\.line_buffering typing(_extensions)?\.TextIO\.newlines +# These are typing._SpecialGenericAlias at runtime, which is not a real type, but it +# behaves like one in most cases +typing(_extensions)?\.(Async)?ContextManager + types.MethodType.__closure__ # read-only but not actually a property; stubtest thinks it doesn't exist. types.MethodType.__defaults__ # read-only but not actually a property; stubtest thinks it doesn't exist. types.ModuleType.__dict__ # read-only but not actually a property; stubtest thinks it's a mutable attribute.