From 88b382a7a73a7fddfbcf27ae032ac203c4f2410a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 15 Mar 2021 13:31:29 +0200 Subject: [PATCH 01/18] Update skel, drop py3.5 --- .appveyor.yml | 34 ++++++++++++++++++---------------- .bumpversion.cfg | 5 ++--- .cookiecutterrc | 8 ++++---- .travis.yml | 18 ------------------ CONTRIBUTING.rst | 3 ++- README.rst | 6 +++--- ci/templates/.appveyor.yml | 2 +- docs/conf.py | 2 +- setup.py | 3 +-- tox.ini | 3 +-- 10 files changed, 33 insertions(+), 51 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index c85d3c6..707f762 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -12,28 +12,30 @@ environment: PYTHON_HOME: C:\Python36 PYTHON_VERSION: '3.6' PYTHON_ARCH: '32' - - TOXENV: py35-cover,codecov,coveralls - TOXPYTHON: C:\Python35\python.exe - PYTHON_HOME: C:\Python35 - PYTHON_VERSION: '3.5' + - TOXENV: py27-cover,codecov,coveralls + TOXPYTHON: C:\Python27\python.exe + PYTHON_HOME: C:\Python27 + PYTHON_VERSION: '2.7' PYTHON_ARCH: '32' - - TOXENV: py35-cover,codecov,coveralls - TOXPYTHON: C:\Python35-x64\python.exe - PYTHON_HOME: C:\Python35-x64 - PYTHON_VERSION: '3.5' + - TOXENV: py27-cover,codecov,coveralls + TOXPYTHON: C:\Python27-x64\python.exe + PYTHON_HOME: C:\Python27-x64 + PYTHON_VERSION: '2.7' PYTHON_ARCH: '64' - - TOXENV: py35-nocov - TOXPYTHON: C:\Python35\python.exe - PYTHON_HOME: C:\Python35 - PYTHON_VERSION: '3.5' + WINDOWS_SDK_VERSION: v7.0 + - TOXENV: py27-nocov + TOXPYTHON: C:\Python27\python.exe + PYTHON_HOME: C:\Python27 + PYTHON_VERSION: '2.7' PYTHON_ARCH: '32' WHEEL_PATH: .tox/dist - - TOXENV: py35-nocov - TOXPYTHON: C:\Python35-x64\python.exe - PYTHON_HOME: C:\Python35-x64 - PYTHON_VERSION: '3.5' + - TOXENV: py27-nocov + TOXPYTHON: C:\Python27-x64\python.exe + PYTHON_HOME: C:\Python27-x64 + 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/.bumpversion.cfg b/.bumpversion.cfg index 10e47c4..76f2b2e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -8,8 +8,8 @@ search = 'fallback_version': '{current_version}' replace = 'fallback_version': '{new_version}' [bumpversion:file:README.rst] -search = v{current_version}. -replace = v{new_version}. +search = /v{current_version}.svg +replace = /v{new_version}.svg [bumpversion:file:docs/conf.py] search = version = release = '{current_version}' @@ -18,4 +18,3 @@ replace = version = release = '{new_version}' [bumpversion:file:src/lazy_object_proxy/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' - diff --git a/.cookiecutterrc b/.cookiecutterrc index 623c213..4ceb3e2 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -20,7 +20,7 @@ default_context: distribution_name: lazy-object-proxy email: contact@ionelmc.ro full_name: Ionel Cristian Mărieș - landscape: no + legacy_python: yes license: BSD 2-Clause License linter: flake8 package_name: lazy_object_proxy @@ -29,7 +29,7 @@ default_context: project_short_description: A fast and thorough lazy object proxy. pypi_badge: yes pypi_disable_upload: no - release_date: '2020-06-05' + release_date: '2020-11-26' repo_hosting: github.com repo_hosting_domain: github.com repo_name: python-lazy-object-proxy @@ -47,7 +47,7 @@ default_context: test_runner: pytest travis: yes travis_osx: yes - version: 1.5.0 + version: 1.5.2 website: https://blog.ionelmc.ro year_from: '2014' - year_to: '2020' + year_to: '2021' diff --git a/.travis.yml b/.travis.yml index 40f9dea..db4d50d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,24 +40,6 @@ matrix: - WHEEL_MANYLINUX="1 cp27" python: '2.7' arch: amd64 - - env: - - TOXENV=py35-cover,codecov,extension-coveralls,coveralls - python: '3.5' - arch: arm64 - - env: - - TOXENV=py35-cover,codecov,extension-coveralls,coveralls - python: '3.5' - arch: amd64 - - env: - - TOXENV=py35-nocov - - WHEEL_MANYLINUX="2014-arm cp35" - python: '3.5' - arch: arm64 - - env: - - TOXENV=py35-nocov - - WHEEL_MANYLINUX="1 cp35" - python: '3.5' - arch: amd64 - env: - TOXENV=py36-cover,codecov,extension-coveralls,coveralls python: '3.6' diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4c23f79..74587c9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -74,7 +74,8 @@ For merging, you should: 4. Add yourself to ``AUTHORS.rst``. .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will - `run the tests `_ for each change you add in the pull request. + `run the tests `_ + for each change you add in the pull request. It will be slower though ... diff --git a/README.rst b/README.rst index c5adba1..5aa866b 100644 --- a/README.rst +++ b/README.rst @@ -19,9 +19,9 @@ Overview :target: https://readthedocs.org/projects/python-lazy-object-proxy :alt: Documentation Status -.. |travis| image:: https://api.travis-ci.org/ionelmc/python-lazy-object-proxy.svg?branch=master +.. |travis| image:: https://api.travis-ci.com/ionelmc/python-lazy-object-proxy.svg?branch=master :alt: Travis-CI Build Status - :target: https://travis-ci.org/ionelmc/python-lazy-object-proxy + :target: https://travis-ci.com/github/ionelmc/python-lazy-object-proxy .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/ionelmc/python-lazy-object-proxy?branch=master&svg=true :alt: AppVeyor Build Status @@ -107,7 +107,7 @@ https://python-lazy-object-proxy.readthedocs.io/ Development =========== -To run the all tests run:: +To run all the tests run:: tox diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml index 66a2f9f..63318e0 100644 --- a/ci/templates/.appveyor.yml +++ b/ci/templates/.appveyor.yml @@ -13,7 +13,7 @@ environment: PYTHON_VERSION: '3.6' PYTHON_ARCH: '32' {% for env in tox_environments %} -{% if env.startswith('py3') %} +{% if env.startswith(('py2', 'py3')) %} - TOXENV: {{ env }}{% if env.endswith('-cover') %},codecov,coveralls{% endif %}{{ "" }} TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe PYTHON_HOME: C:\Python{{ env[2:4] }} diff --git a/docs/conf.py b/docs/conf.py index 16d0e69..b3f3d33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ source_suffix = '.rst' master_doc = 'index' project = 'lazy-object-proxy' -year = '2014-2020' +year = '2014-2021' author = 'Ionel Cristian Mărieș' copyright = '{0}, {1}'.format(year, author) try: diff --git a/setup.py b/setup.py index b66268a..2a3b67c 100755 --- a/setup.py +++ b/setup.py @@ -94,7 +94,6 @@ def read(*names, **kwargs): 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', @@ -115,7 +114,7 @@ def read(*names, **kwargs): keywords=[ # eg: 'keyword1', 'keyword2', 'keyword3', ], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*', install_requires=[ # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/tox.ini b/tox.ini index 9a97497..9b380b7 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ envlist = clean, check, docs, - {py27,py35,py36,py37,py38,py39,pypy,pypy3}-{cover,nocov}, + {py27,py36,py37,py38,py39,pypy,pypy3}-{cover,nocov}, report ignore_basepython_conflict = true @@ -24,7 +24,6 @@ basepython = pypy: {env:TOXPYTHON:pypy} pypy3: {env:TOXPYTHON:pypy3} py27: {env:TOXPYTHON:python2.7} - py35: {env:TOXPYTHON:python3.5} py36: {env:TOXPYTHON:python3.6} py37: {env:TOXPYTHON:python3.7} py38: {env:TOXPYTHON:python3.8} From e0daddded9abbbf27df05eb8a5268ea7b9e3f31c 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 02/18] 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 5d6dc2f9c7d159f32775521d9e8ac187dbdf09f0 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 03/18] 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 57aaf34f4fc210796caaceade6490904b25b2ff7 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 04/18] 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 0297d685334c0eeeef0c3b240a889d0084e614ba 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 05/18] 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 a5bbf68904eebbc67c3e64da6bb1a9faa6f69f06 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 06/18] 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 c62e615fd930f996aab27893ee184da4cf23bc24 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 07/18] 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 31ec3e34c70495d5f3d744ab5f42df275bf22234 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 08/18] 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 c544befa180de31094bad00c967bd837b68885b1 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 09/18] 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 fb05eac29468041958aaba129568c662c3f4825d 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 10/18] 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 b9cd8ccdb955af07e49a922ca8a3308253f2c1c2 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 11/18] 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 0ab93a78c286729fa8095b146a946c5ac412600b 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 12/18] 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 19a2406961f6f2fd629698b1ec6bf8bb2ab63f5a 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 13/18] 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 From 7f90ab788a6a6047d1004e4e47a286538e3c99e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 18 Mar 2021 14:24:46 +0200 Subject: [PATCH 14/18] Refactor a bit to get better tracebacks (source lines). --- CHANGELOG.rst | 9 ++++++ src/lazy_object_proxy/simple.py | 25 ++++++----------- src/lazy_object_proxy/slots.py | 25 ++++++----------- src/lazy_object_proxy/utils.py | 35 ++++++++---------------- src/lazy_object_proxy/utils_py3.py | 44 ++++++++++++++++++++++++++++++ tests/test_async_py3.py | 5 +--- 6 files changed, 82 insertions(+), 61 deletions(-) create mode 100644 src/lazy_object_proxy/utils_py3.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7862884..e24cc8c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,15 @@ Changelog ========= +1.6.0 (2021-03-19) +------------------ + +* Added support for async special methods (``__aiter__``, ``__anext__``, + ``__await__``, ``__aenter__``, ``__aexit__``). + These are used in the ``async for``, ``await` and ``async with`` statements. + + Note that ``__await__`` returns a wrapper that deals with the iterable/coroutine distinction + 1.5.2 (2020-11-26) ------------------ diff --git a/src/lazy_object_proxy/simple.py b/src/lazy_object_proxy/simple.py index 903f02f..9fc90d1 100644 --- a/src/lazy_object_proxy/simple.py +++ b/src/lazy_object_proxy/simple.py @@ -258,20 +258,11 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) - if await_ is not None: - exec(""" -def __aiter__(self): - return self.__wrapped__.__aiter__() - -async def __anext__(self): - return await self.__wrapped__.__anext__() - -def __await__(self): - return await_(self.__wrapped__) - -def __aenter__(self): - return self.__wrapped__.__aenter__() - -def __aexit__(self, *args, **kwargs): - return self.__wrapped__.__aexit__(*args, **kwargs) -""") + if await_: + from .utils import __aenter__ + from .utils import __aexit__ + from .utils import __aiter__ + from .utils import __anext__ + from .utils import __await__ + + __aiter__, __anext__, __await__, __aenter__, __aexit__ # noqa diff --git a/src/lazy_object_proxy/slots.py b/src/lazy_object_proxy/slots.py index edb9e08..c7fb235 100644 --- a/src/lazy_object_proxy/slots.py +++ b/src/lazy_object_proxy/slots.py @@ -429,20 +429,11 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return identity, (self.__wrapped__,) - if await_ is not None: - exec(""" -def __aiter__(self): - return self.__wrapped__.__aiter__() - -async def __anext__(self): - return await self.__wrapped__.__anext__() - -def __await__(self): - return await_(self.__wrapped__) - -def __aenter__(self): - return self.__wrapped__.__aenter__() - -def __aexit__(self, *args, **kwargs): - return self.__wrapped__.__aexit__(*args, **kwargs) -""") + if await_: + from .utils import __aenter__ + from .utils import __aexit__ + from .utils import __aiter__ + from .utils import __anext__ + from .utils import __await__ + + __aiter__, __anext__, __await__, __aenter__, __aexit__ # noqa diff --git a/src/lazy_object_proxy/utils.py b/src/lazy_object_proxy/utils.py index 31b9af1..3307abe 100644 --- a/src/lazy_object_proxy/utils.py +++ b/src/lazy_object_proxy/utils.py @@ -1,3 +1,15 @@ + # flake8: noqa +try: + from .utils_py3 import __aenter__ + from .utils_py3 import __aexit__ + from .utils_py3 import __aiter__ + from .utils_py3 import __anext__ + from .utils_py3 import __await__ + from .utils_py3 import await_ +except (ImportError, SyntaxError): + await_ = None + + def identity(obj): return obj @@ -11,26 +23,3 @@ def __get__(self, obj, cls): return self 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 - - -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) -""") -except (ImportError, SyntaxError): - await_ = None diff --git a/src/lazy_object_proxy/utils_py3.py b/src/lazy_object_proxy/utils_py3.py new file mode 100644 index 0000000..101f270 --- /dev/null +++ b/src/lazy_object_proxy/utils_py3.py @@ -0,0 +1,44 @@ +from collections.abc import Awaitable +from inspect import CO_ITERABLE_COROUTINE +from types import CoroutineType +from types import GeneratorType + + +async def do_await(obj): + return await obj + + +def do_yield_from(gen): + return (yield from gen) + + +def await_(obj): + obj_type = type(obj) + if ( + obj_type is CoroutineType or + obj_type is GeneratorType and bool(obj.gi_code.co_flags & CO_ITERABLE_COROUTINE) or + isinstance(obj, Awaitable) + ): + return do_await(obj).__await__() + else: + return do_yield_from(obj) + + +def __aiter__(self): + return self.__wrapped__.__aiter__() + + +async def __anext__(self): + return await self.__wrapped__.__anext__() + + +def __await__(self): + return await_(self.__wrapped__) + + +def __aenter__(self): + return self.__wrapped__.__aenter__() + + +def __aexit__(self, *args, **kwargs): + return self.__wrapped__.__aexit__(*args, **kwargs) diff --git a/tests/test_async_py3.py b/tests/test_async_py3.py index 36f70f6..dba934f 100644 --- a/tests/test_async_py3.py +++ b/tests/test_async_py3.py @@ -169,10 +169,7 @@ def bar(): yield 2 async def foo(): - await proxy(lop.Proxy(bar)) - - import dis - dis.dis(foo) + await lop.Proxy(bar) f = lop.Proxy(foo) assert f.send(None) == 1 From 4885b1fa64357a520525a6847abf0748e66b196b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 18 Mar 2021 14:31:57 +0200 Subject: [PATCH 15/18] Better text. --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e24cc8c..ea9d94d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,8 +9,8 @@ Changelog ``__await__``, ``__aenter__``, ``__aexit__``). These are used in the ``async for``, ``await` and ``async with`` statements. - Note that ``__await__`` returns a wrapper that deals with the iterable/coroutine distinction - + Note that ``__await__`` returns a wrapper that tries to emulate the crazy + stuff going on in the ceval loop, so there will be a small performance overhead. 1.5.2 (2020-11-26) ------------------ From 631a9d5664ab24b601da646053fd9ad57d5d307e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 22 Mar 2021 15:40:02 +0200 Subject: [PATCH 16/18] Add __resolved__ property. Closes #41. --- src/lazy_object_proxy/cext.c | 14 ++++++++++++++ src/lazy_object_proxy/simple.py | 4 ++++ src/lazy_object_proxy/slots.py | 10 ++++++++++ tests/test_lazy_object_proxy.py | 20 ++++++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/src/lazy_object_proxy/cext.c b/src/lazy_object_proxy/cext.c index 515bb39..da9d7f8 100644 --- a/src/lazy_object_proxy/cext.c +++ b/src/lazy_object_proxy/cext.c @@ -1080,6 +1080,18 @@ static int Proxy_set_annotations(ProxyObject *self, /* ------------------------------------------------------------------------- */ +static PyObject *Proxy_get_resolved( + ProxyObject *self) +{ + PyObject *result; + + result = self->wrapped ? Py_True : Py_False; + Py_INCREF(result); + return result; +} + +/* ------------------------------------------------------------------------- */ + static PyObject *Proxy_get_wrapped( ProxyObject *self) { @@ -1444,6 +1456,8 @@ static PyGetSetDef Proxy_getset[] = { (setter)Proxy_set_wrapped, 0 }, { "__factory__", (getter)Proxy_get_factory, (setter)Proxy_set_factory, 0 }, + { "__resolved__", (getter)Proxy_get_resolved, + NULL, 0 }, { NULL }, }; diff --git a/src/lazy_object_proxy/simple.py b/src/lazy_object_proxy/simple.py index 9fc90d1..ea5cf8a 100644 --- a/src/lazy_object_proxy/simple.py +++ b/src/lazy_object_proxy/simple.py @@ -71,6 +71,10 @@ class Proxy(with_metaclass(_ProxyMetaType)): def __init__(self, factory): self.__dict__['__factory__'] = factory + @property + def __resolved__(self): + return '__wrapped__' in self.__dict__ + @cached_property def __wrapped__(self): self = self.__dict__ diff --git a/src/lazy_object_proxy/slots.py b/src/lazy_object_proxy/slots.py index c7fb235..41cee25 100644 --- a/src/lazy_object_proxy/slots.py +++ b/src/lazy_object_proxy/slots.py @@ -72,6 +72,7 @@ class Proxy(with_metaclass(_ProxyMetaType)): * ``__factory__`` is the callback that "materializes" the object we proxy to. * ``__target__`` will contain the object we proxy to, once it's "materialized". + * ``__resolved__`` is a boolean, `True` if factory was called. * ``__wrapped__`` is a property that does either: * return ``__target__`` if it's set. @@ -83,6 +84,15 @@ class Proxy(with_metaclass(_ProxyMetaType)): def __init__(self, factory): object.__setattr__(self, '__factory__', factory) + @property + def __resolved__(self, __getattr__=object.__getattribute__): + try: + __getattr__(self, '__target__') + except AttributeError: + return False + else: + return True + @property def __wrapped__(self, __getattr__=object.__getattribute__, __setattr__=object.__setattr__, __delattr__=object.__delattr__): diff --git a/tests/test_lazy_object_proxy.py b/tests/test_lazy_object_proxy.py index 879539a..8bd3532 100644 --- a/tests/test_lazy_object_proxy.py +++ b/tests/test_lazy_object_proxy.py @@ -1846,6 +1846,7 @@ def test_proto(benchmark, prototype): def test_subclassing_with_local_attr(lop): class Foo: pass + called = [] class LazyProxy(lop.Proxy): @@ -1897,3 +1898,22 @@ def test_fspath(lop): def test_fspath_method(lop): assert lop.Proxy(FSPathMock).__fspath__() == '/tmp' + + +def test_resolved_new(lop): + obj = lop.Proxy.__new__(lop.Proxy) + assert obj.__resolved__ is False + + +def test_resolved(lop): + obj = lop.Proxy(lambda: None) + assert obj.__resolved__ is False + assert obj.__wrapped__ is None + assert obj.__resolved__ is True + + +def test_resolved_str(lop): + obj = lop.Proxy(lambda: None) + assert obj.__resolved__ is False + str(obj) + assert obj.__resolved__ is True From 07e55c28ea1e8b25e8d03e05333526ada546a45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 22 Mar 2021 17:06:19 +0200 Subject: [PATCH 17/18] Update changelog. --- CHANGELOG.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ea9d94d..20ad4b2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,7 @@ Changelog ========= -1.6.0 (2021-03-19) +1.6.0 (2021-03-22) ------------------ * Added support for async special methods (``__aiter__``, ``__anext__``, @@ -11,6 +11,8 @@ Changelog Note that ``__await__`` returns a wrapper that tries to emulate the crazy stuff going on in the ceval loop, so there will be a small performance overhead. +* Added the ``__resolved__`` property. You can use it to check if the factory has + been called. 1.5.2 (2020-11-26) ------------------ From 9488eba4f52593538f551d90694f1c996f7b7acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 22 Mar 2021 17:06:24 +0200 Subject: [PATCH 18/18] =?UTF-8?q?Bump=20version:=201.5.2=20=E2=86=92=201.6?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 3 ++- README.rst | 2 +- docs/conf.py | 2 +- setup.py | 2 +- src/lazy_object_proxy/__init__.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 76f2b2e..9609bf6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.5.2 +current_version = 1.6.0 commit = True tag = True @@ -18,3 +18,4 @@ replace = version = release = '{new_version}' [bumpversion:file:src/lazy_object_proxy/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' + diff --git a/README.rst b/README.rst index 5aa866b..8e70976 100644 --- a/README.rst +++ b/README.rst @@ -55,7 +55,7 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/lazy-object-proxy -.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-lazy-object-proxy/v1.5.2.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-lazy-object-proxy/v1.6.0.svg :alt: Commits since latest release :target: https://github.com/ionelmc/python-lazy-object-proxy/compare/v1.5.2...master diff --git a/docs/conf.py b/docs/conf.py index b3f3d33..0e01416 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ version = release = get_distribution('lazy_object_proxy').version except Exception: traceback.print_exc() - version = release = '1.5.2' + version = release = '1.6.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/setup.py b/setup.py index 2a3b67c..40a7fd5 100755 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ def read(*names, **kwargs): use_scm_version={ 'local_scheme': 'dirty-tag', 'write_to': 'src/lazy_object_proxy/_version.py', - 'fallback_version': '1.5.2', + 'fallback_version': '1.6.0', }, license='BSD-2-Clause', description='A fast and thorough lazy object proxy.', diff --git a/src/lazy_object_proxy/__init__.py b/src/lazy_object_proxy/__init__.py index 19e6900..db37c85 100644 --- a/src/lazy_object_proxy/__init__.py +++ b/src/lazy_object_proxy/__init__.py @@ -18,6 +18,6 @@ try: from ._version import version as __version__ except ImportError: - __version__ = '1.5.2' + __version__ = '1.6.0' __all__ = "Proxy",