Skip to content

gh-118660: Add second type parameter to (Async)ContextManager #118681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Doc/library/typing.rst

Choose a reason for hiding this comment

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

I would maybe put a little bit more emphasis on the new type parameter being optional. While it defaulting to bool | None implies that, it's maybe a little bit too subtle for user-facing documentation, especially for a new feature in 3.13.

We could also consider changing the notation from Generic[T_co, ExitT_co] to Generic[T_co, ExitT_co = bool | None], so its behavior can be inferred just from this header, similar to function signatures, even if that is not valid syntax, it's similar to PEP-695 notation, while retaining the implicit reference to Generic within the typing module.

Copy link
Member Author

Choose a reason for hiding this comment

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

I considered that syntax with Generic but didn't use it here because it's a SyntaxError. Switching to PEP 695 syntax might be nice but I think it causes problems with Sphinx. In any case, this is something we can adjust after the beta.

Copy link
Member

Choose a reason for hiding this comment

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

yes, we have to use an old version of Sphinx to keep Linux redistributors happy, and old versions of Sphinx don't know about PEP-695 syntax so they do weird things with the syntax highlighting

Original file line number Diff line number Diff line change
Expand Up @@ -3819,28 +3819,44 @@ Aliases to other ABCs in :mod:`collections.abc`
Aliases to :mod:`contextlib` ABCs
"""""""""""""""""""""""""""""""""

.. class:: ContextManager(Generic[T_co])
.. class:: ContextManager(Generic[T_co, ExitT_co])

Deprecated alias to :class:`contextlib.AbstractContextManager`.

The first type parameter, ``T_co``, represents the type returned by
the :meth:`~object.__enter__` method. The optional second type parameter, ``ExitT_co``,
which defaults to ``bool | None``, represents the type returned by the
:meth:`~object.__exit__` method.

.. versionadded:: 3.5.4

.. deprecated:: 3.9
:class:`contextlib.AbstractContextManager`
now supports subscripting (``[]``).
See :pep:`585` and :ref:`types-genericalias`.

.. class:: AsyncContextManager(Generic[T_co])
.. versionchanged:: 3.13
Added the optional second type parameter, ``ExitT_co``.

.. class:: AsyncContextManager(Generic[T_co, AExitT_co])

Deprecated alias to :class:`contextlib.AbstractAsyncContextManager`.

The first type parameter, ``T_co``, represents the type returned by
the :meth:`~object.__aenter__` method. The optional second type parameter, ``AExitT_co``,
which defaults to ``bool | None``, represents the type returned by the
:meth:`~object.__aexit__` method.

.. versionadded:: 3.6.2

.. deprecated:: 3.9
:class:`contextlib.AbstractAsyncContextManager`
now supports subscripting (``[]``).
See :pep:`585` and :ref:`types-genericalias`.

.. versionchanged:: 3.13
Added the optional second type parameter, ``AExitT_co``.

Deprecation Timeline of Major Features
======================================

Expand Down
23 changes: 19 additions & 4 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7486,6 +7486,15 @@ def manager():
self.assertIsInstance(cm, typing.ContextManager)
self.assertNotIsInstance(42, typing.ContextManager)

def test_contextmanager_type_params(self):
cm1 = typing.ContextManager[int]
self.assertEqual(get_args(cm1), (int, bool | None))
cm2 = typing.ContextManager[int, None]
self.assertEqual(get_args(cm2), (int, types.NoneType))

type gen_cm[T1, T2] = typing.ContextManager[T1, T2]
self.assertEqual(get_args(gen_cm.__value__[int, None]), (int, types.NoneType))

def test_async_contextmanager(self):
class NotACM:
pass
Expand All @@ -7497,11 +7506,17 @@ def manager():

cm = manager()
self.assertNotIsInstance(cm, typing.AsyncContextManager)
self.assertEqual(typing.AsyncContextManager[int].__args__, (int,))
self.assertEqual(typing.AsyncContextManager[int].__args__, (int, bool | None))
with self.assertRaises(TypeError):
isinstance(42, typing.AsyncContextManager[int])
with self.assertRaises(TypeError):
typing.AsyncContextManager[int, str]
typing.AsyncContextManager[int, str, float]

def test_asynccontextmanager_type_params(self):
cm1 = typing.AsyncContextManager[int]
self.assertEqual(get_args(cm1), (int, bool | None))
cm2 = typing.AsyncContextManager[int, None]
self.assertEqual(get_args(cm2), (int, types.NoneType))


class TypeTests(BaseTestCase):
Expand Down Expand Up @@ -9928,7 +9943,7 @@ def test_special_attrs(self):
typing.ValuesView: 'ValuesView',
# Subscribed ABC classes
typing.AbstractSet[Any]: 'AbstractSet',
typing.AsyncContextManager[Any]: 'AsyncContextManager',
typing.AsyncContextManager[Any, Any]: 'AsyncContextManager',
typing.AsyncGenerator[Any, Any]: 'AsyncGenerator',
typing.AsyncIterable[Any]: 'AsyncIterable',
typing.AsyncIterator[Any]: 'AsyncIterator',
Expand All @@ -9938,7 +9953,7 @@ def test_special_attrs(self):
typing.ChainMap[Any, Any]: 'ChainMap',
typing.Collection[Any]: 'Collection',
typing.Container[Any]: 'Container',
typing.ContextManager[Any]: 'ContextManager',
typing.ContextManager[Any, Any]: 'ContextManager',
typing.Coroutine[Any, Any, Any]: 'Coroutine',
typing.Counter[Any]: 'Counter',
typing.DefaultDict[Any, Any]: 'DefaultDict',
Expand Down
2 changes: 1 addition & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3764,7 +3764,7 @@ def __getattr__(attr):
obj = _alias(getattr(re, attr), 1)
elif attr in {"ContextManager", "AsyncContextManager"}:
import contextlib
obj = _alias(getattr(contextlib, f"Abstract{attr}"), 1, name=attr)
obj = _alias(getattr(contextlib, f"Abstract{attr}"), 2, name=attr, defaults=(bool | None,))
else:
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
globals()[attr] = obj
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add an optional second type parameter to :class:`typing.ContextManager` and
:class:`typing.AsyncContextManager`, representing the return types of
:meth:`~object.__exit__` and :meth:`~object.__aexit__` respectively.
This parameter defaults to ``bool | None``.
Loading