diff --git a/docs/page-objects/fields.rst b/docs/page-objects/fields.rst index d6096499..220331a7 100644 --- a/docs/page-objects/fields.rst +++ b/docs/page-objects/fields.rst @@ -485,6 +485,17 @@ 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`, 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 +in mind. + Field metadata -------------- 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=[ 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..f0f6054e 100644 --- a/web_poet/utils.py +++ b/web_poet/utils.py @@ -1,11 +1,13 @@ 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 +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,16 @@ async def inner(self, *args, **kwargs): return inner +# 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: 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) + + def as_list(value: Optional[Any]) -> List[Any]: """Normalizes the value input as a list.