From 2f00d3e32af8b557040e44c46dee447bb3babee7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Jun 2017 12:47:17 +0200 Subject: [PATCH 1/7] Add AsyncContextManager --- src/test_typing.py | 21 ++++++++++++++++++++- src/typing.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/test_typing.py b/src/test_typing.py index 5c1cc56d..869905af 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -1552,6 +1552,12 @@ def __anext__(self) -> T_a: return data else: raise StopAsyncIteration + +class ACM: + async def __aenter__(self): + return self + async def __aexit__(self, etype, eval, tb): + return None """ if ASYNCIO: @@ -1562,7 +1568,7 @@ def __anext__(self) -> T_a: else: # fake names for the sake of static analysis asyncio = None - AwaitableWrapper = AsyncIteratorWrapper = object + AwaitableWrapper = AsyncIteratorWrapper = ACM = object PY36 = sys.version_info[:2] >= (3, 6) @@ -2165,6 +2171,19 @@ def manager(): self.assertIsInstance(cm, typing.ContextManager) self.assertNotIsInstance(42, typing.ContextManager) + @skipUnless(ASYNCIO, 'Python 3.5 required') + def test_async_contextmanager(self): + class NotACM: + pass + self.assertIsInstance(ACM(), typing.AsyncContextManager) + self.assertNotIsInstance(NotACM(), typing.AsyncContextManager) + @contextlib.contextmanager + def manager(): + yield 42 + + cm = manager() + self.assertNotIsInstance(cm, typing.AsyncContextManager) + class TypeTests(BaseTestCase): diff --git a/src/typing.py b/src/typing.py index 1d7698fb..b2b7c3f3 100644 --- a/src/typing.py +++ b/src/typing.py @@ -59,6 +59,7 @@ # Coroutine, # Collection, # AsyncGenerator, + # AsyncContextManager # Structural checks, a.k.a. protocols. 'Reversible', @@ -1974,6 +1975,34 @@ def __subclasshook__(cls, C): return NotImplemented +if hasattr(contextlib, 'AbstractAsyncContextManager'): + class AsyncContextManager(Generic[T_co], extra=contextlib.AbstractAsyncContextManager): + __slots__ = () + + __all__.append('AsyncContextManager') +elif sys.version_info[:2] >= (3, 5): + exec(""" +class AsyncContextManager(Generic[T_co]): + __slots__ = () + + async def __aenter__(self): + return self + + @abc.abstractmethod + async def __aexit__(self, exc_type, exc_value, traceback): + return None + + @classmethod + def __subclasshook__(cls, C): + if cls is AsyncContextManager: + return _collections_abc._check_methods(C, "__aenter__", + "__aexit__") + return NotImplemented + +__all__.append('AsyncContextManager') +""") + + class Dict(dict, MutableMapping[KT, VT], extra=dict): __slots__ = () From dffa255ca3a440cb31bd9b844392aecd458f825f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Jun 2017 14:23:37 +0200 Subject: [PATCH 2/7] Fix lint and underscore --- src/typing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/typing.py b/src/typing.py index b2b7c3f3..4a8c94bc 100644 --- a/src/typing.py +++ b/src/typing.py @@ -1976,7 +1976,8 @@ def __subclasshook__(cls, C): if hasattr(contextlib, 'AbstractAsyncContextManager'): - class AsyncContextManager(Generic[T_co], extra=contextlib.AbstractAsyncContextManager): + class AsyncContextManager(Generic[T_co], + extra=contextlib.AbstractAsyncContextManager): __slots__ = () __all__.append('AsyncContextManager') @@ -1995,8 +1996,7 @@ async def __aexit__(self, exc_type, exc_value, traceback): @classmethod def __subclasshook__(cls, C): if cls is AsyncContextManager: - return _collections_abc._check_methods(C, "__aenter__", - "__aexit__") + return collections_abc._check_methods(C, "__aenter__", "__aexit__") return NotImplemented __all__.append('AsyncContextManager') From 2cbdb1b07754b6cc3128c47e3e090b1cb76710fd Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Jun 2017 14:29:42 +0200 Subject: [PATCH 3/7] A real fix of import --- src/typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/typing.py b/src/typing.py index 4a8c94bc..ab1db5ae 100644 --- a/src/typing.py +++ b/src/typing.py @@ -10,6 +10,7 @@ import collections.abc as collections_abc except ImportError: import collections as collections_abc # Fallback for PY3.2. +import _collections_abc # Needed for private function _check_methods try: from types import WrapperDescriptorType, MethodWrapperType, MethodDescriptorType except ImportError: @@ -1996,7 +1997,7 @@ async def __aexit__(self, exc_type, exc_value, traceback): @classmethod def __subclasshook__(cls, C): if cls is AsyncContextManager: - return collections_abc._check_methods(C, "__aenter__", "__aexit__") + return _collections_abc._check_methods(C, "__aenter__", "__aexit__") return NotImplemented __all__.append('AsyncContextManager') From cd137de1f220313fc3dc230b48f7284e6022907d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Jun 2017 14:45:11 +0200 Subject: [PATCH 4/7] Only import on newer versions --- src/typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/typing.py b/src/typing.py index ab1db5ae..cbaeb8cf 100644 --- a/src/typing.py +++ b/src/typing.py @@ -10,7 +10,8 @@ import collections.abc as collections_abc except ImportError: import collections as collections_abc # Fallback for PY3.2. -import _collections_abc # Needed for private function _check_methods +if sys.version_info[:2] >= (3, 5): + import _collections_abc # Needed for private function _check_methods # noqa try: from types import WrapperDescriptorType, MethodWrapperType, MethodDescriptorType except ImportError: From d0eea947200ccac2e914c7b0b5182a59152711a3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Jun 2017 15:13:01 +0200 Subject: [PATCH 5/7] Inline _check_methods on Python 3.5 similar to AsyncIterator --- src/typing.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/typing.py b/src/typing.py index cbaeb8cf..49aae0b9 100644 --- a/src/typing.py +++ b/src/typing.py @@ -10,7 +10,7 @@ import collections.abc as collections_abc except ImportError: import collections as collections_abc # Fallback for PY3.2. -if sys.version_info[:2] >= (3, 5): +if sys.version_info[:2] >= (3, 6): import _collections_abc # Needed for private function _check_methods # noqa try: from types import WrapperDescriptorType, MethodWrapperType, MethodDescriptorType @@ -1994,11 +1994,15 @@ async def __aenter__(self): @abc.abstractmethod async def __aexit__(self, exc_type, exc_value, traceback): return None - + @classmethod def __subclasshook__(cls, C): if cls is AsyncContextManager: - return _collections_abc._check_methods(C, "__aenter__", "__aexit__") + if sys.version_info[:2] >= (3, 6): + return _collections_abc._check_methods(C, "__aenter__", "__aexit__") + if (any("__aenter__" in B.__dict__ for B in C.__mro__) and + any("__aexit__" in B.__dict__ for B in C.__mro__)): + return True return NotImplemented __all__.append('AsyncContextManager') From 8b19224d8c805652e4aab96000109d72df386374 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Jun 2017 15:42:03 +0200 Subject: [PATCH 6/7] Fix whitespace --- src/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing.py b/src/typing.py index 49aae0b9..c487afcb 100644 --- a/src/typing.py +++ b/src/typing.py @@ -1994,7 +1994,7 @@ async def __aenter__(self): @abc.abstractmethod async def __aexit__(self, exc_type, exc_value, traceback): return None - + @classmethod def __subclasshook__(cls, C): if cls is AsyncContextManager: From 1283d709b8c99bf09f3c3757e16c66b1080b1544 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Jun 2017 17:16:06 +0200 Subject: [PATCH 7/7] Add a test that shows how to use AsyncContextManager (and two more tests) --- src/test_typing.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 869905af..fd2d93c3 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -1554,8 +1554,8 @@ def __anext__(self) -> T_a: raise StopAsyncIteration class ACM: - async def __aenter__(self): - return self + async def __aenter__(self) -> int: + return 42 async def __aexit__(self, etype, eval, tb): return None """ @@ -1574,6 +1574,7 @@ async def __aexit__(self, etype, eval, tb): PY36_TESTS = """ from test import ann_module, ann_module2, ann_module3 +from typing import AsyncContextManager class A: y: float @@ -1610,6 +1611,16 @@ def __str__(self): return f'{self.x} -> {self.y}' def __add__(self, other): return 0 + +async def g_with(am: AsyncContextManager[int]): + x: int + async with am as x: + return x + +try: + g_with(ACM()).send(None) +except StopIteration as e: + assert e.args[0] == 42 """ if PY36: @@ -2183,6 +2194,11 @@ def manager(): cm = manager() self.assertNotIsInstance(cm, typing.AsyncContextManager) + self.assertEqual(typing.AsyncContextManager[int].__args__, (int,)) + with self.assertRaises(TypeError): + isinstance(42, typing.AsyncContextManager[int]) + with self.assertRaises(TypeError): + typing.AsyncContextManager[int, str] class TypeTests(BaseTestCase):