From 5c05299d51b5db8e7186df67c7833f62671479bb Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 20 Feb 2023 19:09:53 +0500 Subject: [PATCH 1/6] fixed compatibility with async_lru >= 2 --- docs/page-objects/fields.rst | 12 ++++++++++++ tests/test_utils.py | 9 ++++----- web_poet/utils.py | 11 ++++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/page-objects/fields.rst b/docs/page-objects/fields.rst index d6096499..f1f16310 100644 --- a/docs/page-objects/fields.rst +++ b/docs/page-objects/fields.rst @@ -485,6 +485,18 @@ with async versions of ``@property`` and ``@cached_property`` decorators; unlike .. _async_property: https://github.com/ryananguiano/async_property +Exceptions caching +~~~~~~~~~~~~~~~~~~ + +Note that exceptions are not cached - neither by :func:`~.cached_method`, +nor by `@field(cached=True)`, nor by :func:`functools.lru_cache` +and :func:`functools.cached_property` stdlib options. So, if a cached method +raises an exception + +Usually it's not an issue, because an exception is usually propagated, +and so there are no duplicate calls anyways. But, just in case, keep this +in mind. + Field metadata -------------- diff --git a/tests/test_utils.py b/tests/test_utils.py index eb11d5d9..c6f52733 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -405,7 +405,6 @@ async def meth(self): assert await foo.meth() == 1 -@pytest.mark.xfail def test_cached_method_exception() -> None: class Error(Exception): pass @@ -420,10 +419,10 @@ def meth(self): foo = Foo() - for _ in range(2): + for idx in range(2): with pytest.raises(Error): foo.meth() - assert foo.n_called == 1 + assert foo.n_called == idx + 1 @pytest.mark.asyncio @@ -441,10 +440,10 @@ async def meth(self): foo = Foo() - for _ in range(2): + for idx in range(2): with pytest.raises(Error): await foo.meth() - assert foo.n_called == 1 + assert foo.n_called == idx + 1 @pytest.mark.asyncio diff --git a/web_poet/utils.py b/web_poet/utils.py index 7056b1ab..6de0604f 100644 --- a/web_poet/utils.py +++ b/web_poet/utils.py @@ -6,6 +6,8 @@ from typing import Any, Callable, List, Optional, TypeVar, Union from warnings import warn +import packaging.version +from async_lru import __version__ as async_lru_version from async_lru import alru_cache from url_matcher import Patterns @@ -203,7 +205,7 @@ async def inner(self, *args, **kwargs): # on a first call, create an alru_cache-wrapped method, # and store it on the instance bound_method = MethodType(method, self) - cached_meth = alru_cache(maxsize=None)(bound_method) + cached_meth = _alru_cache(maxsize=None)(bound_method) setattr(self, cached_method_name, cached_meth) else: cached_meth = getattr(self, cached_method_name) @@ -212,6 +214,13 @@ async def inner(self, *args, **kwargs): return inner +def _alru_cache(**kwargs): + ver = packaging.version.parse(async_lru_version) + if ver.major < 2: + kwargs["cache_exceptions"] = False + return alru_cache(**kwargs) + + def as_list(value: Optional[Any]) -> List[Any]: """Normalizes the value input as a list. From 481db7a03e1db5be0902717907b281fd8ac6a5c4 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 20 Feb 2023 22:17:33 +0500 Subject: [PATCH 2/6] clean up docs about exceptions caching --- docs/page-objects/fields.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/page-objects/fields.rst b/docs/page-objects/fields.rst index f1f16310..d4001550 100644 --- a/docs/page-objects/fields.rst +++ b/docs/page-objects/fields.rst @@ -489,9 +489,8 @@ Exceptions caching ~~~~~~~~~~~~~~~~~~ Note that exceptions are not cached - neither by :func:`~.cached_method`, -nor by `@field(cached=True)`, nor by :func:`functools.lru_cache` -and :func:`functools.cached_property` stdlib options. So, if a cached method -raises an exception +nor by `@field(cached=True)`, nor by :func:`functools.lru_cache`, nor by + :func:`functools.cached_property`. Usually it's not an issue, because an exception is usually propagated, and so there are no duplicate calls anyways. But, just in case, keep this From 8c8145f98ea1e7626fb481deddc51cf165c1e68e Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Mon, 20 Feb 2023 22:19:26 +0500 Subject: [PATCH 3/6] fixed docs syntax --- docs/page-objects/fields.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/page-objects/fields.rst b/docs/page-objects/fields.rst index d4001550..220331a7 100644 --- a/docs/page-objects/fields.rst +++ b/docs/page-objects/fields.rst @@ -490,7 +490,7 @@ Exceptions caching Note that exceptions are not cached - neither by :func:`~.cached_method`, nor by `@field(cached=True)`, nor by :func:`functools.lru_cache`, nor by - :func:`functools.cached_property`. +:func:`functools.cached_property`. Usually it's not an issue, because an exception is usually propagated, and so there are no duplicate calls anyways. But, just in case, keep this From 7b4522b29710ca4a2901f77a8fb4cb598e07a8fc Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 21 Feb 2023 15:53:42 +0500 Subject: [PATCH 4/6] faster _async_lru implementation --- web_poet/utils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/web_poet/utils.py b/web_poet/utils.py index 6de0604f..f2f0b664 100644 --- a/web_poet/utils.py +++ b/web_poet/utils.py @@ -1,7 +1,7 @@ import inspect import weakref from collections.abc import Iterable -from functools import lru_cache, wraps +from functools import lru_cache, partial, wraps from types import MethodType from typing import Any, Callable, List, Optional, TypeVar, Union from warnings import warn @@ -214,11 +214,14 @@ async def inner(self, *args, **kwargs): return inner -def _alru_cache(**kwargs): - ver = packaging.version.parse(async_lru_version) - if ver.major < 2: - kwargs["cache_exceptions"] = False - return alru_cache(**kwargs) +# async_lru >= 2.0.0 removed cache_exceptions argument, and changed +# its default value. `_alru_cache` is a compatibility function which works with +# all async_lru versions and uses the same approach for exception caching +# as async_lru >= 2.0.0. +_alru_cache = alru_cache +_async_lru_version = packaging.version.parse(async_lru_version) +if _async_lru_version.major < 2: + _alru_cache = partial(alru_cache, cache_exceptions=False) def as_list(value: Optional[Any]) -> List[Any]: From 18a8cb92f352f4da0ae9ac6340f82f5c7af18594 Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 21 Feb 2023 15:54:09 +0500 Subject: [PATCH 5/6] add packaging to install_requires explicitly --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a4658daa..8e8f5ff8 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ "andi", "python-dateutil", "time-machine", + "packaging", "backports.zoneinfo; python_version < '3.9' and platform_system != 'Windows'", ], classifiers=[ From d77296ef7067ab20d3702e5024c5e9e2429ce2bd Mon Sep 17 00:00:00 2001 From: Mikhail Korobov Date: Tue, 21 Feb 2023 15:57:35 +0500 Subject: [PATCH 6/6] fix typing --- web_poet/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_poet/utils.py b/web_poet/utils.py index f2f0b664..f0f6054e 100644 --- a/web_poet/utils.py +++ b/web_poet/utils.py @@ -218,7 +218,7 @@ async def inner(self, *args, **kwargs): # its default value. `_alru_cache` is a compatibility function which works with # all async_lru versions and uses the same approach for exception caching # as async_lru >= 2.0.0. -_alru_cache = alru_cache +_alru_cache: Callable = alru_cache _async_lru_version = packaging.version.parse(async_lru_version) if _async_lru_version.major < 2: _alru_cache = partial(alru_cache, cache_exceptions=False)