|
16 | 16 | from concurrent.futures import ThreadPoolExecutor |
17 | 17 | from datetime import datetime, timedelta |
18 | 18 | from functools import wraps |
19 | | -from typing import Any, Callable, Optional, Union |
| 19 | +from typing import Any, Callable, Optional, ParamSpec, Protocol, TypeVar, Union |
20 | 20 | from warnings import warn |
21 | 21 |
|
22 | 22 | from ._types import RedisClient, S3Client |
|
31 | 31 | from .metrics import CacheMetrics, MetricsContext |
32 | 32 | from .util import parse_bytes |
33 | 33 |
|
| 34 | +_P = ParamSpec("_P") |
| 35 | +_R = TypeVar("_R") |
| 36 | +_R_co = TypeVar("_R_co", covariant=True) |
| 37 | + |
| 38 | + |
| 39 | +class _CachierWrappedFunc(Protocol[_P, _R_co]): |
| 40 | + """Callable returned by ``@cachier`` with the decorated function's signature. |
| 41 | +
|
| 42 | + Preserves the original function's parameter and return types via ``ParamSpec`` |
| 43 | + while also exposing the cache-management attributes attached by the decorator. |
| 44 | + Per-call cachier options such as ``max_age`` and ``cachier__skip_cache`` are |
| 45 | + accepted at runtime but are not surfaced in the ``__call__`` signature here; |
| 46 | + PEP 612 does not permit mixing ParamSpec kwargs with additional keyword-only |
| 47 | + parameters. |
| 48 | +
|
| 49 | + """ |
| 50 | + |
| 51 | + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... # pragma: no cover |
| 52 | + |
| 53 | + clear_cache: Callable[[], Any] |
| 54 | + clear_being_calculated: Callable[[], Any] |
| 55 | + aclear_cache: Callable[[], Any] |
| 56 | + aclear_being_calculated: Callable[[], Any] |
| 57 | + cache_dpath: Callable[[], Optional[str]] |
| 58 | + precache_value: Callable[..., Any] |
| 59 | + metrics: Optional[CacheMetrics] |
| 60 | + |
| 61 | + |
34 | 62 | MAX_WORKERS_ENVAR_NAME = "CACHIER_MAX_WORKERS" |
35 | 63 | DEFAULT_MAX_WORKERS = 8 |
36 | 64 | ZERO_TIMEDELTA = timedelta(seconds=0) |
@@ -221,7 +249,7 @@ def cachier( |
221 | 249 | allow_non_static_methods: Optional[bool] = None, |
222 | 250 | enable_metrics: bool = False, |
223 | 251 | metrics_sampling_rate: float = 1.0, |
224 | | -): |
| 252 | +) -> Callable[[Callable[_P, _R]], _CachierWrappedFunc[_P, _R]]: |
225 | 253 | """Wrap as a persistent, stale-free memoization decorator. |
226 | 254 |
|
227 | 255 | The positional and keyword arguments to the wrapped function must be |
@@ -400,7 +428,7 @@ def cachier( |
400 | 428 | else: |
401 | 429 | raise ValueError("specified an invalid core: %s" % backend) |
402 | 430 |
|
403 | | - def _cachier_decorator(func): |
| 431 | + def _cachier_decorator(func: Callable[_P, _R]) -> _CachierWrappedFunc[_P, _R]: |
404 | 432 | core.set_func(func) |
405 | 433 |
|
406 | 434 | # Guard: raise TypeError when decorating an instance method unless |
@@ -513,7 +541,7 @@ def _call(*args, max_age: Optional[timedelta] = None, **kwds): |
513 | 541 | from .config import _global_params |
514 | 542 |
|
515 | 543 | if ignore_cache or not _global_params.caching_enabled: |
516 | | - return func(args[0], **kwargs) if core.func_is_method else func(**kwargs) |
| 544 | + return func(args[0], **kwargs) if core.func_is_method else func(**kwargs) # type: ignore[call-arg] |
517 | 545 |
|
518 | 546 | with MetricsContext(cache_metrics) as _mctx: |
519 | 547 | key, entry = core.get_entry((), kwargs) |
@@ -629,7 +657,7 @@ async def _call_async(*args, max_age: Optional[timedelta] = None, **kwds): |
629 | 657 | from .config import _global_params |
630 | 658 |
|
631 | 659 | if ignore_cache or not _global_params.caching_enabled: |
632 | | - return await func(args[0], **kwargs) if core.func_is_method else await func(**kwargs) |
| 660 | + return await func(args[0], **kwargs) if core.func_is_method else await func(**kwargs) # type: ignore[call-arg,misc] |
633 | 661 |
|
634 | 662 | with MetricsContext(cache_metrics) as _mctx: |
635 | 663 | key, entry = await core.aget_entry((), kwargs) |
@@ -699,14 +727,14 @@ async def _call_async(*args, max_age: Optional[timedelta] = None, **kwds): |
699 | 727 | if is_coroutine: |
700 | 728 |
|
701 | 729 | @wraps(func) |
702 | | - async def func_wrapper(*args, **kwargs): |
703 | | - return await _call_async(*args, **kwargs) |
| 730 | + async def func_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: |
| 731 | + return await _call_async(*args, **kwargs) # type: ignore[arg-type] |
704 | 732 |
|
705 | 733 | else: |
706 | 734 |
|
707 | 735 | @wraps(func) |
708 | | - def func_wrapper(*args, **kwargs): |
709 | | - return _call(*args, **kwargs) |
| 736 | + def func_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: |
| 737 | + return _call(*args, **kwargs) # type: ignore[arg-type] |
710 | 738 |
|
711 | 739 | def _clear_cache(): |
712 | 740 | """Clear the cache.""" |
@@ -751,13 +779,13 @@ def _precache_value(*args, value_to_cache, **kwds): |
751 | 779 | kwargs = _convert_args_kwargs(func, _is_method=core.func_is_method, args=args, kwds=kwds) |
752 | 780 | return core.precache_value((), kwargs, value_to_cache) |
753 | 781 |
|
754 | | - func_wrapper.clear_cache = _clear_cache |
755 | | - func_wrapper.clear_being_calculated = _clear_being_calculated |
756 | | - func_wrapper.aclear_cache = _aclear_cache |
757 | | - func_wrapper.aclear_being_calculated = _aclear_being_calculated |
758 | | - func_wrapper.cache_dpath = _cache_dpath |
759 | | - func_wrapper.precache_value = _precache_value |
760 | | - func_wrapper.metrics = cache_metrics # Expose metrics object |
761 | | - return func_wrapper |
| 782 | + func_wrapper.clear_cache = _clear_cache # type: ignore[attr-defined] |
| 783 | + func_wrapper.clear_being_calculated = _clear_being_calculated # type: ignore[attr-defined] |
| 784 | + func_wrapper.aclear_cache = _aclear_cache # type: ignore[attr-defined] |
| 785 | + func_wrapper.aclear_being_calculated = _aclear_being_calculated # type: ignore[attr-defined] |
| 786 | + func_wrapper.cache_dpath = _cache_dpath # type: ignore[attr-defined] |
| 787 | + func_wrapper.precache_value = _precache_value # type: ignore[attr-defined] |
| 788 | + func_wrapper.metrics = cache_metrics # type: ignore[attr-defined] |
| 789 | + return func_wrapper # type: ignore[return-value] |
762 | 790 |
|
763 | 791 | return _cachier_decorator |
0 commit comments