Skip to content

Commit 72b1b85

Browse files
Vendor asyncache to allow directly depending on cachetools (#683)
1 parent 5950d18 commit 72b1b85

File tree

7 files changed

+819
-5
lines changed

7 files changed

+819
-5
lines changed

.pre-commit-config.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ repos:
66
hooks:
77
- id: black
88
language_version: python3
9-
exclude: versioneer.py
9+
exclude: versioneer.py|.*_vendored\/.*
1010
args:
1111
- --target-version=py38
1212
- repo: https://github.com/astral-sh/ruff-pre-commit
@@ -15,6 +15,7 @@ repos:
1515
hooks:
1616
- id: ruff
1717
args: [--fix, --exit-non-zero-on-fix]
18+
exclude: ".*_vendored\/.*"
1819
- repo: https://github.com/Lucas-C/pre-commit-hooks
1920
rev: v1.5.5
2021
hooks:
@@ -25,14 +26,16 @@ repos:
2526
- --use-current-year
2627
- --no-extra-eol
2728
- --detect-license-in-X-top-lines=5
29+
exclude: ".*_vendored\/.*"
2830
- repo: https://github.com/pre-commit/mirrors-mypy
2931
rev: 'v1.18.2'
3032
hooks:
3133
- id: mypy
32-
exclude: "examples|venv|ci|docs"
33-
additional_dependencies: [types-pyyaml>=6.0]
34+
exclude: "examples|venv|ci|docs|.*_vendored\/.*"
35+
additional_dependencies: [types-pyyaml>=6.0, types-cachetools]
3436
- repo: https://github.com/asottile/pyupgrade
3537
rev: v3.21.0
3638
hooks:
3739
- id: pyupgrade
3840
args: [--py39-plus]
41+
exclude: ".*_vendored\/.*"

kr8s/_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import anyio
2020
import httpx
2121
import httpx_ws
22-
from asyncache import cached # type: ignore
2322
from cachetools import TTLCache # type: ignore
2423
from cryptography import x509
2524
from packaging.version import parse as parse_version
@@ -31,6 +30,7 @@
3130
)
3231
from ._data_utils import dict_to_selector, sort_versions
3332
from ._exceptions import APITimeoutError, ServerError
33+
from ._vendored.asyncache import cached # type: ignore
3434
from ._version import __version__
3535

3636
if TYPE_CHECKING:

kr8s/_vendored/asyncache/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 hephex
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2018 hephex
2+
# SPDX-License-Identifier: MIT License
3+
# Original-Source: https://github.com/hephex/asyncache/blob/35c7966101f3b0c61c9254a03fb02cca6a9b2f50/asyncache/__init__.py
4+
"""
5+
Helpers to use [cachetools](https://github.com/tkem/cachetools) with
6+
asyncio.
7+
8+
This implementation is a fork of the original asyncache implementation by hephex
9+
which is licensed under the included MIT License.
10+
11+
The original implementation became unmaintained and ran into dependency resolution issues.
12+
Vendoring in this tiny wrapper allows us to depend on cachetools directly.
13+
See https://github.com/kr8s-org/kr8s/issues/681 for more details.
14+
15+
If the cachetools package gets support for asyncio then we can remove these helpers and
16+
use cachetools directly. See https://github.com/tkem/cachetools/issues/222 for status on this.
17+
"""
18+
19+
import asyncio
20+
import functools
21+
from collections.abc import MutableMapping
22+
from contextlib import AbstractContextManager
23+
from typing import Any, Callable, Optional, Protocol, TypeVar
24+
25+
from cachetools import keys
26+
27+
__all__ = ["cached"]
28+
29+
30+
_KT = TypeVar("_KT")
31+
_T = TypeVar("_T")
32+
33+
34+
class IdentityFunction(Protocol): # pylint: disable=too-few-public-methods
35+
"""
36+
Type for a function returning the same type as the one it received.
37+
"""
38+
39+
def __call__(self, __x: _T) -> _T: ...
40+
41+
42+
class NullContext:
43+
"""A class for noop context managers."""
44+
45+
def __enter__(self):
46+
"""Return ``self`` upon entering the runtime context."""
47+
return self
48+
49+
def __exit__(self, exc_type, exc_value, traceback):
50+
"""Raise any exception triggered within the runtime context."""
51+
return None
52+
53+
async def __aenter__(self):
54+
"""Return ``self`` upon entering the runtime context."""
55+
return self
56+
57+
async def __aexit__(self, exc_type, exc_value, traceback):
58+
"""Raise any exception triggered within the runtime context."""
59+
return None
60+
61+
62+
def cached(
63+
cache: Optional[MutableMapping[_KT, Any]],
64+
# ignoring the mypy error to be consistent with the type used
65+
# in https://github.com/python/typeshed/tree/master/stubs/cachetools
66+
key: Callable[..., _KT] = keys.hashkey, # type:ignore
67+
lock: Optional["AbstractContextManager[Any]"] = None,
68+
) -> IdentityFunction:
69+
"""
70+
Decorator to wrap a function or a coroutine with a memoizing callable
71+
that saves results in a cache.
72+
73+
When ``lock`` is provided for a standard function, it's expected to
74+
implement ``__enter__`` and ``__exit__`` that will be used to lock
75+
the cache when gets updated. If it wraps a coroutine, ``lock``
76+
must implement ``__aenter__`` and ``__aexit__``.
77+
"""
78+
lock = lock or NullContext()
79+
80+
def decorator(func):
81+
if asyncio.iscoroutinefunction(func):
82+
83+
async def wrapper(*args, **kwargs):
84+
k = key(*args, **kwargs)
85+
try:
86+
async with lock:
87+
return cache[k]
88+
89+
except KeyError:
90+
pass # key not found
91+
92+
val = await func(*args, **kwargs)
93+
94+
try:
95+
async with lock:
96+
cache[k] = val
97+
98+
except ValueError:
99+
pass # val too large
100+
101+
return val
102+
103+
else:
104+
105+
def wrapper(*args, **kwargs):
106+
k = key(*args, **kwargs)
107+
try:
108+
with lock:
109+
return cache[k]
110+
111+
except KeyError:
112+
pass # key not found
113+
114+
val = func(*args, **kwargs)
115+
116+
try:
117+
with lock:
118+
cache[k] = val
119+
120+
except ValueError:
121+
pass # val too large
122+
123+
return val
124+
125+
return functools.wraps(func)(wrapper)
126+
127+
return decorator
128+
129+
130+
def cachedmethod(
131+
cache: Callable[[Any], Optional[MutableMapping[_KT, Any]]],
132+
# ignoring the mypy error to be consistent with the type used
133+
# in https://github.com/python/typeshed/tree/master/stubs/cachetools
134+
key: Callable[..., _KT] = keys.hashkey, # type:ignore
135+
lock: Optional[Callable[[Any], "AbstractContextManager[Any]"]] = None,
136+
) -> IdentityFunction:
137+
"""Decorator to wrap a class or instance method with a memoizing
138+
callable that saves results in a cache. This works similarly to
139+
`cached`, but the arguments `cache` and `lock` are callables that
140+
return the cache object and the lock respectively.
141+
"""
142+
lock = lock or (lambda _: NullContext())
143+
144+
def decorator(method):
145+
if asyncio.iscoroutinefunction(method):
146+
147+
async def wrapper(self, *args, **kwargs):
148+
method_cache = cache(self)
149+
if method_cache is None:
150+
return await method(self, *args, **kwargs)
151+
152+
k = key(self, *args, **kwargs)
153+
try:
154+
async with lock(self):
155+
return method_cache[k]
156+
157+
except KeyError:
158+
pass # key not found
159+
160+
val = await method(self, *args, **kwargs)
161+
162+
try:
163+
async with lock(self):
164+
method_cache[k] = val
165+
166+
except ValueError:
167+
pass # val too large
168+
169+
return val
170+
171+
else:
172+
173+
def wrapper(self, *args, **kwargs):
174+
method_cache = cache(self)
175+
if method_cache is None:
176+
return method(self, *args, **kwargs)
177+
178+
k = key(*args, **kwargs)
179+
try:
180+
with lock(self):
181+
return method_cache[k]
182+
183+
except KeyError:
184+
pass # key not found
185+
186+
val = method(self, *args, **kwargs)
187+
188+
try:
189+
with lock(self):
190+
method_cache[k] = val
191+
192+
except ValueError:
193+
pass # val too large
194+
195+
return val
196+
197+
return functools.wraps(method)(wrapper)
198+
199+
return decorator

0 commit comments

Comments
 (0)