Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-async-lru for 
openSUSE:Factory checked in at 2026-10-20 12:40:16
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-async-lru (Old)
 and      /work/SRC/openSUSE:Factory/.python-async-lru.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-async-lru"

Tue Oct 20 12:40:16 2026 rev:7 rq:1386240 version:2.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-async-lru/python-async-lru.changes        
2026-04-04 19:09:18.625781890 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-async-lru.new.3698736/python-async-lru.changes
   2026-10-20 12:41:43.332026986 +0200
@@ -1,0 +2,11 @@
+Sat Oct 17 15:14:56 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 2.4.0:
+  * Added a ``key`` parameter for custom cache key computation.
+  * Fixed a ``NameError`` when decorating functions whose
+    annotations refer to names defined later, on Python 3.14.
+  * On Python 3.14, cached functions expose ``__annotate__``
+    instead of ``__annotations__``, as ``functools.lru_cache``
+    wrappers do.
+
+-------------------------------------------------------------------

Old:
----
  async_lru-2.3.0.tar.gz

New:
----
  async_lru-2.4.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-async-lru.spec ++++++
--- /var/tmp/diff_new_pack.xrJSn4/_old  2026-10-20 12:41:44.027056074 +0200
+++ /var/tmp/diff_new_pack.xrJSn4/_new  2026-10-20 12:41:44.031056242 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-async-lru
-Version:        2.3.0
+Version:        2.4.0
 Release:        0
 Summary:        Simple LRU cache for asyncio
 License:        MIT

++++++ async_lru-2.3.0.tar.gz -> async_lru-2.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/PKG-INFO new/async_lru-2.4.0/PKG-INFO
--- old/async_lru-2.3.0/PKG-INFO        2026-03-19 02:04:16.406802400 +0100
+++ new/async_lru-2.4.0/PKG-INFO        2026-10-12 20:12:35.842727200 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: async-lru
-Version: 2.3.0
+Version: 2.4.0
 Summary: Simple LRU cache for asyncio
 Home-page: https://github.com/aio-libs/async-lru
 Maintainer: aiohttp team <[email protected]>
@@ -152,6 +152,32 @@
 The method returns `True` if the result for the given arguments is cached, 
`False`
 otherwise.
 
+Custom cache keys
+-----------------
+
+By default the cache key is built from all arguments, like
+``functools.lru_cache`` does. The ``key`` parameter accepts a callable that
+receives the same arguments as the wrapped function and returns the cache key,
+so arguments that do not affect the result can be excluded from it. This is an
+async-lru extension beyond the ``functools.lru_cache`` interface:
+
+.. code-block:: python
+
+    @alru_cache(key=lambda db, query: query)
+    async def query_db(db, query):
+        return await db.execute(query)
+
+    # Both calls share one cache entry despite the different connections.
+    await query_db(conn1, "SELECT ...")
+    await query_db(conn2, "SELECT ...")
+
+The returned key must be hashable. ``cache_invalidate()`` and
+``cache_contains()`` compute the key the same way, so they accept the full
+argument list as usual. For decorated methods the key callable receives the
+instance as its first argument. Passing ``typed=True`` together with ``key``
+raises ``ValueError``, since ``typed`` only affects the default key
+computation.
+
 Limitations
 -----------
 
@@ -199,6 +225,40 @@
     cached_task_loop2 = alru_cache(maxsize=32)(my_task.__wrapped__)
     await cached_task_loop2(x)
 
+Security considerations
+-----------------------
+
+**Cache keys are built only from explicit arguments.** Like
+`functools.lru_cache 
<https://docs.python.org/3/library/functools.html#functools.lru_cache>`_,
+``alru_cache`` derives its cache key solely from the positional and keyword 
arguments
+passed to the wrapped function. Implicit, request-scoped context — such as
+authentication headers, the current user or tenant, ``contextvars``, 
thread/task
+locals, or module globals — is **not** part of the key and therefore **not** 
isolated
+between callers.
+
+Because concurrent calls with the same key also share a single in-flight 
result (see
+the Usage section above), a value computed for one caller can be returned to 
another whenever
+their arguments are equal. In multi-tenant or multi-user services this can 
lead to
+cross-tenant data exposure if the cached coroutine's result depends on 
anything other
+than its explicit arguments.
+
+To use ``alru_cache`` safely in these contexts:
+
+- **Make the cached coroutine a pure function of its arguments.** Any value 
that
+  affects the result — ``user_id``, ``tenant_id``, role, locale, feature 
flags, etc. —
+  must be passed as an argument so it becomes part of the cache key, or use a 
separate
+  cache instance per security domain.
+- **Avoid caching context-dependent functions.** If a function reads 
request-scoped
+  state from ``contextvars``/globals rather than from its arguments, either 
refactor it
+  to take that state explicitly or do not cache it.
+- **Consider** ``typed=True`` **when callers may pass multiple types.** With 
the default
+  ``typed=False``, arguments that compare and hash equal share an entry (for 
example
+  ``1`` and ``1.0``, or ``True`` and ``1``). Pass ``typed=True`` to key such 
arguments
+  distinctly.
+- **Be wary of attacker-controlled key arguments.** Objects with unusual 
``__hash__`` /
+  ``__eq__`` semantics can collide unexpectedly; only use trusted, 
well-behaved values
+  as cache key components.
+
 Benchmarks
 ----------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/README.rst 
new/async_lru-2.4.0/README.rst
--- old/async_lru-2.3.0/README.rst      2026-03-19 02:04:08.000000000 +0100
+++ new/async_lru-2.4.0/README.rst      2026-10-12 20:12:30.073179000 +0200
@@ -121,6 +121,32 @@
 The method returns `True` if the result for the given arguments is cached, 
`False`
 otherwise.
 
+Custom cache keys
+-----------------
+
+By default the cache key is built from all arguments, like
+``functools.lru_cache`` does. The ``key`` parameter accepts a callable that
+receives the same arguments as the wrapped function and returns the cache key,
+so arguments that do not affect the result can be excluded from it. This is an
+async-lru extension beyond the ``functools.lru_cache`` interface:
+
+.. code-block:: python
+
+    @alru_cache(key=lambda db, query: query)
+    async def query_db(db, query):
+        return await db.execute(query)
+
+    # Both calls share one cache entry despite the different connections.
+    await query_db(conn1, "SELECT ...")
+    await query_db(conn2, "SELECT ...")
+
+The returned key must be hashable. ``cache_invalidate()`` and
+``cache_contains()`` compute the key the same way, so they accept the full
+argument list as usual. For decorated methods the key callable receives the
+instance as its first argument. Passing ``typed=True`` together with ``key``
+raises ``ValueError``, since ``typed`` only affects the default key
+computation.
+
 Limitations
 -----------
 
@@ -168,6 +194,40 @@
     cached_task_loop2 = alru_cache(maxsize=32)(my_task.__wrapped__)
     await cached_task_loop2(x)
 
+Security considerations
+-----------------------
+
+**Cache keys are built only from explicit arguments.** Like
+`functools.lru_cache 
<https://docs.python.org/3/library/functools.html#functools.lru_cache>`_,
+``alru_cache`` derives its cache key solely from the positional and keyword 
arguments
+passed to the wrapped function. Implicit, request-scoped context — such as
+authentication headers, the current user or tenant, ``contextvars``, 
thread/task
+locals, or module globals — is **not** part of the key and therefore **not** 
isolated
+between callers.
+
+Because concurrent calls with the same key also share a single in-flight 
result (see
+the Usage section above), a value computed for one caller can be returned to 
another whenever
+their arguments are equal. In multi-tenant or multi-user services this can 
lead to
+cross-tenant data exposure if the cached coroutine's result depends on 
anything other
+than its explicit arguments.
+
+To use ``alru_cache`` safely in these contexts:
+
+- **Make the cached coroutine a pure function of its arguments.** Any value 
that
+  affects the result — ``user_id``, ``tenant_id``, role, locale, feature 
flags, etc. —
+  must be passed as an argument so it becomes part of the cache key, or use a 
separate
+  cache instance per security domain.
+- **Avoid caching context-dependent functions.** If a function reads 
request-scoped
+  state from ``contextvars``/globals rather than from its arguments, either 
refactor it
+  to take that state explicitly or do not cache it.
+- **Consider** ``typed=True`` **when callers may pass multiple types.** With 
the default
+  ``typed=False``, arguments that compare and hash equal share an entry (for 
example
+  ``1`` and ``1.0``, or ``True`` and ``1``). Pass ``typed=True`` to key such 
arguments
+  distinctly.
+- **Be wary of attacker-controlled key arguments.** Objects with unusual 
``__hash__`` /
+  ``__eq__`` semantics can collide unexpectedly; only use trusted, 
well-behaved values
+  as cache key components.
+
 Benchmarks
 ----------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/async_lru/__init__.py 
new/async_lru-2.4.0/async_lru/__init__.py
--- old/async_lru-2.3.0/async_lru/__init__.py   2026-03-19 02:04:08.000000000 
+0100
+++ new/async_lru-2.4.0/async_lru/__init__.py   2026-10-12 20:12:30.073536200 
+0200
@@ -4,24 +4,10 @@
 import random
 import sys
 import warnings
+from collections import OrderedDict
+from collections.abc import Callable, Coroutine, Hashable
 from functools import _CacheInfo, _make_key, partial, partialmethod
-from typing import (
-    Any,
-    Callable,
-    Coroutine,
-    Generic,
-    Hashable,
-    List,
-    Optional,
-    OrderedDict,
-    Type,
-    TypedDict,
-    TypeVar,
-    Union,
-    cast,
-    final,
-    overload,
-)
+from typing import Any, Generic, TypedDict, TypeVar, cast, final, overload
 
 
 if sys.version_info >= (3, 11):
@@ -33,7 +19,7 @@
     from asyncio.coroutines import _is_coroutine  # type: ignore[attr-defined]
 
 
-__version__ = "2.3.0"
+__version__ = "2.4.0"
 
 __all__ = ("AlruCacheLoopResetWarning", "alru_cache")
 
@@ -42,7 +28,7 @@
 _R = TypeVar("_R")
 _Coro = Coroutine[Any, Any, _R]
 _CB = Callable[..., _Coro[_R]]
-_CBP = Union[_CB[_R], "partial[_Coro[_R]]", "partialmethod[_Coro[_R]]"]
+_CBP = _CB[_R] | partial[_Coro[_R]] | partialmethod[_Coro[_R]]
 
 
 class AlruCacheLoopResetWarning(UserWarning):
@@ -52,16 +38,16 @@
 @final
 class _CacheParameters(TypedDict):
     typed: bool
-    maxsize: Optional[int]
+    maxsize: int | None
     tasks: int
     closed: bool
 
 
 @final
[email protected]
[email protected](slots=True)
 class _CacheItem(Generic[_R]):
     task: "asyncio.Task[_R]"
-    later_call: Optional[asyncio.Handle]
+    later_call: asyncio.Handle | None
     waiters: int
 
     def cancel(self) -> None:
@@ -72,13 +58,34 @@
 
 @final
 class _LRUCacheWrapper(Generic[_R]):
+    # No slot for _is_coroutine_marker: inspect.markcoroutinefunction() must
+    # store it in __dict__, which _LRUCacheWrapperInstanceMethod copies.
+    __slots__ = (
+        "__wrapped__",
+        "__maxsize",
+        "__typed",
+        "__ttl",
+        "__jitter",
+        "__key",
+        "__cache",
+        "__closed",
+        "__hits",
+        "__misses",
+        "__first_loop",
+        "__warned_loop_reset",
+        "_is_coroutine",
+        "__dict__",
+        "__weakref__",
+    )
+
     def __init__(
         self,
         fn: _CB[_R],
-        maxsize: Optional[int],
+        maxsize: int | None,
         typed: bool,
-        ttl: Optional[float],
-        jitter: Optional[float],
+        ttl: float | None,
+        jitter: float | None,
+        key: Callable[..., Hashable] | None = None,
     ) -> None:
         try:
             self.__module__ = fn.__module__
@@ -96,10 +103,21 @@
             self.__doc__ = fn.__doc__
         except AttributeError:
             pass
-        try:
-            self.__annotations__ = fn.__annotations__
-        except AttributeError:
-            pass
+        # Copy the lazy __annotate__ function; reading fn.__annotations__
+        # would force evaluation of deferred annotations (PEP 649).
+        if sys.version_info >= (3, 14):
+            try:
+                self.__annotate__ = fn.__annotate__
+            except AttributeError:
+                try:
+                    self.__annotations__ = fn.__annotations__
+                except AttributeError:
+                    pass
+        else:
+            try:
+                self.__annotations__ = fn.__annotations__
+            except AttributeError:
+                pass
         try:
             self.__dict__.update(fn.__dict__)
         except AttributeError:
@@ -113,15 +131,16 @@
         self.__typed = typed
         self.__ttl = ttl
         self.__jitter = jitter
+        self.__key = key
         self.__cache: OrderedDict[Hashable, _CacheItem[_R]] = OrderedDict()
         self.__closed = False
         self.__hits = 0
         self.__misses = 0
-        self.__first_loop: Optional[asyncio.AbstractEventLoop] = None
+        self.__first_loop: asyncio.AbstractEventLoop | None = None
         self.__warned_loop_reset = False
 
     @property
-    def __tasks(self) -> List["asyncio.Task[_R]"]:
+    def __tasks(self) -> list["asyncio.Task[_R]"]:
         # NOTE: I don't think we need to form a set first here but not
         # too sure we want it for guarantees
         return list(
@@ -155,11 +174,19 @@
 
         Does not affect hit/miss counters or LRU ordering.
         """
-        key = _make_key(args, kwargs, self.__typed)
+        # Inlined instead of a shared helper: an extra method call is
+        # measurable on these short code paths (CodSpeed).
+        if self.__key is not None:
+            key = self.__key(*args, **kwargs)
+        else:
+            key = _make_key(args, kwargs, self.__typed)
         return key in self.__cache
 
     def cache_invalidate(self, /, *args: Hashable, **kwargs: Any) -> bool:
-        key = _make_key(args, kwargs, self.__typed)
+        if self.__key is not None:
+            key = self.__key(*args, **kwargs)
+        else:
+            key = _make_key(args, kwargs, self.__typed)
 
         cache_item = self.__cache.pop(key, None)
         if cache_item is None:
@@ -258,7 +285,10 @@
         loop = asyncio.get_running_loop()
         self._check_loop(loop)
 
-        key = _make_key(fn_args, fn_kwargs, self.__typed)
+        if self.__key is not None:
+            key = self.__key(*fn_args, **fn_kwargs)
+        else:
+            key = _make_key(fn_args, fn_kwargs, self.__typed)
         cache_item = self.__cache.get(key)
 
         if cache_item is not None:
@@ -287,8 +317,8 @@
         return await self._shield_and_handle_cancelled_error(cache_item, key)
 
     def __get__(
-        self, instance: _T, owner: Optional[Type[_T]]
-    ) -> Union[Self, "_LRUCacheWrapperInstanceMethod[_R, _T]"]:
+        self, instance: _T, owner: type[_T] | None
+    ) -> Self | "_LRUCacheWrapperInstanceMethod[_R, _T]":
         if owner is None:
             return self
         else:
@@ -297,6 +327,15 @@
 
 @final
 class _LRUCacheWrapperInstanceMethod(Generic[_R, _T]):
+    __slots__ = (
+        "__dict__",
+        "__weakref__",
+        "_is_coroutine",
+        "__wrapped__",
+        "__instance",
+        "__wrapper",
+    )
+
     def __init__(
         self,
         wrapper: _LRUCacheWrapper[_R],
@@ -318,10 +357,21 @@
             self.__doc__ = wrapper.__doc__
         except AttributeError:
             pass
-        try:
-            self.__annotations__ = wrapper.__annotations__
-        except AttributeError:
-            pass
+        # Prefer the lazy __annotate__ function, see the matching logic
+        # in _LRUCacheWrapper.__init__.
+        if sys.version_info >= (3, 14):
+            try:
+                self.__annotate__ = wrapper.__annotate__
+            except AttributeError:
+                try:
+                    self.__annotations__ = wrapper.__annotations__
+                except AttributeError:
+                    pass
+        else:
+            try:
+                self.__annotations__ = wrapper.__annotations__
+            except AttributeError:
+                pass
         try:
             self.__dict__.update(wrapper.__dict__)
         except AttributeError:
@@ -370,15 +420,20 @@
 
 
 def _make_wrapper(
-    maxsize: Optional[int],
+    maxsize: int | None,
     typed: bool,
-    ttl: Optional[float] = None,
-    jitter: Optional[float] = None,
+    ttl: float | None = None,
+    jitter: float | None = None,
+    key: Callable[..., Hashable] | None = None,
 ) -> Callable[[_CBP[_R]], _LRUCacheWrapper[_R]]:
     if jitter is not None and ttl is None:
         raise ValueError("jitter requires ttl to be set")
     if jitter is not None and jitter < 0:
         raise ValueError("jitter must be non-negative")
+    if key is not None and not callable(key):
+        raise TypeError("key must be callable")
+    if key is not None and typed:
+        raise ValueError("typed is ignored when a custom key callable is 
provided")
 
     def wrapper(fn: _CBP[_R]) -> _LRUCacheWrapper[_R]:
         origin = fn
@@ -392,7 +447,7 @@
         if hasattr(fn, "_make_unbound_method"):
             fn = fn._make_unbound_method()
 
-        wrapper = _LRUCacheWrapper(cast(_CB[_R], fn), maxsize, typed, ttl, 
jitter)
+        wrapper = _LRUCacheWrapper(cast(_CB[_R], fn), maxsize, typed, ttl, 
jitter, key)
         if sys.version_info >= (3, 12):
             wrapper = inspect.markcoroutinefunction(wrapper)
         return wrapper
@@ -402,11 +457,12 @@
 
 @overload
 def alru_cache(
-    maxsize: Optional[int] = 128,
+    maxsize: int | None = 128,
     typed: bool = False,
     *,
-    ttl: Optional[float] = None,
-    jitter: Optional[float] = None,
+    ttl: float | None = None,
+    jitter: float | None = None,
+    key: Callable[..., Hashable] | None = None,
 ) -> Callable[[_CBP[_R]], _LRUCacheWrapper[_R]]:
     ...
 
@@ -420,14 +476,15 @@
 
 
 def alru_cache(
-    maxsize: Union[Optional[int], _CBP[_R]] = 128,
+    maxsize: int | _CBP[_R] | None = 128,
     typed: bool = False,
     *,
-    ttl: Optional[float] = None,
-    jitter: Optional[float] = None,
-) -> Union[Callable[[_CBP[_R]], _LRUCacheWrapper[_R]], _LRUCacheWrapper[_R]]:
+    ttl: float | None = None,
+    jitter: float | None = None,
+    key: Callable[..., Hashable] | None = None,
+) -> Callable[[_CBP[_R]], _LRUCacheWrapper[_R]] | _LRUCacheWrapper[_R]:
     if maxsize is None or isinstance(maxsize, int):
-        return _make_wrapper(maxsize, typed, ttl, jitter)
+        return _make_wrapper(maxsize, typed, ttl, jitter, key)
     else:
         fn = cast(_CB[_R], maxsize)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/async_lru.egg-info/PKG-INFO 
new/async_lru-2.4.0/async_lru.egg-info/PKG-INFO
--- old/async_lru-2.3.0/async_lru.egg-info/PKG-INFO     2026-03-19 
02:04:16.000000000 +0100
+++ new/async_lru-2.4.0/async_lru.egg-info/PKG-INFO     2026-10-12 
20:12:35.831064000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: async-lru
-Version: 2.3.0
+Version: 2.4.0
 Summary: Simple LRU cache for asyncio
 Home-page: https://github.com/aio-libs/async-lru
 Maintainer: aiohttp team <[email protected]>
@@ -152,6 +152,32 @@
 The method returns `True` if the result for the given arguments is cached, 
`False`
 otherwise.
 
+Custom cache keys
+-----------------
+
+By default the cache key is built from all arguments, like
+``functools.lru_cache`` does. The ``key`` parameter accepts a callable that
+receives the same arguments as the wrapped function and returns the cache key,
+so arguments that do not affect the result can be excluded from it. This is an
+async-lru extension beyond the ``functools.lru_cache`` interface:
+
+.. code-block:: python
+
+    @alru_cache(key=lambda db, query: query)
+    async def query_db(db, query):
+        return await db.execute(query)
+
+    # Both calls share one cache entry despite the different connections.
+    await query_db(conn1, "SELECT ...")
+    await query_db(conn2, "SELECT ...")
+
+The returned key must be hashable. ``cache_invalidate()`` and
+``cache_contains()`` compute the key the same way, so they accept the full
+argument list as usual. For decorated methods the key callable receives the
+instance as its first argument. Passing ``typed=True`` together with ``key``
+raises ``ValueError``, since ``typed`` only affects the default key
+computation.
+
 Limitations
 -----------
 
@@ -199,6 +225,40 @@
     cached_task_loop2 = alru_cache(maxsize=32)(my_task.__wrapped__)
     await cached_task_loop2(x)
 
+Security considerations
+-----------------------
+
+**Cache keys are built only from explicit arguments.** Like
+`functools.lru_cache 
<https://docs.python.org/3/library/functools.html#functools.lru_cache>`_,
+``alru_cache`` derives its cache key solely from the positional and keyword 
arguments
+passed to the wrapped function. Implicit, request-scoped context — such as
+authentication headers, the current user or tenant, ``contextvars``, 
thread/task
+locals, or module globals — is **not** part of the key and therefore **not** 
isolated
+between callers.
+
+Because concurrent calls with the same key also share a single in-flight 
result (see
+the Usage section above), a value computed for one caller can be returned to 
another whenever
+their arguments are equal. In multi-tenant or multi-user services this can 
lead to
+cross-tenant data exposure if the cached coroutine's result depends on 
anything other
+than its explicit arguments.
+
+To use ``alru_cache`` safely in these contexts:
+
+- **Make the cached coroutine a pure function of its arguments.** Any value 
that
+  affects the result — ``user_id``, ``tenant_id``, role, locale, feature 
flags, etc. —
+  must be passed as an argument so it becomes part of the cache key, or use a 
separate
+  cache instance per security domain.
+- **Avoid caching context-dependent functions.** If a function reads 
request-scoped
+  state from ``contextvars``/globals rather than from its arguments, either 
refactor it
+  to take that state explicitly or do not cache it.
+- **Consider** ``typed=True`` **when callers may pass multiple types.** With 
the default
+  ``typed=False``, arguments that compare and hash equal share an entry (for 
example
+  ``1`` and ``1.0``, or ``True`` and ``1``). Pass ``typed=True`` to key such 
arguments
+  distinctly.
+- **Be wary of attacker-controlled key arguments.** Objects with unusual 
``__hash__`` /
+  ``__eq__`` semantics can collide unexpectedly; only use trusted, 
well-behaved values
+  as cache key components.
+
 Benchmarks
 ----------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/async_lru.egg-info/SOURCES.txt 
new/async_lru-2.4.0/async_lru.egg-info/SOURCES.txt
--- old/async_lru-2.3.0/async_lru.egg-info/SOURCES.txt  2026-03-19 
02:04:16.000000000 +0100
+++ new/async_lru-2.4.0/async_lru.egg-info/SOURCES.txt  2026-10-12 
20:12:35.836659200 +0200
@@ -19,8 +19,10 @@
 tests/test_cache_invalidate.py
 tests/test_cancel.py
 tests/test_close.py
+tests/test_deferred_annotations.py
 tests/test_exception.py
 tests/test_internals.py
+tests/test_key.py
 tests/test_partialmethod.py
 tests/test_readme_examples.py
 tests/test_size.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/setup.cfg 
new/async_lru-2.4.0/setup.cfg
--- old/async_lru-2.3.0/setup.cfg       2026-03-19 02:04:16.407925000 +0100
+++ new/async_lru-2.4.0/setup.cfg       2026-10-12 20:12:35.843384500 +0200
@@ -52,6 +52,8 @@
 max-line-length = 88
 extend-select = B950
 ignore = N801,N802,N803,E252,W503,E133,E203,E501
+per-file-ignores = 
+       tests/test_deferred_annotations.py:F821
 
 [coverage:run]
 branch = True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/conftest.py 
new/async_lru-2.4.0/tests/conftest.py
--- old/async_lru-2.3.0/tests/conftest.py       2026-03-19 02:04:08.000000000 
+0100
+++ new/async_lru-2.4.0/tests/conftest.py       2026-10-12 20:12:30.073536200 
+0200
@@ -1,5 +1,5 @@
+from collections.abc import Callable
 from functools import _CacheInfo
-from typing import Callable
 
 import pytest
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_basic.py 
new/async_lru-2.4.0/tests/test_basic.py
--- old/async_lru-2.3.0/tests/test_basic.py     2026-03-19 02:04:08.000000000 
+0100
+++ new/async_lru-2.4.0/tests/test_basic.py     2026-10-12 20:12:30.073994400 
+0200
@@ -1,8 +1,9 @@
 import asyncio
 import inspect
 import sys
+import weakref
+from collections.abc import Callable
 from functools import _CacheInfo, partial
-from typing import Callable
 
 import pytest
 
@@ -217,6 +218,27 @@
     )
 
 
+async def test_alru_cache_method_is_coroutine_function() -> None:
+    class A:
+        @alru_cache
+        async def coro(self, val: int) -> int:
+            return val
+
+        @classmethod
+        @alru_cache
+        async def cls_coro(cls, val: int) -> int:
+            return val
+
+    if sys.version_info >= (3, 12):
+        assert inspect.iscoroutinefunction(A().coro)
+        assert inspect.iscoroutinefunction(A.cls_coro)
+    if sys.version_info < (3, 14):
+        assert asyncio.iscoroutinefunction(A().coro)
+        assert asyncio.iscoroutinefunction(A.cls_coro)
+    assert await A().coro(1) == 1
+    assert await A.cls_coro(2) == 2
+
+
 async def test_invalidate_cache_for_method() -> None:
     class A:
         @alru_cache
@@ -231,3 +253,23 @@
     a.coro.cache_invalidate(42)
 
     assert a.coro.cache_info() == _CacheInfo(0, 1, 128, 0)
+
+
+async def test_alru_cache_weakref() -> None:
+    @alru_cache
+    async def coro(val: int) -> int:
+        return val
+
+    assert weakref.ref(coro)() is coro
+    assert await coro(1) == 1
+
+
+async def test_alru_cache_method_weakref() -> None:
+    class A:
+        @alru_cache
+        async def coro(self, val: int) -> int:
+            return val
+
+    method = A().coro
+    assert weakref.ref(method)() is method
+    assert await method(1) == 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_cache_clear.py 
new/async_lru-2.4.0/tests/test_cache_clear.py
--- old/async_lru-2.3.0/tests/test_cache_clear.py       2026-03-19 
02:04:08.000000000 +0100
+++ new/async_lru-2.4.0/tests/test_cache_clear.py       2026-10-12 
20:12:30.073994400 +0200
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable
+from collections.abc import Callable
 
 from async_lru import alru_cache
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_cache_contains.py 
new/async_lru-2.4.0/tests/test_cache_contains.py
--- old/async_lru-2.3.0/tests/test_cache_contains.py    2026-03-19 
02:04:08.000000000 +0100
+++ new/async_lru-2.4.0/tests/test_cache_contains.py    2026-10-12 
20:12:30.073994400 +0200
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable
+from collections.abc import Callable
 
 from async_lru import alru_cache
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_cache_info.py 
new/async_lru-2.4.0/tests/test_cache_info.py
--- old/async_lru-2.3.0/tests/test_cache_info.py        2026-03-19 
02:04:08.000000000 +0100
+++ new/async_lru-2.4.0/tests/test_cache_info.py        2026-10-12 
20:12:30.073994400 +0200
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable
+from collections.abc import Callable
 
 from async_lru import alru_cache
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_cache_invalidate.py 
new/async_lru-2.4.0/tests/test_cache_invalidate.py
--- old/async_lru-2.3.0/tests/test_cache_invalidate.py  2026-03-19 
02:04:08.000000000 +0100
+++ new/async_lru-2.4.0/tests/test_cache_invalidate.py  2026-10-12 
20:12:30.073994400 +0200
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable
+from collections.abc import Callable
 
 from async_lru import alru_cache
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_close.py 
new/async_lru-2.4.0/tests/test_close.py
--- old/async_lru-2.3.0/tests/test_close.py     2026-03-19 02:04:08.000000000 
+0100
+++ new/async_lru-2.4.0/tests/test_close.py     2026-10-12 20:12:30.073994400 
+0200
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable
+from collections.abc import Callable
 
 import pytest
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_deferred_annotations.py 
new/async_lru-2.4.0/tests/test_deferred_annotations.py
--- old/async_lru-2.3.0/tests/test_deferred_annotations.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/async_lru-2.4.0/tests/test_deferred_annotations.py      2026-10-12 
20:12:30.073994400 +0200
@@ -0,0 +1,98 @@
+"""Tests for PEP 649 deferred annotations (Python 3.14+).
+
+On Python 3.14 annotations are evaluated lazily via ``__annotate__``.
+Copying ``fn.__annotations__`` eagerly at decoration time forces that
+evaluation and crashes with ``NameError`` when the annotation refers to
+a name defined after the decorated function, a pattern that works with
+``functools.lru_cache``.
+"""
+
+import inspect
+import sys
+from functools import lru_cache, partial
+
+import pytest
+
+from async_lru import alru_cache
+
+
+requires_py314 = pytest.mark.skipif(
+    sys.version_info < (3, 14),
+    reason="deferred annotation evaluation requires Python 3.14",
+)
+
+
+@requires_py314
+async def test_deco_with_annotation_defined_after() -> None:
+    @alru_cache(maxsize=1)
+    async def get_foo() -> Foo:
+        return Foo()
+
+    class Foo:
+        pass
+
+    first = await get_foo()
+    assert isinstance(first, Foo)
+    assert await get_foo() is first
+
+
+@requires_py314
+async def test_annotations_stay_lazy_like_lru_cache() -> None:
+    @alru_cache(maxsize=1)
+    async def get_foo_async() -> Foo:
+        return Foo()
+
+    @lru_cache(maxsize=1)
+    def get_foo_sync() -> Foo:
+        return Foo()
+
+    class Foo:
+        pass
+
+    assert isinstance(await get_foo_async(), Foo)
+    assert isinstance(get_foo_sync(), Foo)
+    assert (
+        inspect.get_annotations(get_foo_async)
+        == inspect.get_annotations(get_foo_sync)
+        == {"return": Foo}
+    )
+
+
+@requires_py314
+async def test_unresolvable_annotation_raises_only_on_access() -> None:
+    @alru_cache(maxsize=1)
+    async def broken() -> Missing:  # type: ignore[name-defined]  # noqa: F821
+        pass  # pragma: no cover
+
+    with pytest.raises(NameError):
+        inspect.get_annotations(broken)
+
+
+async def test_method_wrapping_partial_without_annotation_attributes() -> None:
+    """A wrapped ``partial`` carries neither ``__annotate__`` nor
+    ``__annotations__``; binding it as a method copies neither."""
+
+    async def impl(self: object) -> int:
+        return 42
+
+    class Api:
+        meth = alru_cache(partial(impl))
+
+    api = Api()
+    assert await api.meth() == 42
+    assert not hasattr(api.meth, "__annotations__")
+
+
+@requires_py314
+async def test_method_annotations_stay_lazy() -> None:
+    class Api:
+        @alru_cache(maxsize=1)
+        async def get_foo(self) -> Foo:
+            return Foo()
+
+    class Foo:
+        pass
+
+    api = Api()
+    assert isinstance(await api.get_foo(), Foo)
+    assert inspect.get_annotations(api.get_foo) == {"return": Foo}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_exception.py 
new/async_lru-2.4.0/tests/test_exception.py
--- old/async_lru-2.3.0/tests/test_exception.py 2026-03-19 02:04:08.000000000 
+0100
+++ new/async_lru-2.4.0/tests/test_exception.py 2026-10-12 20:12:30.073994400 
+0200
@@ -1,7 +1,7 @@
 import asyncio
 import gc
 import sys
-from typing import Callable
+from collections.abc import Callable
 
 import pytest
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_key.py 
new/async_lru-2.4.0/tests/test_key.py
--- old/async_lru-2.3.0/tests/test_key.py       1970-01-01 01:00:00.000000000 
+0100
+++ new/async_lru-2.4.0/tests/test_key.py       2026-10-12 20:12:30.073994400 
+0200
@@ -0,0 +1,145 @@
+import asyncio
+from collections.abc import Callable
+
+import pytest
+
+from async_lru import alru_cache
+
+
+async def test_key_excludes_argument(check_lru: Callable[..., None]) -> None:
+    """Calls differing only in the excluded argument share one entry."""
+
+    @alru_cache(key=lambda conn, query: query)
+    async def query_db(conn: str, query: str) -> str:
+        return f"{conn}={query}"
+
+    assert await query_db("conn1", "SELECT 1") == "conn1=SELECT 1"
+    check_lru(query_db, hits=0, misses=1, cache=1, tasks=0)
+
+    assert await query_db("conn2", "SELECT 1") == "conn1=SELECT 1"
+    check_lru(query_db, hits=1, misses=1, cache=1, tasks=0)
+
+    assert await query_db("conn2", "SELECT 2") == "conn2=SELECT 2"
+    check_lru(query_db, hits=1, misses=2, cache=2, tasks=0)
+
+
+async def test_key_receives_kwargs(check_lru: Callable[..., None]) -> None:
+    @alru_cache(key=lambda conn, *, query: query)
+    async def query_db(conn: str, *, query: str) -> str:
+        return f"{conn}={query}"
+
+    assert await query_db("conn1", query="q") == "conn1=q"
+    assert await query_db("conn2", query="q") == "conn1=q"
+    check_lru(query_db, hits=1, misses=1, cache=1, tasks=0)
+
+
+async def test_key_cache_invalidate(check_lru: Callable[..., None]) -> None:
+    """cache_invalidate computes the same custom key as the call did."""
+
+    @alru_cache(key=lambda conn, query: query)
+    async def query_db(conn: str, query: str) -> str:
+        return f"{conn}={query}"
+
+    await query_db("conn1", "SELECT 1")
+    check_lru(query_db, hits=0, misses=1, cache=1, tasks=0)
+
+    # A different excluded argument still addresses the same entry.
+    assert query_db.cache_invalidate("conn9", "SELECT 1")
+    check_lru(query_db, hits=0, misses=1, cache=0, tasks=0)
+    assert not query_db.cache_invalidate("conn9", "SELECT 1")
+
+
+async def test_key_cache_contains(check_lru: Callable[..., None]) -> None:
+    @alru_cache(key=lambda conn, query: query)
+    async def query_db(conn: str, query: str) -> str:
+        return f"{conn}={query}"
+
+    await query_db("conn1", "SELECT 1")
+    assert query_db.cache_contains("conn9", "SELECT 1")
+    assert not query_db.cache_contains("conn9", "SELECT 2")
+
+
+def test_key_with_typed_raises() -> None:
+    with pytest.raises(ValueError, match="typed"):
+        alru_cache(typed=True, key=lambda x: x)
+
+
+def test_key_not_callable() -> None:
+    with pytest.raises(TypeError, match="callable"):
+        alru_cache(key="query")  # type: ignore[call-overload]
+
+
+async def test_typed_without_key_still_works() -> None:
+    @alru_cache(typed=True)
+    async def coro(val: int) -> int:
+        return val
+
+    assert coro.cache_parameters()["typed"] is True
+    assert await coro(1) == 1
+
+
+async def test_key_on_method(check_lru: Callable[..., None]) -> None:
+    """The key callable receives the instance for bound methods."""
+
+    class Api:
+        @alru_cache(key=lambda self, query: query)
+        async def fetch(self, query: str) -> str:
+            return f"{id(self)}={query}"
+
+    first = Api()
+    second = Api()
+    result = await first.fetch("SELECT 1")
+    # Another instance shares the entry since self is excluded.
+    assert await second.fetch("SELECT 1") == result
+    check_lru(Api.fetch, hits=1, misses=1, cache=1, tasks=0)
+
+
+async def test_key_unhashable_result(check_lru: Callable[..., None]) -> None:
+    @alru_cache(key=lambda val: [val])  # type: ignore[arg-type,return-value]
+    async def coro(val: int) -> int:
+        return val  # pragma: no cover  # the key fails before the call
+
+    with pytest.raises(TypeError, match="unhashable"):
+        await coro(1)
+    check_lru(coro, hits=0, misses=0, cache=0, tasks=0)
+
+
+async def test_key_callable_raising(check_lru: Callable[..., None]) -> None:
+    def broken_key(val: int) -> int:
+        raise RuntimeError("broken key")
+
+    @alru_cache(key=broken_key)
+    async def coro(val: int) -> int:
+        return val  # pragma: no cover  # the key fails before the call
+
+    with pytest.raises(RuntimeError, match="broken key"):
+        await coro(1)
+    check_lru(coro, hits=0, misses=0, cache=0, tasks=0)
+
+
+async def test_key_with_ttl(check_lru: Callable[..., None]) -> None:
+    @alru_cache(ttl=0.05, key=lambda conn, query: query)
+    async def query_db(conn: str, query: str) -> str:
+        return f"{conn}={query}"
+
+    await query_db("conn1", "SELECT 1")
+    assert await query_db("conn2", "SELECT 1") == "conn1=SELECT 1"
+    check_lru(query_db, hits=1, misses=1, cache=1, tasks=0)
+
+    await asyncio.sleep(0.06)
+    assert await query_db("conn2", "SELECT 1") == "conn2=SELECT 1"
+    check_lru(query_db, hits=1, misses=2, cache=1, tasks=0)
+
+
+async def test_key_with_maxsize(check_lru: Callable[..., None]) -> None:
+    @alru_cache(maxsize=1, key=lambda conn, query: query)
+    async def query_db(conn: str, query: str) -> str:
+        return f"{conn}={query}"
+
+    await query_db("conn1", "SELECT 1")
+    await query_db("conn1", "SELECT 2")
+    check_lru(query_db, hits=0, misses=2, cache=1, tasks=0, maxsize=1)
+
+    # The first entry was evicted; same query recomputes.
+    assert await query_db("conn2", "SELECT 1") == "conn2=SELECT 1"
+    check_lru(query_db, hits=0, misses=3, cache=1, tasks=0, maxsize=1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_partialmethod.py 
new/async_lru-2.4.0/tests/test_partialmethod.py
--- old/async_lru-2.3.0/tests/test_partialmethod.py     2026-03-19 
02:04:08.000000000 +0100
+++ new/async_lru-2.4.0/tests/test_partialmethod.py     2026-10-12 
20:12:30.073994400 +0200
@@ -1,6 +1,6 @@
 import asyncio
+from collections.abc import Callable
 from functools import partial, partialmethod
-from typing import Callable
 
 from async_lru import alru_cache
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_size.py 
new/async_lru-2.4.0/tests/test_size.py
--- old/async_lru-2.3.0/tests/test_size.py      2026-03-19 02:04:08.000000000 
+0100
+++ new/async_lru-2.4.0/tests/test_size.py      2026-10-12 20:12:30.073994400 
+0200
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable
+from collections.abc import Callable
 
 from async_lru import alru_cache
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/async_lru-2.3.0/tests/test_ttl.py 
new/async_lru-2.4.0/tests/test_ttl.py
--- old/async_lru-2.3.0/tests/test_ttl.py       2026-03-19 02:04:08.000000000 
+0100
+++ new/async_lru-2.4.0/tests/test_ttl.py       2026-10-12 20:12:30.073994400 
+0200
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable
+from collections.abc import Callable
 from unittest import mock
 
 import pytest

Reply via email to