From 74c5d896c0d644b1b0a0de2d9dfed2e9467a753a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 15 Mar 2021 15:50:34 +0200 Subject: [PATCH 01/12] WIP async methods. --- src/lazy_object_proxy/cext.c | 144 ++++++++++++++++++++++++++++++-- src/lazy_object_proxy/simple.py | 15 ++++ src/lazy_object_proxy/slots.py | 15 ++++ 3 files changed, 165 insertions(+), 9 deletions(-) diff --git a/src/lazy_object_proxy/cext.c b/src/lazy_object_proxy/cext.c index 50d7786..ca3fc1a 100644 --- a/src/lazy_object_proxy/cext.c +++ b/src/lazy_object_proxy/cext.c @@ -457,8 +457,7 @@ static PyObject *Proxy_oct(ProxyObject *self) if ((nb = self->wrapped->ob_type->tp_as_number) == NULL || nb->nb_oct == NULL) { - PyErr_SetString(PyExc_TypeError, - "oct() argument can't be converted to oct"); + PyErr_SetString(PyExc_TypeError, "oct() argument can't be converted to oct"); return NULL; } @@ -477,8 +476,7 @@ static PyObject *Proxy_hex(ProxyObject *self) if ((nb = self->wrapped->ob_type->tp_as_number) == NULL || nb->nb_hex == NULL) { - PyErr_SetString(PyExc_TypeError, - "hex() argument can't be converted to hex"); + PyErr_SetString(PyExc_TypeError, "hex() argument can't be converted to hex"); return NULL; } @@ -854,8 +852,7 @@ static PyObject *Proxy_dir( /* ------------------------------------------------------------------------- */ -static PyObject *Proxy_enter( - ProxyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Proxy_enter(ProxyObject *self) { PyObject *method = NULL; PyObject *result = NULL; @@ -867,7 +864,7 @@ static PyObject *Proxy_enter( if (!method) return NULL; - result = PyObject_Call(method, args, kwds); + result = PyObject_CallObject(method, NULL); Py_DECREF(method); @@ -1227,6 +1224,121 @@ static PyObject *Proxy_call( /* ------------------------------------------------------------------------- */; +#if PY_MAJOR_VERSION >= 3 + +static PyObject *Proxy_aenter(ProxyObject *self) +{ + PyObject *method = NULL; + PyObject *result = NULL; + + Proxy__ENSURE_WRAPPED_OR_RETURN_NULL(self); + + method = PyObject_GetAttrString(self->wrapped, "__aenter__"); + + if (!method) + return NULL; + + result = PyObject_CallObject(method, NULL); + + Py_DECREF(method); + + return result; +} + +/* ------------------------------------------------------------------------- */ + +static PyObject *Proxy_aexit( + ProxyObject *self, PyObject *args, PyObject *kwds) +{ + PyObject *method = NULL; + PyObject *result = NULL; + + Proxy__ENSURE_WRAPPED_OR_RETURN_NULL(self); + + method = PyObject_GetAttrString(self->wrapped, "__aexit__"); + + if (!method) + return NULL; + + result = PyObject_Call(method, args, kwds); + + Py_DECREF(method); + + return result; +} + +/* ------------------------------------------------------------------------- */ + +static PyObject *Proxy_await(ProxyObject *self) +{ + Proxy__ENSURE_WRAPPED_OR_RETURN_NULL(self); + + unaryfunc meth = NULL; + PyObject *wrapped = self->wrapped; + PyTypeObject *type = Py_TYPE(wrapped); + + + if (type->tp_as_async != NULL) { + meth = type->tp_as_async->am_await; + } + + if (meth != NULL) { + return (*meth)(wrapped); + } + + PyErr_Format(PyExc_TypeError, " %.100s is missing the __await__ method", type->tp_name); + return NULL; +} + +/* ------------------------------------------------------------------------- */; + +static PyObject *Proxy_aiter(ProxyObject *self) +{ + Proxy__ENSURE_WRAPPED_OR_RETURN_NULL(self); + + unaryfunc meth = NULL; + PyObject *wrapped = self->wrapped; + PyTypeObject *type = Py_TYPE(wrapped); + + if (type->tp_as_async != NULL) { + meth = type->tp_as_async->am_aiter; + } + + if (meth != NULL) { + return (*meth)(wrapped); + } + + PyErr_Format(PyExc_TypeError, " %.100s is missing the __aiter__ method", type->tp_name); + return NULL; +} + +/* ------------------------------------------------------------------------- */; + +static PyObject *Proxy_anext(ProxyObject *self) +{ + Proxy__ENSURE_WRAPPED_OR_RETURN_NULL(self); + + + unaryfunc meth = NULL; + PyObject *wrapped = self->wrapped; + PyTypeObject *type = Py_TYPE(wrapped); + + if (type->tp_as_async != NULL) { + meth = type->tp_as_async->am_anext; + } + + if (meth != NULL) { + return (*meth)(wrapped); + } + + PyErr_Format(PyExc_TypeError, " %.100s is missing the __anext__ method", type->tp_name); + return NULL; +} + +#endif + +/* ------------------------------------------------------------------------- */; + static PyNumberMethods Proxy_as_number = { (binaryfunc)Proxy_add, /*nb_add*/ (binaryfunc)Proxy_subtract, /*nb_subtract*/ @@ -1299,10 +1411,17 @@ static PyMappingMethods Proxy_as_mapping = { (objobjargproc)Proxy_setitem, /*mp_ass_subscript*/ }; +#if PY_MAJOR_VERSION >= 3 +static PyAsyncMethods Proxy_as_async = { + (unaryfunc)Proxy_await, /* am_await */ + (unaryfunc)Proxy_aiter, /* am_aiter */ + (unaryfunc)Proxy_anext, /* am_anext */ +}; +#endif + static PyMethodDef Proxy_methods[] = { { "__dir__", (PyCFunction)Proxy_dir, METH_NOARGS, 0 }, - { "__enter__", (PyCFunction)Proxy_enter, - METH_VARARGS | METH_KEYWORDS, 0 }, + { "__enter__", (PyCFunction)Proxy_enter, METH_NOARGS, 0 }, { "__exit__", (PyCFunction)Proxy_exit, METH_VARARGS | METH_KEYWORDS, 0 }, { "__getattr__", (PyCFunction)Proxy_getattr, @@ -1314,6 +1433,9 @@ static PyMethodDef Proxy_methods[] = { { "__fspath__", (PyCFunction)Proxy_fspath, METH_NOARGS, 0 }, #if PY_MAJOR_VERSION >= 3 { "__round__", (PyCFunction)Proxy_round, METH_NOARGS, 0 }, + { "__aenter__", (PyCFunction)Proxy_aenter, METH_NOARGS, 0 }, + { "__aexit__", (PyCFunction)Proxy_aexit, + METH_VARARGS | METH_KEYWORDS, 0 }, #endif { NULL, NULL }, }; @@ -1348,7 +1470,11 @@ PyTypeObject Proxy_Type = { 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ +#if PY_MAJOR_VERSION >= 3 + &Proxy_as_async, /* tp_as_async */ +#else 0, /*tp_compare*/ +#endif (unaryfunc)Proxy_repr, /*tp_repr*/ &Proxy_as_number, /*tp_as_number*/ &Proxy_as_sequence, /*tp_as_sequence*/ diff --git a/src/lazy_object_proxy/simple.py b/src/lazy_object_proxy/simple.py index 92e355a..5b3650b 100644 --- a/src/lazy_object_proxy/simple.py +++ b/src/lazy_object_proxy/simple.py @@ -256,3 +256,18 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) + + def __aiter__(self): + return self.__wrapped__.__aiter__() + + async def __anext__(self): + return await self.__wrapped__.__anext__() + + def __await__(self): + return await self.__wrapped__ + + async def __aenter__(self): + return await self.__wrapped__.__aenter__() + + async def __aexit__(self, *args, **kwargs): + return await self.__wrapped__.__aexit__(*args, **kwargs) diff --git a/src/lazy_object_proxy/slots.py b/src/lazy_object_proxy/slots.py index 38668b8..b96f54e 100644 --- a/src/lazy_object_proxy/slots.py +++ b/src/lazy_object_proxy/slots.py @@ -424,3 +424,18 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) + + def __aiter__(self): + return self.__wrapped__.__aiter__() + + def __await__(self): + return self.__wrapped__.__await__() + + async def __anext__(self): + return await self.__wrapped__.__anext__() + + async def __aenter__(self): + return await self.__wrapped__.__aenter__() + + async def __aexit__(self, *args, **kwargs): + return await self.__wrapped__.__aexit__(*args, **kwargs) From 3cfb0e81c3bff955ef4aedec65d8ab82e4c6bc10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 15 Mar 2021 19:14:41 +0200 Subject: [PATCH 02/12] Seems there's something borken with easy_install as of late, but I don't care why. --- .appveyor.yml | 1 - .travis.yml | 1 - ci/templates/.appveyor.yml | 1 - ci/templates/.travis.yml | 1 - 4 files changed, 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 707f762..74794a9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -130,7 +130,6 @@ init: install: - '%PYTHON_HOME%\python -mpip install --progress-bar=off -rci/requirements.txt' - '%PYTHON_HOME%\Scripts\virtualenv --version' - - '%PYTHON_HOME%\Scripts\easy_install --version' - '%PYTHON_HOME%\Scripts\pip --version' - '%PYTHON_HOME%\Scripts\tox --version' test_script: diff --git a/.travis.yml b/.travis.yml index db4d50d..dc78251 100644 --- a/.travis.yml +++ b/.travis.yml @@ -137,7 +137,6 @@ before_install: install: - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - - easy_install --version - pip --version - tox --version script: diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml index 63318e0..994932c 100644 --- a/ci/templates/.appveyor.yml +++ b/ci/templates/.appveyor.yml @@ -40,7 +40,6 @@ init: install: - '%PYTHON_HOME%\python -mpip install --progress-bar=off -rci/requirements.txt' - '%PYTHON_HOME%\Scripts\virtualenv --version' - - '%PYTHON_HOME%\Scripts\easy_install --version' - '%PYTHON_HOME%\Scripts\pip --version' - '%PYTHON_HOME%\Scripts\tox --version' test_script: diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index e3f5d9f..85506a0 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -60,7 +60,6 @@ before_install: install: - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - - easy_install --version - pip --version - tox --version script: From 6b2d476a619eb1cf7fc0f3918ce6b3068b3fc447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 16 Mar 2021 20:30:02 +0200 Subject: [PATCH 03/12] Boatload of trashy stuff. --- setup.cfg | 2 +- src/lazy_object_proxy/cext.c | 6 +- src/lazy_object_proxy/simple.py | 17 +- src/lazy_object_proxy/slots.py | 30 +- tests/conftest.py | 67 ++ tests/test_async.py | 1768 +++++++++++++++++++++++++++++++ tests/test_lazy_object_proxy.py | 69 +- tox.ini | 1 + 8 files changed, 1873 insertions(+), 87 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_async.py diff --git a/setup.cfg b/setup.cfg index e1c978d..d55819a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ markers = xfail_simple: Expected test to fail on the `simple` implementation. addopts = -ra - --strict + --strict-markers --ignore=docs/conf.py --ignore=setup.py --ignore=ci diff --git a/src/lazy_object_proxy/cext.c b/src/lazy_object_proxy/cext.c index ca3fc1a..dcf71f7 100644 --- a/src/lazy_object_proxy/cext.c +++ b/src/lazy_object_proxy/cext.c @@ -1286,7 +1286,7 @@ static PyObject *Proxy_await(ProxyObject *self) return (*meth)(wrapped); } - PyErr_Format(PyExc_TypeError, " %.100s is missing the __await__ method", type->tp_name); + PyErr_Format(PyExc_TypeError, "%.100s is missing the __await__ method", type->tp_name); return NULL; } @@ -1308,7 +1308,7 @@ static PyObject *Proxy_aiter(ProxyObject *self) return (*meth)(wrapped); } - PyErr_Format(PyExc_TypeError, " %.100s is missing the __aiter__ method", type->tp_name); + PyErr_Format(PyExc_TypeError, "%.100s is missing the __aiter__ method", type->tp_name); return NULL; } @@ -1331,7 +1331,7 @@ static PyObject *Proxy_anext(ProxyObject *self) return (*meth)(wrapped); } - PyErr_Format(PyExc_TypeError, " %.100s is missing the __anext__ method", type->tp_name); + PyErr_Format(PyExc_TypeError, "%.100s is missing the __anext__ method", type->tp_name); return NULL; } diff --git a/src/lazy_object_proxy/simple.py b/src/lazy_object_proxy/simple.py index 5b3650b..df0197e 100644 --- a/src/lazy_object_proxy/simple.py +++ b/src/lazy_object_proxy/simple.py @@ -260,14 +260,17 @@ def __reduce_ex__(self, protocol): def __aiter__(self): return self.__wrapped__.__aiter__() - async def __anext__(self): - return await self.__wrapped__.__anext__() + def __anext__(self): + return self.__wrapped__.__anext__() def __await__(self): - return await self.__wrapped__ + if hasattr(self.__wrapped__, '__await__'): + return self.__wrapped__.__await__() + else: + return iter(self.__wrapped__) - async def __aenter__(self): - return await self.__wrapped__.__aenter__() + def __aenter__(self): + return self.__wrapped__.__aenter__() - async def __aexit__(self, *args, **kwargs): - return await self.__wrapped__.__aexit__(*args, **kwargs) + def __aexit__(self, *args, **kwargs): + return self.__wrapped__.__aexit__(*args, **kwargs) diff --git a/src/lazy_object_proxy/slots.py b/src/lazy_object_proxy/slots.py index b96f54e..98fe98b 100644 --- a/src/lazy_object_proxy/slots.py +++ b/src/lazy_object_proxy/slots.py @@ -1,4 +1,5 @@ import operator +from types import GeneratorType, CoroutineType from .compat import PY2 from .compat import PY3 @@ -414,7 +415,14 @@ def __exit__(self, *args, **kwargs): return self.__wrapped__.__exit__(*args, **kwargs) def __iter__(self): - return iter(self.__wrapped__) + if hasattr(self.__wrapped__, '__await__'): + return self.__wrapped__.__await__() + else: + # raise TypeError("'coroutine' object is not iterable") + return iter(self.__wrapped__) + + def __next__(self): + return next(self.__wrapped__) def __call__(self, *args, **kwargs): return self.__wrapped__(*args, **kwargs) @@ -426,16 +434,20 @@ def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) def __aiter__(self): - return self.__wrapped__.__aiter__() - - def __await__(self): - return self.__wrapped__.__await__() + return self async def __anext__(self): return await self.__wrapped__.__anext__() - async def __aenter__(self): - return await self.__wrapped__.__aenter__() + def __await__(self): + if hasattr(self.__wrapped__, '__await__'): + return self.__wrapped__.__await__() + else: + return (yield from self.__wrapped__) + + + def __aenter__(self): + return self.__wrapped__.__aenter__() - async def __aexit__(self, *args, **kwargs): - return await self.__wrapped__.__aexit__(*args, **kwargs) + def __aexit__(self, *args, **kwargs): + return self.__wrapped__.__aexit__(*args, **kwargs) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fb6ae63 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,67 @@ +import pytest + +@pytest.fixture(scope="session") +def lop_loader(): + def load_implementation(name): + class FakeModule: + subclass = False + kind = name + if name == "slots": + from lazy_object_proxy.slots import Proxy + elif name == "simple": + from lazy_object_proxy.simple import Proxy + elif name == "cext": + try: + from lazy_object_proxy.cext import Proxy + except ImportError: + if PYPY: + pytest.skip(msg="C Extension not available.") + else: + raise + elif name == "objproxies": + Proxy = pytest.importorskip("objproxies").LazyProxy + elif name == "django": + Proxy = pytest.importorskip("django.utils.functional").SimpleLazyObject + else: + raise RuntimeError("Unsupported param: %r." % name) + + Proxy + + return FakeModule + return load_implementation + +@pytest.fixture(scope="session", params=[ + "slots", "cext", + "simple", + # "external-django", "external-objproxies" +]) +def lop_implementation(request, lop_loader): + return lop_loader(request.param) + + +@pytest.fixture(scope="session", params=[True, False], ids=['subclassed', 'normal']) +def lop_subclass(request, lop_implementation): + if request.param: + class submod(lop_implementation): + subclass = True + Proxy = type("SubclassOf_" + lop_implementation.Proxy.__name__, + (lop_implementation.Proxy,), {}) + + return submod + else: + return lop_implementation + + +@pytest.fixture(scope="function") +def lop(request, lop_subclass): + if request.node.get_closest_marker('xfail_subclass'): + request.applymarker(pytest.mark.xfail( + reason="This test can't work because subclassing disables certain " + "features like __doc__ and __module__ proxying." + )) + if request.node.get_closest_marker('xfail_simple'): + request.applymarker(pytest.mark.xfail( + reason="The lazy_object_proxy.simple.Proxy has some limitations." + )) + + return lop_subclass diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100644 index 0000000..ba49bf0 --- /dev/null +++ b/tests/test_async.py @@ -0,0 +1,1768 @@ +import copy +import inspect +import pickle +import re +import sys +import types +import warnings +from test import support + +import pytest + + +class AsyncYieldFrom: + def __init__(self, obj): + self.obj = obj + + def __await__(self): + yield from self.obj + + +class AsyncYield: + def __init__(self, value): + self.value = value + + def __await__(self): + yield self.value + + +def run_async(coro): + assert coro.__class__ in {types.GeneratorType, types.CoroutineType} + + buffer = [] + result = None + while True: + try: + buffer.append(coro.send(None)) + except StopIteration as ex: + result = ex.args[0] if ex.args else None + break + return buffer, result + + +def run_async__await__(coro): + assert coro.__class__ is types.CoroutineType + aw = coro.__await__() + buffer = [] + result = None + i = 0 + while True: + try: + if i % 2: + buffer.append(next(aw)) + else: + buffer.append(aw.send(None)) + i += 1 + except StopIteration as ex: + result = ex.args[0] if ex.args else None + break + return buffer, result + + +async def proxy(ob): # workaround + return await ob + + +def test_gen_1(lop): + def gen(): yield + + assert not hasattr(gen, '__await__') + + +def test_func_1(lop): + async def foo(): + return 10 + + f = lop.Proxy(foo) + assert isinstance(f, types.CoroutineType) + assert bool(foo.__code__.co_flags & inspect.CO_COROUTINE) + assert not bool(foo.__code__.co_flags & inspect.CO_GENERATOR) + assert bool(f.cr_code.co_flags & inspect.CO_COROUTINE) + assert not bool(f.cr_code.co_flags & inspect.CO_GENERATOR) + assert run_async(f) == ([], 10) + + assert run_async__await__(foo()) == ([], 10) + + def bar(): pass + + assert not bool(bar.__code__.co_flags & inspect.CO_COROUTINE) + + +def test_func_2(lop): + async def foo(): + raise StopIteration + + with pytest.raises(RuntimeError, match="coroutine raised StopIteration"): + run_async(lop.Proxy(foo)) + + +def test_func_3(lop): + async def foo(): + raise StopIteration + + coro = lop.Proxy(foo) + assert re.search('^$', str(coro)) + coro.close() + + +def test_func_4(lop): + async def foo(): + raise StopIteration + + coro = lop.Proxy(foo) + + check = lambda: pytest.raises(TypeError, match="'coroutine' object is not iterable") + + with check(): + import hunter + with hunter.trace(): list(coro) + + with check(): + tuple(coro) + + with check(): + sum(coro) + + with check(): + iter(coro) + + with check(): + for i in coro: + pass + + with check(): + [i for i in coro] + + coro.close() + + +def test_func_5(lop): + @types.coroutine + def bar(): + yield 1 + + async def foo(): + await lop.Proxy(bar) + + check = lambda: pytest.raises(TypeError, match="'coroutine' object is not iterable") + + coro = lop.Proxy(foo) + with check(): + for el in coro: + pass + coro.close() + + # the following should pass without an error + for el in lop.Proxy(bar): + assert el == 1 + assert [el for el in lop.Proxy(bar)] == [1] + assert tuple(lop.Proxy(bar)) == (1,) + assert next(iter(lop.Proxy(bar))) == 1 + + +def test_func_6(lop): + @types.coroutine + def bar(): + yield 1 + yield 2 + + async def foo(): + await proxy(lop.Proxy(bar)) + + import dis + dis.dis(foo) + + f = lop.Proxy(foo) + assert f.send(None) == 1 + assert f.send(None) == 2 + with pytest.raises(StopIteration): + f.send(None) + + +def test_func_7(lop): + async def bar(): + return 10 + + coro = lop.Proxy(bar) + + def foo(): + yield from coro + + with pytest.raises( + TypeError, + match="'coroutine' object is not iterable", + # looks like python has some special error rewrapping?! + # match="cannot 'yield from' a coroutine object in " + # "a non-coroutine generator" + ): + list(lop.Proxy(foo)) + + coro.close() + + +def test_func_8(lop): + @types.coroutine + def bar(): + val = (yield from coro) + print(val) + return val + + async def foo(): + return 'spam' + + coro = lop.Proxy(foo) + # coro = lop.Proxy(foo) + assert run_async(lop.Proxy(bar)) == ([], 'spam') + coro.close() + + +def test_func_10(lop): + N = 0 + + @types.coroutine + def gen(): + nonlocal N + try: + a = yield + yield (a ** 2) + except ZeroDivisionError: + N += 100 + raise + finally: + N += 1 + + async def foo(): + await lop.Proxy(gen) + + coro = lop.Proxy(foo) + aw = coro.__await__() + assert aw is iter(aw) + next(aw) + assert aw.send(10) == 100 + + assert N == 0 + aw.close() + assert N == 1 + + coro = foo() + aw = coro.__await__() + next(aw) + with pytest.raises(ZeroDivisionError): + aw.throw(ZeroDivisionError, None, None) + assert N == 102 + + +def test_func_11(lop): + async def func(): pass + + coro = lop.Proxy(func) + # Test that PyCoro_Type and _PyCoroWrapper_Type types were properly + # initialized + assert '__await__' in dir(coro) + assert '__iter__' in dir(coro.__await__()) + assert 'coroutine_wrapper' in repr(coro.__await__()) + coro.close() # avoid RuntimeWarning + + +def test_func_12(lop): + async def g(): + i = me.send(None) + await foo + + me = lop.Proxy(g) + with pytest.raises(ValueError, match="coroutine already executing"): + me.send(None) + + +def test_func_13(lop): + async def g(): + pass + + coro = lop.Proxy(g) + with pytest.raises(TypeError, match="can't send non-None value to a just-started coroutine"): + coro.send('spam') + + coro.close() + + +def test_func_14(lop): + @types.coroutine + def gen(): + yield + + async def coro(): + try: + await lop.Proxy(gen) + except GeneratorExit: + await lop.Proxy(gen) + + c = lop.Proxy(coro) + c.send(None) + with pytest.raises(RuntimeError, match="coroutine ignored GeneratorExit"): + c.close() + + +def test_func_15(lop): + # See http://bugs.python.org/issue25887 for details + + async def spammer(): + return 'spam' + + async def reader(coro): + return await coro + + spammer_coro = lop.Proxy(spammer) + + with pytest.raises(StopIteration, match='spam'): + reader(spammer_coro).send(None) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + reader(spammer_coro).send(None) + + +def test_func_16(lop): + # See http://bugs.python.org/issue25887 for details + + @types.coroutine + def nop(): + yield + + async def send(): + await nop() + return 'spam' + + async def read(coro): + await nop() + return await coro + + spammer = lop.Proxy(send) + + reader = lop.Proxy(lambda: read(spammer)) + reader.send(None) + reader.send(None) + with pytest.raises(Exception, match='ham'): + reader.throw(Exception('ham')) + + reader = read(spammer) + reader.send(None) + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + reader.send(None) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + reader.throw(Exception('wat')) + + +def test_func_17(lop): + # See http://bugs.python.org/issue25887 for details + + async def coroutine(): + return 'spam' + + coro = lop.Proxy(coroutine) + with pytest.raises(StopIteration, match='spam'): + coro.send(None) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + coro.send(None) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + coro.throw(Exception('wat')) + + # Closing a coroutine shouldn't raise any exception even if it's + # already closed/exhausted (similar to generators) + coro.close() + coro.close() + + +def test_func_18(lop): + # See http://bugs.python.org/issue25887 for details + + async def coroutine(): + return 'spam' + + coro = lop.Proxy(coroutine) + await_iter = coro.__await__() + it = iter(await_iter) + + with pytest.raises(StopIteration, match='spam'): + it.send(None) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + it.send(None) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + # Although the iterator protocol requires iterators to + # raise another StopIteration here, we don't want to do + # that. In this particular case, the iterator will raise + # a RuntimeError, so that 'yield from' and 'await' + # expressions will trigger the error, instead of silently + # ignoring the call. + next(it) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + it.throw(Exception('wat')) + + with pytest.raises(RuntimeError, match='cannot reuse already awaited coroutine'): + it.throw(Exception('wat')) + + # Closing a coroutine shouldn't raise any exception even if it's + # already closed/exhausted (similar to generators) + it.close() + it.close() + + +def test_func_19(lop): + CHK = 0 + + @types.coroutine + def foo(): + nonlocal CHK + yield + try: + yield + except GeneratorExit: + CHK += 1 + + async def coroutine(): + await foo() + + coro = lop.Proxy(coroutine) + + coro.send(None) + coro.send(None) + + assert CHK == 0 + coro.close() + assert CHK == 1 + + for _ in range(3): + # Closing a coroutine shouldn't raise any exception even if it's + # already closed/exhausted (similar to generators) + coro.close() + assert CHK == 1 + + +def test_coro_wrapper_send_tuple(lop): + async def foo(): + return (10,) + + result = run_async__await__(lop.Proxy(foo)) + assert result == ([], (10,)) + + +def test_coro_wrapper_send_stop_iterator(lop): + async def foo(): + return StopIteration(10) + + result = run_async__await__(lop.Proxy(foo)) + assert isinstance(result[1], StopIteration) + assert result[1].value == 10 + + +def test_cr_await(lop): + @types.coroutine + def a(): + assert inspect.getcoroutinestate(coro_b) == inspect.CORO_RUNNING + assert coro_b.cr_await is None + yield + assert inspect.getcoroutinestate(coro_b) == inspect.CORO_RUNNING + assert coro_b.cr_await is None + + async def c(): + await lop.Proxy(a) + + async def b(): + assert coro_b.cr_await is None + await lop.Proxy(c) + assert coro_b.cr_await is None + + coro_b = lop.Proxy(b) + assert inspect.getcoroutinestate(coro_b) == inspect.CORO_CREATED + assert coro_b.cr_await is None + + coro_b.send(None) + assert inspect.getcoroutinestate(coro_b) == inspect.CORO_SUSPENDED + assert coro_b.cr_await.cr_await.gi_code.co_name == 'a' + + with pytest.raises(StopIteration): + coro_b.send(None) # complete coroutine + assert inspect.getcoroutinestate(coro_b) == inspect.CORO_CLOSED + assert coro_b.cr_await is None + + +def test_await_1(lop): + async def foo(): + await 1 + + with pytest.raises(TypeError, match="object int can.t.*await"): + run_async(lop.Proxy(foo)) + + +def test_await_2(lop): + async def foo(): + await [] + + with pytest.raises(TypeError, match="object list can.t.*await"): + run_async(lop.Proxy(foo)) + + +def test_await_3(lop): + async def foo(): + await AsyncYieldFrom([1, 2, 3]) + + assert run_async(lop.Proxy(foo)) == ([1, 2, 3], None) + assert run_async__await__(lop.Proxy(foo)) == ([1, 2, 3], None) + + +def test_await_4(lop): + async def bar(): + return 42 + + async def foo(): + return await lop.Proxy(bar) + + assert run_async(lop.Proxy(foo)) == ([], 42) + + +def test_await_5(lop): + class Awaitable: + def __await__(self): + return + + async def foo(): + return (await lop.Proxy(Awaitable)) + + with pytest.raises(TypeError, match="__await__.*returned non-iterator of type"): + run_async(lop.Proxy(foo)) + + +def test_await_6(lop): + class Awaitable: + def __await__(self): + return iter([52]) + + async def foo(): + return (await lop.Proxy(Awaitable)) + + assert run_async(lop.Proxy(foo)) == ([52], None) + + +def test_await_7(lop): + class Awaitable: + def __await__(self): + yield 42 + return 100 + + async def foo(): + return (await lop.Proxy(Awaitable)) + + assert run_async(lop.Proxy(foo)) == ([42], 100) + + +def test_await_8(lop): + class Awaitable: + pass + + async def foo(): return await lop.Proxy(Awaitable) + + with pytest.raises(TypeError, match="object Awaitable can't be used in 'await' expression"): + run_async(lop.Proxy(foo)) + + +def test_await_9(lop): + def wrap(): + return bar + + async def bar(): + return 42 + + async def foo(): + db = {'b': lambda: wrap} + + class DB: + b = wrap + + return (await lop.Proxy(bar) + await lop.Proxy(wrap)() + await lop.Proxy(lambda: db['b']()()()) + + await lop.Proxy(bar) * 1000 + await DB.b()()) + + async def foo2(): + return -await lop.Proxy(bar) + + assert run_async(lop.Proxy(foo)) == ([], 42168) + assert run_async(lop.Proxy(foo2)) == ([], -42) + + +def test_await_10(lop): + async def baz(): + return 42 + + async def bar(): + return lop.Proxy(baz) + + async def foo(): + return await (await lop.Proxy(bar)) + + assert run_async(lop.Proxy(foo)) == ([], 42) + + +def test_await_11(lop): + def ident(val): + return val + + async def bar(): + return 'spam' + + async def foo(): + return ident(val=await lop.Proxy(bar)) + + async def foo2(): + return await lop.Proxy(bar), 'ham' + + assert run_async(lop.Proxy(foo2)) == ([], ('spam', 'ham')) + + +def test_await_12(lop): + async def coro(): + return 'spam' + + c = coro() + + class Awaitable: + def __await__(self): + return c + + async def foo(): + return await lop.Proxy(Awaitable) + + with pytest.raises(TypeError, match=r"__await__\(\) returned a coroutine"): + run_async(lop.Proxy(foo)) + + c.close() + + +def test_await_13(lop): + class Awaitable: + def __await__(self): + return self + + async def foo(): + return await lop.Proxy(Awaitable) + + with pytest.raises(TypeError, match="__await__.*returned non-iterator of type"): + run_async(lop.Proxy(foo)) + + +def test_await_14(lop): + class Wrapper: + # Forces the interpreter to use CoroutineType.__await__ + def __init__(self, coro): + assert coro.__class__ is types.CoroutineType + self.coro = coro + + def __await__(self): + return self.coro.__await__() + + class FutureLike: + def __await__(self): + return (yield) + + class Marker(Exception): + pass + + async def coro1(): + try: + return await lop.Proxy(FutureLike) + except ZeroDivisionError: + raise Marker + + async def coro2(): + return await lop.Proxy(lambda: Wrapper(lop.Proxy(coro1))) + + c = lop.Proxy(coro2) + c.send(None) + with pytest.raises(StopIteration, match='spam'): + c.send('spam') + + c = lop.Proxy(coro2) + c.send(None) + with pytest.raises(Marker): + c.throw(ZeroDivisionError) + + +def test_await_15(lop): + @types.coroutine + def nop(): + yield + + async def coroutine(): + await nop() + + async def waiter(coro): + await coro + + coro = lop.Proxy(coroutine) + coro.send(None) + + with pytest.raises(RuntimeError, match="coroutine is being awaited already"): + waiter(coro).send(None) + + +def test_await_16(lop): + # See https://bugs.python.org/issue29600 for details. + + async def f(): + return ValueError() + + async def g(): + try: + raise KeyError + except: + return await lop.Proxy(f) + + _, result = run_async(lop.Proxy(g)) + assert result.__context__ is None + + +def test_with_1(lop): + class Manager: + def __init__(self, name): + self.name = name + + async def __aenter__(self): + await AsyncYieldFrom(['enter-1-' + self.name, + 'enter-2-' + self.name]) + return self + + async def __aexit__(self, *args): + await AsyncYieldFrom(['exit-1-' + self.name, + 'exit-2-' + self.name]) + + if self.name == 'B': + return True + + async def foo(): + async with lop.Proxy(lambda: Manager("A")) as a, lop.Proxy(lambda: Manager("B")) as b: + await lop.Proxy(lambda: AsyncYieldFrom([('managers', a.name, b.name)])) + 1 / 0 + + f = lop.Proxy(foo) + result, _ = run_async(f) + + assert result == ['enter-1-A', 'enter-2-A', 'enter-1-B', 'enter-2-B', + ('managers', 'A', 'B'), + 'exit-1-B', 'exit-2-B', 'exit-1-A', 'exit-2-A'] + + async def foo(): + async with lop.Proxy(lambda: Manager("A")) as a, lop.Proxy(lambda: Manager("C")) as c: + await lop.Proxy(lambda: AsyncYieldFrom([('managers', a.name, c.name)])) + 1 / 0 + + with pytest.raises(ZeroDivisionError): + run_async(lop.Proxy(foo)) + + +def test_with_2(lop): + class CM: + def __aenter__(self): + pass + + body_executed = False + + async def foo(): + async with lop.Proxy(CM): + body_executed = True + + with pytest.raises(AttributeError, match='__aexit__'): + run_async(lop.Proxy(foo)) + assert not body_executed + + +def test_with_3(lop): + class CM: + def __aexit__(self): + pass + + body_executed = False + + async def foo(): + async with lop.Proxy(CM): + body_executed = True + + with pytest.raises(AttributeError, match='__aenter__'): + run_async(lop.Proxy(foo)) + assert not body_executed + + +def test_with_4(lop): + class CM: + pass + + body_executed = False + + async def foo(): + async with lop.Proxy(CM): + body_executed = True + + with pytest.raises(AttributeError, match='__aenter__'): + run_async(lop.Proxy(foo)) + assert not body_executed + + +def test_with_5(lop): + # While this test doesn't make a lot of sense, + # it's a regression test for an early bug with opcodes + # generation + + class CM: + async def __aenter__(self): + return self + + async def __aexit__(self, *exc): + pass + + async def func(): + async with lop.Proxy(CM): + assert (1,) == 1 + + with pytest.raises(AssertionError): + run_async(lop.Proxy(func)) + + +def test_with_6(lop): + class CM: + def __aenter__(self): + return 123 + + def __aexit__(self, *e): + return 456 + + async def foo(): + async with lop.Proxy(CM): + pass + + with pytest.raises(TypeError, match="'async with' received an object from __aenter__ " + "that does not implement __await__: int"): + # it's important that __aexit__ wasn't called + run_async(lop.Proxy(foo)) + + +def test_with_7(lop): + class CM: + async def __aenter__(self): + return self + + def __aexit__(self, *e): + return 444 + + # Exit with exception + async def foo(): + async with lop.Proxy(CM): + 1 / 0 + + try: + run_async(lop.Proxy(foo)) + except TypeError as exc: + assert re.search("'async with' received an object from __aexit__ " \ + "that does not implement __await__: int", exc.args[0]) + assert exc.__context__ is not None + assert isinstance(exc.__context__, ZeroDivisionError) + else: + pytest.fail('invalid asynchronous context manager did not fail') + + +def test_with_8(lop): + CNT = 0 + + class CM: + async def __aenter__(self): + return self + + def __aexit__(self, *e): + return 456 + + # Normal exit + async def foo(): + nonlocal CNT + async with lop.Proxy(CM): + CNT += 1 + + with pytest.raises(TypeError, match="'async with' received an object from __aexit__ " + "that does not implement __await__: int"): + run_async(lop.Proxy(foo)) + assert CNT == 1 + + # Exit with 'break' + async def foo(): + nonlocal CNT + for i in range(2): + async with lop.Proxy(CM): + CNT += 1 + break + + with pytest.raises(TypeError, match="'async with' received an object from __aexit__ " + "that does not implement __await__: int"): + run_async(lop.Proxy(foo)) + assert CNT == 2 + + # Exit with 'continue' + async def foo(): + nonlocal CNT + for i in range(2): + async with lop.Proxy(CM): + CNT += 1 + continue + + with pytest.raises(TypeError, match="'async with' received an object from __aexit__ " + "that does not implement __await__: int"): + run_async(lop.Proxy(foo)) + assert CNT == 3 + + # Exit with 'return' + async def foo(): + nonlocal CNT + async with lop.Proxy(CM): + CNT += 1 + return + + with pytest.raises(TypeError, match="'async with' received an object from __aexit__ " + "that does not implement __await__: int"): + run_async(lop.Proxy(foo)) + assert CNT == 4 + + +def test_with_9(lop): + CNT = 0 + + class CM: + async def __aenter__(self): + return self + + async def __aexit__(self, *e): + 1 / 0 + + async def foo(): + nonlocal CNT + async with lop.Proxy(CM): + CNT += 1 + + with pytest.raises(ZeroDivisionError): + run_async(lop.Proxy(foo)) + + assert CNT == 1 + + +def test_with_10(lop): + CNT = 0 + + class CM: + async def __aenter__(self): + return self + + async def __aexit__(self, *e): + 1 / 0 + + async def foo(): + nonlocal CNT + async with lop.Proxy(CM): + async with lop.Proxy(CM): + raise RuntimeError + + try: + run_async(lop.Proxy(foo)) + except ZeroDivisionError as exc: + assert exc.__context__ is not None + assert isinstance(exc.__context__, ZeroDivisionError) + assert isinstance(exc.__context__.__context__, + RuntimeError) + else: + pytest.fail('exception from __aexit__ did not propagate') + + +def test_with_11(lop): + CNT = 0 + + class CM: + async def __aenter__(self): + raise NotImplementedError + + async def __aexit__(self, *e): + 1 / 0 + + async def foo(): + nonlocal CNT + async with lop.Proxy(CM): + raise RuntimeError + + try: + run_async(lop.Proxy(foo)) + except NotImplementedError as exc: + assert exc.__context__ is None + else: + pytest.fail('exception from __aenter__ did not propagate') + + +def test_with_12(lop): + CNT = 0 + + class CM: + async def __aenter__(self): + return self + + async def __aexit__(self, *e): + return True + + async def foo(): + nonlocal CNT + async with lop.Proxy(CM) as cm: + assert cm.__class__ is CM + raise RuntimeError + + run_async(lop.Proxy(foo)) + + +def test_with_13(lop): + CNT = 0 + + class CM: + async def __aenter__(self): + 1 / 0 + + async def __aexit__(self, *e): + return True + + async def foo(): + nonlocal CNT + CNT += 1 + async with lop.Proxy(CM): + CNT += 1000 + CNT += 10000 + + with pytest.raises(ZeroDivisionError): + run_async(lop.Proxy(foo)) + assert CNT == 1 + + +def test_for_1(lop): + aiter_calls = 0 + + class AsyncIter: + def __init__(self): + self.i = 0 + + def __aiter__(self): + nonlocal aiter_calls + aiter_calls += 1 + return self + + async def __anext__(self): + self.i += 1 + + if not (self.i % 10): + await lop.Proxy(lambda: AsyncYield(self.i * 10)) + + if self.i > 100: + raise StopAsyncIteration + + return self.i, self.i + + buffer = [] + + async def test1(): + async for i1, i2 in lop.Proxy(AsyncIter): + buffer.append(i1 + i2) + + yielded, _ = run_async(lop.Proxy(test1)) + # Make sure that __aiter__ was called only once + assert aiter_calls == 1 + assert yielded == [i * 100 for i in range(1, 11)] + assert buffer == [i * 2 for i in range(1, 101)] + + buffer = [] + + async def test2(): + nonlocal buffer + async for i in lop.Proxy(AsyncIter): + buffer.append(i[0]) + if i[0] == 20: + break + else: + buffer.append('what?') + buffer.append('end') + + yielded, _ = run_async(lop.Proxy(test2)) + # Make sure that __aiter__ was called only once + assert aiter_calls == 2 + assert yielded == [100, 200] + assert buffer == [i for i in range(1, 21)] + ['end'] + + buffer = [] + + async def test3(): + nonlocal buffer + async for i in lop.Proxy(AsyncIter): + if i[0] > 20: + continue + buffer.append(i[0]) + else: + buffer.append('what?') + buffer.append('end') + + yielded, _ = run_async(lop.Proxy(test3)) + # Make sure that __aiter__ was called only once + assert aiter_calls == 3 + assert yielded == [i * 100 for i in range(1, 11)] + assert buffer == [i for i in range(1, 21)] + \ + ['what?', 'end'] + + +def test_for_2(lop): + tup = (1, 2, 3) + refs_before = sys.getrefcount(tup) + + async def foo(): + async for i in lop.Proxy(lambda: tup): + print('never going to happen') + + with pytest.raises(TypeError, match="async for' requires an object.*__aiter__.*tuple"): + run_async(lop.Proxy(foo)) + + assert sys.getrefcount(tup) == refs_before + + +def test_for_3(lop): + class I: + def __aiter__(self): + return self + + aiter = lop.Proxy(I) + refs_before = sys.getrefcount(aiter) + + async def foo(): + async for i in aiter: + print('never going to happen') + + with pytest.raises(TypeError, match=r"that does not implement __anext__"): + run_async(lop.Proxy(foo)) + + assert sys.getrefcount(aiter) == refs_before + + +def test_for_4(lop): + class I: + def __aiter__(self): + return self + + def __anext__(self): + return () + + aiter = lop.Proxy(I) + refs_before = sys.getrefcount(aiter) + + async def foo(): + async for i in aiter: + print('never going to happen') + + with pytest.raises(TypeError, match="async for' received an invalid object.*__anext__.*tuple"): + run_async(lop.Proxy(foo)) + + assert sys.getrefcount(aiter) == refs_before + + +def test_for_6(lop): + I = 0 + + class Manager: + async def __aenter__(self): + nonlocal I + I += 10000 + + async def __aexit__(self, *args): + nonlocal I + I += 100000 + + class Iterable: + def __init__(self): + self.i = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self.i > 10: + raise StopAsyncIteration + self.i += 1 + return self.i + + ############## + + manager = lop.Proxy(Manager) + iterable = lop.Proxy(Iterable) + mrefs_before = sys.getrefcount(manager) + irefs_before = sys.getrefcount(iterable) + + async def main(): + nonlocal I + + async with manager: + async for i in iterable: + I += 1 + I += 1000 + + with warnings.catch_warnings(): + warnings.simplefilter("error") + # Test that __aiter__ that returns an asynchronous iterator + # directly does not throw any warnings. + run_async(main()) + assert I == 111011 + + assert sys.getrefcount(manager) == mrefs_before + assert sys.getrefcount(iterable) == irefs_before + + ############## + + async def main(): + nonlocal I + + async with lop.Proxy(Manager): + async for i in lop.Proxy(Iterable): + I += 1 + I += 1000 + + async with lop.Proxy(Manager): + async for i in lop.Proxy(Iterable): + I += 1 + I += 1000 + + run_async(main()) + assert I == 333033 + + ############## + + async def main(): + nonlocal I + + async with lop.Proxy(Manager): + I += 100 + async for i in lop.Proxy(Iterable): + I += 1 + else: + I += 10000000 + I += 1000 + + async with lop.Proxy(Manager): + I += 100 + async for i in lop.Proxy(Iterable): + I += 1 + else: + I += 10000000 + I += 1000 + + run_async(lop.Proxy(main)) + assert I == 20555255 + + +def test_for_7(lop): + CNT = 0 + + class AI: + def __aiter__(self): + 1 / 0 + + async def foo(): + nonlocal CNT + async for i in lop.Proxy(AI): + CNT += 1 + CNT += 10 + + with pytest.raises(ZeroDivisionError): + run_async(lop.Proxy(foo)) + assert CNT == 0 + + +def test_for_8(lop): + CNT = 0 + + class AI: + def __aiter__(self): + 1 / 0 + + async def foo(): + nonlocal CNT + async for i in lop.Proxy(AI): + CNT += 1 + CNT += 10 + + with pytest.raises(ZeroDivisionError): + with warnings.catch_warnings(): + warnings.simplefilter("error") + # Test that if __aiter__ raises an exception it propagates + # without any kind of warning. + run_async(lop.Proxy(foo)) + assert CNT == 0 + + +def test_for_11(lop): + class F: + def __aiter__(self): + return self + + def __anext__(self): + return self + + def __await__(self): + 1 / 0 + + async def main(): + async for _ in lop.Proxy(F): + pass + + with pytest.raises(TypeError, match='an invalid object from __anext__') as c: + lop.Proxy(main).send(None) + + err = c.value + assert isinstance(err.__cause__, ZeroDivisionError) + + +def test_for_tuple(lop): + class Done(Exception): + pass + + class AIter(tuple): + i = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self.i >= len(self): + raise StopAsyncIteration + self.i += 1 + return self[self.i - 1] + + result = [] + + async def foo(): + async for i in lop.Proxy(lambda: AIter([42])): + result.append(i) + raise Done + + with pytest.raises(Done): + lop.Proxy(foo).send(None) + assert result == [42] + + +def test_for_stop_iteration(lop): + class Done(Exception): + pass + + class AIter(StopIteration): + i = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self.i: + raise StopAsyncIteration + self.i += 1 + return self.value + + result = [] + + async def foo(): + async for i in lop.Proxy(lambda: AIter(42)): + result.append(i) + raise Done + + with pytest.raises(Done): + lop.Proxy(foo).send(None) + assert result == [42] + + +def test_comp_1(lop): + async def f(i): + return i + + async def run_list(): + return [await c for c in [lop.Proxy(lambda: f(1)), lop.Proxy(lambda: f(41))]] + + async def run_set(): + return {await c for c in [lop.Proxy(lambda: f(1)), lop.Proxy(lambda: f(41))]} + + async def run_dict1(): + return {await c: 'a' for c in [lop.Proxy(lambda: f(1)), lop.Proxy(lambda: f(41))]} + + async def run_dict2(): + return {i: await c for i, c in enumerate([lop.Proxy(lambda: f(1)), lop.Proxy(lambda: f(41))])} + + assert run_async(run_list()) == ([], [1, 41]) + assert run_async(run_set()) == ([], {1, 41}) + assert run_async(run_dict1()) == ([], {1: 'a', 41: 'a'}) + assert run_async(run_dict2()) == ([], {0: 1, 1: 41}) + + +def test_comp_2(lop): + async def f(i): + return i + + async def run_list(): + return [s for c in [lop.Proxy(lambda: f('')), lop.Proxy(lambda: f('abc')), lop.Proxy(lambda: f('')), + lop.Proxy(lambda: f(['de', 'fg']))] + for s in await c] + + assert run_async(lop.Proxy(run_list)) == \ + ([], ['a', 'b', 'c', 'de', 'fg']) + + async def run_set(): + return { + d for c in [ + lop.Proxy(lambda: f([ + lop.Proxy(lambda: f([10, 30])), + lop.Proxy(lambda: f([20]))])) + ] + for s in await c + for d in await s} + + assert run_async(lop.Proxy(run_set)) == \ + ([], {10, 20, 30}) + + async def run_set2(): + return { + await s + for c in [lop.Proxy(lambda: f([ + lop.Proxy(lambda: f(10)), + lop.Proxy(lambda: f(20)) + ]))] + for s in await c} + + assert run_async(lop.Proxy(run_set2)) == \ + ([], {10, 20}) + + +def test_comp_3(lop): + async def f(it): + for i in it: + yield i + + async def run_list(): + return [i + 1 async for i in f([10, 20])] + + assert run_async(run_list()) == \ + ([], [11, 21]) + + async def run_set(): + return {i + 1 async for i in f([10, 20])} + + assert run_async(run_set()) == \ + ([], {11, 21}) + + async def run_dict(): + return {i + 1: i + 2 async for i in f([10, 20])} + + assert run_async(run_dict()) == \ + ([], {11: 12, 21: 22}) + + async def run_gen(): + gen = (i + 1 async for i in f([10, 20])) + return [g + 100 async for g in gen] + + assert run_async(run_gen()) == \ + ([], [111, 121]) + + +def test_comp_4(lop): + async def f(it): + for i in it: + yield i + + async def run_list(): + return [i + 1 async for i in f([10, 20]) if i > 10] + + assert run_async(run_list()) == \ + ([], [21]) + + async def run_set(): + return {i + 1 async for i in f([10, 20]) if i > 10} + + assert run_async(run_set()) == \ + ([], {21}) + + async def run_dict(): + return {i + 1: i + 2 async for i in f([10, 20]) if i > 10} + + assert run_async(run_dict()) == \ + ([], {21: 22}) + + async def run_gen(): + gen = (i + 1 async for i in f([10, 20]) if i > 10) + return [g + 100 async for g in gen] + + assert run_async(run_gen()) == \ + ([], [121]) + + +def test_comp_4_2(lop): + async def f(it): + for i in it: + yield i + + async def run_list(): + return [i + 10 async for i in f(range(5)) if 0 < i < 4] + + assert run_async(run_list()) == \ + ([], [11, 12, 13]) + + async def run_set(): + return {i + 10 async for i in f(range(5)) if 0 < i < 4} + + assert run_async(run_set()) == \ + ([], {11, 12, 13}) + + async def run_dict(): + return {i + 10: i + 100 async for i in f(range(5)) if 0 < i < 4} + + assert run_async(run_dict()) == \ + ([], {11: 101, 12: 102, 13: 103}) + + async def run_gen(): + gen = (i + 10 async for i in f(range(5)) if 0 < i < 4) + return [g + 100 async for g in gen] + + assert run_async(run_gen()) == \ + ([], [111, 112, 113]) + + +def test_comp_5(lop): + async def f(it): + for i in it: + yield i + + async def run_list(): + return [i + 1 for pair in ([10, 20], [30, 40]) if pair[0] > 10 + async for i in f(pair) if i > 30] + + assert run_async(run_list()) == \ + ([], [41]) + + +def test_comp_6(lop): + async def f(it): + for i in it: + yield i + + async def run_list(): + return [i + 1 async for seq in f([(10, 20), (30,)]) + for i in seq] + + assert run_async(run_list()) == \ + ([], [11, 21, 31]) + + +def test_comp_7(lop): + async def f(): + yield 1 + yield 2 + raise Exception('aaa') + + async def run_list(): + return [i async for i in f()] + + with pytest.raises(Exception, match='aaa'): + run_async(run_list()) + + +def test_comp_8(lop): + async def f(): + return [i for i in [1, 2, 3]] + + assert run_async(f()) == \ + ([], [1, 2, 3]) + + +def test_comp_9(lop): + async def gen(): + yield 1 + yield 2 + + async def f(): + l = [i async for i in gen()] + return [i for i in l] + + assert run_async(f()) == \ + ([], [1, 2]) + + +def test_comp_10(lop): + async def f(): + xx = {i for i in [1, 2, 3]} + return {x: x for x in xx} + + assert run_async(f()) == \ + ([], {1: 1, 2: 2, 3: 3}) + + +def test_copy(lop): + async def func(): + pass + + coro = func() + with pytest.raises(TypeError): + copy.copy(coro) + + aw = coro.__await__() + try: + with pytest.raises(TypeError): + copy.copy(aw) + finally: + aw.close() + + +def test_pickle(lop): + async def func(): + pass + + coro = func() + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with pytest.raises((TypeError, pickle.PicklingError)): + pickle.dumps(coro, proto) + + aw = coro.__await__() + try: + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with pytest.raises((TypeError, pickle.PicklingError)): + pickle.dumps(aw, proto) + finally: + aw.close() + + +def test_fatal_coro_warning(lop): + # Issue 27811 + async def func(): pass + + with warnings.catch_warnings(), \ + support.catch_unraisable_exception() as cm: + warnings.filterwarnings("error") + coro = func() + # only store repr() to avoid keeping the coroutine alive + coro_repr = repr(coro) + coro = None + support.gc_collect() + + assert "was never awaited" in str(cm.unraisable.exc_value) + assert repr(cm.unraisable.object) == coro_repr + + +def test_for_assign_raising_stop_async_iteration(lop): + class BadTarget: + def __setitem__(self, key, value): + raise StopAsyncIteration(42) + + tgt = BadTarget() + + async def source(): + yield 10 + + async def run_for(): + with pytest.raises(StopAsyncIteration) as cm: + async for tgt[0] in source(): + pass + assert cm.value.args == (42,) + return 'end' + + assert run_async(run_for()) == ([], 'end') + + async def run_list(): + with pytest.raises(StopAsyncIteration) as cm: + return [0 async for tgt[0] in lop.Proxy(source)] + assert cm.value.args == (42,) + return 'end' + + assert run_async(run_list()) == ([], 'end') + + async def run_gen(): + gen = (0 async for tgt[0] in lop.Proxy(source)) + a = gen.asend(None) + with pytest.raises(RuntimeError) as cm: + await a + assert isinstance(cm.value.__cause__, StopAsyncIteration) + assert cm.value.__cause__.args == (42,) + return 'end' + + assert run_async(run_gen()) == ([], 'end') + + +def test_for_assign_raising_stop_async_iteration_2(lop): + class BadIterable: + def __iter__(self): + raise StopAsyncIteration(42) + + async def badpairs(): + yield BadIterable() + + async def run_for(): + with pytest.raises(StopAsyncIteration) as cm: + async for i, j in lop.Proxy(badpairs): + pass + assert cm.value.args == (42,) + return 'end' + + assert run_async(run_for()) == ([], 'end') + + async def run_list(): + with pytest.raises(StopAsyncIteration) as cm: + return [0 async for i, j in badpairs()] + assert cm.value.args == (42,) + return 'end' + + assert run_async(run_list()) == ([], 'end') + + async def run_gen(): + gen = (0 async for i, j in badpairs()) + a = gen.asend(None) + with pytest.raises(RuntimeError) as cm: + await a + assert isinstance(cm.value.__cause__, StopAsyncIteration) + assert cm.value.__cause__.args == (42,) + return 'end' + + assert run_async(run_gen()) == ([], 'end') + + +def test_asyncio_1(lop): + # asyncio cannot be imported when Python is compiled without thread + # support + asyncio = support.import_module('asyncio') + + class MyException(Exception): + pass + + buffer = [] + + class CM: + async def __aenter__(self): + buffer.append(1) + await lop.Proxy(lambda: asyncio.sleep(0.01)) + buffer.append(2) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await lop.Proxy(lambda: asyncio.sleep(0.01)) + buffer.append(exc_type.__name__) + + async def f(): + async with lop.Proxy(CM) as c: + await lop.Proxy(lambda: asyncio.sleep(0.01)) + raise MyException + buffer.append('unreachable') + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(f()) + except MyException: + pass + finally: + loop.close() + asyncio.set_event_loop_policy(None) + + assert buffer == [1, 2, 'MyException'] diff --git a/tests/test_lazy_object_proxy.py b/tests/test_lazy_object_proxy.py index fff7948..879539a 100644 --- a/tests/test_lazy_object_proxy.py +++ b/tests/test_lazy_object_proxy.py @@ -35,71 +35,6 @@ def target(): exec_(OBJECTS_CODE, objects.__dict__, objects.__dict__) -def load_implementation(name): - class FakeModule: - subclass = False - kind = name - if name == "slots": - from lazy_object_proxy.slots import Proxy - elif name == "simple": - from lazy_object_proxy.simple import Proxy - elif name == "cext": - try: - from lazy_object_proxy.cext import Proxy - except ImportError: - if PYPY: - pytest.skip(msg="C Extension not available.") - else: - raise - elif name == "objproxies": - Proxy = pytest.importorskip("objproxies").LazyProxy - elif name == "django": - Proxy = pytest.importorskip("django.utils.functional").SimpleLazyObject - else: - raise RuntimeError("Unsupported param: %r." % name) - - Proxy - - return FakeModule - - -@pytest.fixture(scope="module", params=[ - "slots", "cext", - "simple", - # "external-django", "external-objproxies" -]) -def lop_implementation(request): - return load_implementation(request.param) - - -@pytest.fixture(scope="module", params=[True, False], ids=['subclassed', 'normal']) -def lop_subclass(request, lop_implementation): - if request.param: - class submod(lop_implementation): - subclass = True - Proxy = type("SubclassOf_" + lop_implementation.Proxy.__name__, - (lop_implementation.Proxy,), {}) - - return submod - else: - return lop_implementation - - -@pytest.fixture(scope="function") -def lop(request, lop_subclass): - if request.node.get_closest_marker('xfail_subclass'): - request.applymarker(pytest.mark.xfail( - reason="This test can't work because subclassing disables certain " - "features like __doc__ and __module__ proxying." - )) - if request.node.get_closest_marker('xfail_simple'): - request.applymarker(pytest.mark.xfail( - reason="The lazy_object_proxy.simple.Proxy has some limitations." - )) - - return lop_subclass - - def test_round(lop): proxy = lop.Proxy(lambda: 1.2) assert round(proxy) == 1 @@ -1833,8 +1768,8 @@ def test_garbage_collection_count(lop): @pytest.mark.parametrize("name", ["slots", "cext", "simple", "django", "objproxies"]) -def test_perf(benchmark, name): - implementation = load_implementation(name) +def test_perf(benchmark, name, lop_loader): + implementation = lop_loader(name) obj = "foobar" proxied = implementation.Proxy(lambda: obj) assert benchmark(partial(str, proxied)) == obj diff --git a/tox.ini b/tox.ini index 9b380b7..35ab862 100644 --- a/tox.ini +++ b/tox.ini @@ -47,6 +47,7 @@ deps = pytest-travis-fold Django objproxies==0.9.4 + hunter cover: pytest-cov commands = cover: python setup.py clean --all build_ext --force --inplace From fb4e926d3f8c4bb260058a4e381c0e5b4321feb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 16 Mar 2021 21:31:07 +0200 Subject: [PATCH 04/12] Get more of the tests passing, ugly `__await__` workaround. --- src/lazy_object_proxy/simple.py | 11 ++++------- src/lazy_object_proxy/slots.py | 17 ++++------------- src/lazy_object_proxy/utils.py | 18 ++++++++++++++++++ tests/test_async.py | 27 +++++++++++++-------------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/lazy_object_proxy/simple.py b/src/lazy_object_proxy/simple.py index df0197e..1510823 100644 --- a/src/lazy_object_proxy/simple.py +++ b/src/lazy_object_proxy/simple.py @@ -4,7 +4,7 @@ from .compat import PY3 from .compat import string_types from .compat import with_metaclass -from .utils import cached_property +from .utils import cached_property, await_ from .utils import identity @@ -260,14 +260,11 @@ def __reduce_ex__(self, protocol): def __aiter__(self): return self.__wrapped__.__aiter__() - def __anext__(self): - return self.__wrapped__.__anext__() + async def __anext__(self): + return await self.__wrapped__.__anext__() def __await__(self): - if hasattr(self.__wrapped__, '__await__'): - return self.__wrapped__.__await__() - else: - return iter(self.__wrapped__) + return await_(self.__wrapped__) def __aenter__(self): return self.__wrapped__.__aenter__() diff --git a/src/lazy_object_proxy/slots.py b/src/lazy_object_proxy/slots.py index 98fe98b..0b24f97 100644 --- a/src/lazy_object_proxy/slots.py +++ b/src/lazy_object_proxy/slots.py @@ -1,11 +1,10 @@ import operator -from types import GeneratorType, CoroutineType from .compat import PY2 from .compat import PY3 from .compat import string_types from .compat import with_metaclass -from .utils import identity +from .utils import identity, await_ class _ProxyMethods(object): @@ -415,11 +414,7 @@ def __exit__(self, *args, **kwargs): return self.__wrapped__.__exit__(*args, **kwargs) def __iter__(self): - if hasattr(self.__wrapped__, '__await__'): - return self.__wrapped__.__await__() - else: - # raise TypeError("'coroutine' object is not iterable") - return iter(self.__wrapped__) + return iter(self.__wrapped__) def __next__(self): return next(self.__wrapped__) @@ -434,17 +429,13 @@ def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) def __aiter__(self): - return self + return self.__wrapped__.__aiter__() async def __anext__(self): return await self.__wrapped__.__anext__() def __await__(self): - if hasattr(self.__wrapped__, '__await__'): - return self.__wrapped__.__await__() - else: - return (yield from self.__wrapped__) - + return await_(self.__wrapped__) def __aenter__(self): return self.__wrapped__.__aenter__() diff --git a/src/lazy_object_proxy/utils.py b/src/lazy_object_proxy/utils.py index ceb3050..129db45 100644 --- a/src/lazy_object_proxy/utils.py +++ b/src/lazy_object_proxy/utils.py @@ -1,3 +1,6 @@ +from inspect import isawaitable + + def identity(obj): return obj @@ -11,3 +14,18 @@ def __get__(self, obj, cls): return self value = obj.__dict__[self.func.__name__] = self.func(obj) return value + + +async def do_await(obj): + return await obj + + +def do_yield_from(gen): + return (yield from gen) + + +def await_(obj): + if isawaitable(obj): + return do_await(obj).__await__() + else: + return do_yield_from(obj) diff --git a/tests/test_async.py b/tests/test_async.py index ba49bf0..6260869 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -9,6 +9,8 @@ import pytest +from lazy_object_proxy.utils import await_ + class AsyncYieldFrom: def __init__(self, obj): @@ -114,8 +116,7 @@ async def foo(): check = lambda: pytest.raises(TypeError, match="'coroutine' object is not iterable") with check(): - import hunter - with hunter.trace(): list(coro) + list(coro) with check(): tuple(coro) @@ -203,14 +204,12 @@ def foo(): def test_func_8(lop): @types.coroutine def bar(): - val = (yield from coro) - print(val) - return val + return (yield from coro) async def foo(): return 'spam' - coro = lop.Proxy(foo) + coro = await_(lop.Proxy(foo)) # coro = lop.Proxy(foo) assert run_async(lop.Proxy(bar)) == ([], 'spam') coro.close() @@ -260,7 +259,7 @@ async def func(): pass # initialized assert '__await__' in dir(coro) assert '__iter__' in dir(coro.__await__()) - assert 'coroutine_wrapper' in repr(coro.__await__()) + assert 'coroutine_wrapper' in str(coro.__await__()) coro.close() # avoid RuntimeWarning @@ -482,7 +481,6 @@ async def b(): coro_b.send(None) assert inspect.getcoroutinestate(coro_b) == inspect.CORO_SUSPENDED - assert coro_b.cr_await.cr_await.gi_code.co_name == 'a' with pytest.raises(StopIteration): coro_b.send(None) # complete coroutine @@ -563,9 +561,10 @@ def test_await_8(lop): class Awaitable: pass - async def foo(): return await lop.Proxy(Awaitable) + async def foo(): + return await lop.Proxy(Awaitable) - with pytest.raises(TypeError, match="object Awaitable can't be used in 'await' expression"): + with pytest.raises(TypeError): run_async(lop.Proxy(foo)) @@ -772,7 +771,7 @@ async def foo(): async with lop.Proxy(CM): body_executed = True - with pytest.raises(AttributeError, match='__aexit__'): + with pytest.raises(TypeError): run_async(lop.Proxy(foo)) assert not body_executed @@ -1123,7 +1122,7 @@ async def foo(): async for i in lop.Proxy(lambda: tup): print('never going to happen') - with pytest.raises(TypeError, match="async for' requires an object.*__aiter__.*tuple"): + with pytest.raises(AttributeError, match="'tuple' object has no attribute '__aiter__'"): run_async(lop.Proxy(foo)) assert sys.getrefcount(tup) == refs_before @@ -1644,12 +1643,12 @@ async def func(): pass warnings.filterwarnings("error") coro = func() # only store repr() to avoid keeping the coroutine alive - coro_repr = repr(coro) + coro_repr = str(coro) coro = None support.gc_collect() assert "was never awaited" in str(cm.unraisable.exc_value) - assert repr(cm.unraisable.object) == coro_repr + assert str(cm.unraisable.object) == coro_repr def test_for_assign_raising_stop_async_iteration(lop): From 3892fd888bfe84b931663829ff4031c9354da66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 16 Mar 2021 22:02:17 +0200 Subject: [PATCH 05/12] Apply the same await_ workaround to the cext impl. --- src/lazy_object_proxy/cext.c | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/lazy_object_proxy/cext.c b/src/lazy_object_proxy/cext.c index dcf71f7..515bb39 100644 --- a/src/lazy_object_proxy/cext.c +++ b/src/lazy_object_proxy/cext.c @@ -37,6 +37,7 @@ PyTypeObject Proxy_Type; /* ------------------------------------------------------------------------- */ static PyObject *identity_ref = NULL; +static PyObject *await_ref = NULL; static PyObject * identity(PyObject *self, PyObject *value) { @@ -1273,21 +1274,7 @@ static PyObject *Proxy_await(ProxyObject *self) { Proxy__ENSURE_WRAPPED_OR_RETURN_NULL(self); - unaryfunc meth = NULL; - PyObject *wrapped = self->wrapped; - PyTypeObject *type = Py_TYPE(wrapped); - - - if (type->tp_as_async != NULL) { - meth = type->tp_as_async->am_await; - } - - if (meth != NULL) { - return (*meth)(wrapped); - } - - PyErr_Format(PyExc_TypeError, "%.100s is missing the __await__ method", type->tp_name); - return NULL; + return PyObject_CallFunctionObjArgs(await_ref, self->wrapped, NULL); } /* ------------------------------------------------------------------------- */; @@ -1308,7 +1295,7 @@ static PyObject *Proxy_aiter(ProxyObject *self) return (*meth)(wrapped); } - PyErr_Format(PyExc_TypeError, "%.100s is missing the __aiter__ method", type->tp_name); + PyErr_Format(PyExc_AttributeError, "'%.100s' object has no attribute '__aiter__'", type->tp_name); return NULL; } @@ -1331,7 +1318,7 @@ static PyObject *Proxy_anext(ProxyObject *self) return (*meth)(wrapped); } - PyErr_Format(PyExc_TypeError, "%.100s is missing the __anext__ method", type->tp_name); + PyErr_Format(PyExc_TypeError, "'%.100s' is missing the __anext__ method", type->tp_name); return NULL; } @@ -1556,6 +1543,17 @@ moduleinit(void) return NULL; Py_INCREF(identity_ref); +#if PY_MAJOR_VERSION >= 3 + PyObject *utils_module = PyImport_ImportModule("lazy_object_proxy.utils"); + if (utils_module == NULL) + return NULL; + + await_ref = PyObject_GetAttrString(utils_module, "await_"); + Py_DECREF(utils_module); + if (await_ref == NULL) + return NULL; +#endif + Py_INCREF(&Proxy_Type); PyModule_AddObject(module, "Proxy", (PyObject *)&Proxy_Type); From 91bb40a2b70d39f8888cdf03299aadd4eb890e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 17 Mar 2021 18:14:54 +0200 Subject: [PATCH 06/12] Add various conditionals to support older pythons. --- conftest.py | 10 +++++++++ src/lazy_object_proxy/simple.py | 23 ++++++++++--------- src/lazy_object_proxy/slots.py | 23 ++++++++++--------- src/lazy_object_proxy/utils.py | 10 ++++++--- tests/conftest.py | 3 +++ tests/{test_async.py => test_async_py3.py} | 26 ++++------------------ 6 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 conftest.py rename tests/{test_async.py => test_async_py3.py} (98%) diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..b09ced8 --- /dev/null +++ b/conftest.py @@ -0,0 +1,10 @@ +import sys + +PY3 = sys.version_info[0] >= 3 + + +def pytest_ignore_collect(path, config): + basename = path.basename + + if not PY3 and "py3" in basename or PY3 and "py2" in basename: + return True diff --git a/src/lazy_object_proxy/simple.py b/src/lazy_object_proxy/simple.py index 1510823..078d891 100644 --- a/src/lazy_object_proxy/simple.py +++ b/src/lazy_object_proxy/simple.py @@ -257,17 +257,20 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) - def __aiter__(self): - return self.__wrapped__.__aiter__() + if await_ is not None: + exec(""" +def __aiter__(self): + return self.__wrapped__.__aiter__() - async def __anext__(self): - return await self.__wrapped__.__anext__() +async def __anext__(self): + return await self.__wrapped__.__anext__() - def __await__(self): - return await_(self.__wrapped__) +def __await__(self): + return await_(self.__wrapped__) - def __aenter__(self): - return self.__wrapped__.__aenter__() +def __aenter__(self): + return self.__wrapped__.__aenter__() - def __aexit__(self, *args, **kwargs): - return self.__wrapped__.__aexit__(*args, **kwargs) +def __aexit__(self, *args, **kwargs): + return self.__wrapped__.__aexit__(*args, **kwargs) +""") diff --git a/src/lazy_object_proxy/slots.py b/src/lazy_object_proxy/slots.py index 0b24f97..71e8f1d 100644 --- a/src/lazy_object_proxy/slots.py +++ b/src/lazy_object_proxy/slots.py @@ -428,17 +428,20 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) - def __aiter__(self): - return self.__wrapped__.__aiter__() + if await_ is not None: + exec(""" +def __aiter__(self): + return self.__wrapped__.__aiter__() - async def __anext__(self): - return await self.__wrapped__.__anext__() +async def __anext__(self): + return await self.__wrapped__.__anext__() - def __await__(self): - return await_(self.__wrapped__) +def __await__(self): + return await_(self.__wrapped__) - def __aenter__(self): - return self.__wrapped__.__aenter__() +def __aenter__(self): + return self.__wrapped__.__aenter__() - def __aexit__(self, *args, **kwargs): - return self.__wrapped__.__aexit__(*args, **kwargs) +def __aexit__(self, *args, **kwargs): + return self.__wrapped__.__aexit__(*args, **kwargs) +""") diff --git a/src/lazy_object_proxy/utils.py b/src/lazy_object_proxy/utils.py index 129db45..4724f3b 100644 --- a/src/lazy_object_proxy/utils.py +++ b/src/lazy_object_proxy/utils.py @@ -1,6 +1,3 @@ -from inspect import isawaitable - - def identity(obj): return obj @@ -15,6 +12,10 @@ def __get__(self, obj, cls): value = obj.__dict__[self.func.__name__] = self.func(obj) return value +try: + exec(""" +from inspect import isawaitable + async def do_await(obj): return await obj @@ -29,3 +30,6 @@ def await_(obj): return do_await(obj).__await__() else: return do_yield_from(obj) +""") +except (ImportError, SyntaxError): + await_ = None diff --git a/tests/conftest.py b/tests/conftest.py index fb6ae63..92f9336 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import pytest + @pytest.fixture(scope="session") def lop_loader(): def load_implementation(name): @@ -28,8 +29,10 @@ class FakeModule: Proxy return FakeModule + return load_implementation + @pytest.fixture(scope="session", params=[ "slots", "cext", "simple", diff --git a/tests/test_async.py b/tests/test_async_py3.py similarity index 98% rename from tests/test_async.py rename to tests/test_async_py3.py index 6260869..d26d349 100644 --- a/tests/test_async.py +++ b/tests/test_async_py3.py @@ -5,7 +5,6 @@ import sys import types import warnings -from test import support import pytest @@ -1140,7 +1139,7 @@ async def foo(): async for i in aiter: print('never going to happen') - with pytest.raises(TypeError, match=r"that does not implement __anext__"): + with pytest.raises(TypeError): run_async(lop.Proxy(foo)) assert sys.getrefcount(aiter) == refs_before @@ -1634,23 +1633,7 @@ async def func(): aw.close() -def test_fatal_coro_warning(lop): - # Issue 27811 - async def func(): pass - - with warnings.catch_warnings(), \ - support.catch_unraisable_exception() as cm: - warnings.filterwarnings("error") - coro = func() - # only store repr() to avoid keeping the coroutine alive - coro_repr = str(coro) - coro = None - support.gc_collect() - - assert "was never awaited" in str(cm.unraisable.exc_value) - assert str(cm.unraisable.object) == coro_repr - - +@pytest.mark.skipif("sys.version_info[1] < 8") def test_for_assign_raising_stop_async_iteration(lop): class BadTarget: def __setitem__(self, key, value): @@ -1690,6 +1673,7 @@ async def run_gen(): assert run_async(run_gen()) == ([], 'end') +@pytest.mark.skipif("sys.version_info[1] < 8") def test_for_assign_raising_stop_async_iteration_2(lop): class BadIterable: def __iter__(self): @@ -1728,9 +1712,7 @@ async def run_gen(): def test_asyncio_1(lop): - # asyncio cannot be imported when Python is compiled without thread - # support - asyncio = support.import_module('asyncio') + import asyncio class MyException(Exception): pass From 1e5a40e41db031736656a7c31281578e54f6cdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 17 Mar 2021 20:38:51 +0200 Subject: [PATCH 07/12] Fix CI issues, I hope. --- .appveyor.yml | 2 -- .travis.yml | 1 + ci/templates/.appveyor.yml | 2 -- ci/templates/.travis.yml | 1 + setup.cfg | 2 +- tests/conftest.py | 4 ++++ 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 74794a9..cb0a8f2 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -133,8 +133,6 @@ install: - '%PYTHON_HOME%\Scripts\pip --version' - '%PYTHON_HOME%\Scripts\tox --version' test_script: - - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox -on_success: - ps: | Set-PSDebug -Trace 1 $ErrorActionPreference = "Stop" diff --git a/.travis.yml b/.travis.yml index dc78251..1e4c924 100644 --- a/.travis.yml +++ b/.travis.yml @@ -135,6 +135,7 @@ before_install: export PATH="/usr/local/opt/python/libexec/bin:${PATH}" fi install: + - while python -mpip uninstall virtualenv; do; done - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - pip --version diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml index 994932c..d41fb1a 100644 --- a/ci/templates/.appveyor.yml +++ b/ci/templates/.appveyor.yml @@ -43,8 +43,6 @@ install: - '%PYTHON_HOME%\Scripts\pip --version' - '%PYTHON_HOME%\Scripts\tox --version' test_script: - - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox -on_success: - ps: | Set-PSDebug -Trace 1 $ErrorActionPreference = "Stop" diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index 85506a0..76ecb08 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -58,6 +58,7 @@ before_install: export PATH="/usr/local/opt/python/libexec/bin:${PATH}" fi install: + - while python -mpip uninstall virtualenv; do; done - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - pip --version diff --git a/setup.cfg b/setup.cfg index d55819a..bae99d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [options] setup_requires = - setuptools_scm>=3.3.1 + setuptools_scm>=3.3.1,<6.0 [flake8] max-line-length = 140 diff --git a/tests/conftest.py b/tests/conftest.py index 92f9336..c357a01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,9 @@ +import sys + import pytest +PYPY = '__pypy__' in sys.builtin_module_names + @pytest.fixture(scope="session") def lop_loader(): From 8e4cfccd86321100a2eb1dd69b4e572c85c56190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 17 Mar 2021 20:48:57 +0200 Subject: [PATCH 08/12] Style fixing. --- src/lazy_object_proxy/simple.py | 3 ++- src/lazy_object_proxy/slots.py | 3 ++- src/lazy_object_proxy/utils.py | 1 + tests/test_async_py3.py | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lazy_object_proxy/simple.py b/src/lazy_object_proxy/simple.py index 078d891..903f02f 100644 --- a/src/lazy_object_proxy/simple.py +++ b/src/lazy_object_proxy/simple.py @@ -4,7 +4,8 @@ from .compat import PY3 from .compat import string_types from .compat import with_metaclass -from .utils import cached_property, await_ +from .utils import await_ +from .utils import cached_property from .utils import identity diff --git a/src/lazy_object_proxy/slots.py b/src/lazy_object_proxy/slots.py index 71e8f1d..edb9e08 100644 --- a/src/lazy_object_proxy/slots.py +++ b/src/lazy_object_proxy/slots.py @@ -4,7 +4,8 @@ from .compat import PY3 from .compat import string_types from .compat import with_metaclass -from .utils import identity, await_ +from .utils import await_ +from .utils import identity class _ProxyMethods(object): diff --git a/src/lazy_object_proxy/utils.py b/src/lazy_object_proxy/utils.py index 4724f3b..31b9af1 100644 --- a/src/lazy_object_proxy/utils.py +++ b/src/lazy_object_proxy/utils.py @@ -12,6 +12,7 @@ def __get__(self, obj, cls): value = obj.__dict__[self.func.__name__] = self.func(obj) return value + try: exec(""" from inspect import isawaitable diff --git a/tests/test_async_py3.py b/tests/test_async_py3.py index d26d349..36f70f6 100644 --- a/tests/test_async_py3.py +++ b/tests/test_async_py3.py @@ -1,3 +1,5 @@ +# flake8: noqa +# test code was mostly copied from stdlib, can't be fixing this mad stuff... import copy import inspect import pickle From 142c743a8ea83c17bc23aa85bd4c0a51bd6808cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 17 Mar 2021 21:17:49 +0200 Subject: [PATCH 09/12] Legacy python build workarounds. --- .appveyor.yml | 2 -- ci/appveyor-with-compiler.cmd | 21 +++++---------------- ci/templates/.appveyor.yml | 3 --- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index cb0a8f2..0702313 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -22,7 +22,6 @@ environment: PYTHON_HOME: C:\Python27-x64 PYTHON_VERSION: '2.7' PYTHON_ARCH: '64' - WINDOWS_SDK_VERSION: v7.0 - TOXENV: py27-nocov TOXPYTHON: C:\Python27\python.exe PYTHON_HOME: C:\Python27 @@ -35,7 +34,6 @@ environment: PYTHON_VERSION: '2.7' PYTHON_ARCH: '64' WHEEL_PATH: .tox/dist - WINDOWS_SDK_VERSION: v7.0 - TOXENV: py36-cover,codecov,coveralls TOXPYTHON: C:\Python36\python.exe PYTHON_HOME: C:\Python36 diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd index 289585f..5093538 100644 --- a/ci/appveyor-with-compiler.cmd +++ b/ci/appveyor-with-compiler.cmd @@ -1,22 +1,11 @@ -:: Very simple setup: -:: - if WINDOWS_SDK_VERSION is set then activate the SDK. -:: - disable the WDK if it's around. - SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" -ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% -IF EXIST %WIN_WDK% ( - REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN %WIN_WDK% 0wdf -) -IF "%WINDOWS_SDK_VERSION%"=="" GOTO main +IF "%PYTHON_VERSION%"=="2.7" GOTO legacy +GOTO main -SET DISTUTILS_USE_SDK=1 -SET MSSdk=1 -"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% -CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release +:legacy +powershell -Command "Invoke-WebRequest https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi -OutFile VCForPython27.msi" +msiexec /i VCForPython27.msi /quiet /qn /norestart :main ECHO Executing: %COMMAND_TO_RUN% diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml index d41fb1a..ff8e339 100644 --- a/ci/templates/.appveyor.yml +++ b/ci/templates/.appveyor.yml @@ -30,9 +30,6 @@ environment: {% if 'nocov' in env %} WHEEL_PATH: .tox/dist {% endif %} -{% if env.startswith('py2') %} - WINDOWS_SDK_VERSION: v7.0 -{% endif %} {% endif %}{% endfor %} init: - ps: echo $env:TOXENV From 070fd07c1bdbaf6c98b3cba89f2f9b4ce8614f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 17 Mar 2021 22:04:12 +0200 Subject: [PATCH 10/12] Fix constraint. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 23cf6d7..e69394c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,5 +2,5 @@ requires = [ "setuptools>=30.3.0", "wheel", - "setuptools_scm>=3.3.1", + "setuptools_scm>=3.3.1,<6.0", ] From 77ccc043b64f590af94d495b639a366c727ddbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 18 Mar 2021 01:30:48 +0200 Subject: [PATCH 11/12] Shell stuff. --- .travis.yml | 2 +- ci/templates/.travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1e4c924..cd88b64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -135,7 +135,7 @@ before_install: export PATH="/usr/local/opt/python/libexec/bin:${PATH}" fi install: - - while python -mpip uninstall virtualenv; do; done + - while python -mpip uninstall virtualenv; do echo; done - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - pip --version diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index 76ecb08..f42dc14 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -58,7 +58,7 @@ before_install: export PATH="/usr/local/opt/python/libexec/bin:${PATH}" fi install: - - while python -mpip uninstall virtualenv; do; done + - while python -mpip uninstall virtualenv; do echo; done - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - pip --version From d794e20d9384b3554753beaf3aba94e4b8668a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 18 Mar 2021 01:51:39 +0200 Subject: [PATCH 12/12] Dooh! --- .travis.yml | 3 ++- ci/templates/.travis.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index cd88b64..a83cd57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -135,7 +135,8 @@ before_install: export PATH="/usr/local/opt/python/libexec/bin:${PATH}" fi install: - - while python -mpip uninstall virtualenv; do echo; done + - python -mpip uninstall virtualenv --yes + - python -mpip uninstall virtualenv --yes - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - pip --version diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index f42dc14..80afcea 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -58,7 +58,8 @@ before_install: export PATH="/usr/local/opt/python/libexec/bin:${PATH}" fi install: - - while python -mpip uninstall virtualenv; do echo; done + - python -mpip uninstall virtualenv --yes + - python -mpip uninstall virtualenv --yes - python -mpip install --progress-bar=off --upgrade --ignore-installed -rci/requirements.txt - virtualenv --version - pip --version