Skip to content

Commit 5c07e1c

Browse files
akxchalmerlowe
andauthored
feat: drop cachetools dependency in favor of simple local implementation (#1590)
This PR drops the dependency on `cachetools`, which was only used for its LRU cache class, in favor of a simple local implementation. This should have a small but positive effect on many downstream users given how many times this library is downloaded per day. --------- Co-authored-by: Chalmer Lowe <[email protected]>
1 parent 2cbc2a2 commit 5c07e1c

File tree

6 files changed

+149
-7
lines changed

6 files changed

+149
-7
lines changed

google/auth/_cache.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections import OrderedDict
16+
17+
18+
class LRUCache(dict):
19+
def __init__(self, maxsize):
20+
super().__init__()
21+
self._order = OrderedDict()
22+
self.maxsize = maxsize
23+
24+
def clear(self):
25+
super().clear()
26+
self._order.clear()
27+
28+
def get(self, key, default=None):
29+
try:
30+
value = super().__getitem__(key)
31+
self._update(key)
32+
return value
33+
except KeyError:
34+
return default
35+
36+
def __getitem__(self, key):
37+
value = super().__getitem__(key)
38+
self._update(key)
39+
return value
40+
41+
def __setitem__(self, key, value):
42+
maxsize = self.maxsize
43+
if maxsize <= 0:
44+
return
45+
if key not in self:
46+
while len(self) >= maxsize:
47+
self.popitem()
48+
super().__setitem__(key, value)
49+
self._update(key)
50+
51+
def __delitem__(self, key):
52+
super().__delitem__(key)
53+
del self._order[key]
54+
55+
def popitem(self):
56+
"""Remove and return the least recently used key-value pair."""
57+
key, _ = self._order.popitem(last=False)
58+
return key, super().pop(key)
59+
60+
def _update(self, key):
61+
try:
62+
self._order.move_to_end(key)
63+
except KeyError:
64+
self._order[key] = None

google/auth/jwt.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@
5050
import json
5151
import urllib
5252

53-
import cachetools
54-
53+
from google.auth import _cache
5554
from google.auth import _helpers
5655
from google.auth import _service_account_info
5756
from google.auth import crypt
@@ -630,7 +629,7 @@ def __init__(
630629
token_lifetime (int): The amount of time in seconds for
631630
which the token is valid. Defaults to 1 hour.
632631
max_cache_size (int): The maximum number of JWT tokens to keep in
633-
cache. Tokens are cached using :class:`cachetools.LRUCache`.
632+
cache. Tokens are cached using :class:`google.auth._cache.LRUCache`.
634633
quota_project_id (Optional[str]): The project ID used for quota
635634
and billing.
636635
@@ -646,7 +645,7 @@ def __init__(
646645
additional_claims = {}
647646

648647
self._additional_claims = additional_claims
649-
self._cache = cachetools.LRUCache(maxsize=max_cache_size)
648+
self._cache = _cache.LRUCache(maxsize=max_cache_size)
650649

651650
@classmethod
652651
def _from_signer_and_info(cls, signer, info, **kwargs):

noxfile.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ def mypy(session):
9797
session.install("-e", ".")
9898
session.install(
9999
"mypy",
100-
"types-cachetools",
101100
"types-certifi",
102101
"types-freezegun",
103102
"types-pyOpenSSL",

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121

2222
DEPENDENCIES = (
23-
"cachetools>=2.0.0,<7.0",
2423
"pyasn1-modules>=0.2.1",
2524
# rsa==4.5 is the last version to support 2.7
2625
# https://github.com/sybrenstuvel/python-rsa/issues/152#issuecomment-643470233

testing/constraints-3.7.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
#
66
# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev",
77
# Then this file should have foo==1.14.0
8-
cachetools==2.0.0
98
pyasn1-modules==0.2.1
109
setuptools==40.3.0
1110
rsa==3.1.4

tests/test__cache.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from google.auth._cache import LRUCache
2+
3+
4+
def test_lru_cache():
5+
"""Test the LRUCache for generally expected functionality and ordering."""
6+
lru_cache = LRUCache(2)
7+
lru_cache["a"] = 1
8+
lru_cache["b"] = 2
9+
assert lru_cache["a"] == 1
10+
lru_cache["c"] = 3
11+
assert "b" not in lru_cache
12+
assert lru_cache["a"] == 1
13+
assert lru_cache["c"] == 3
14+
lru_cache["d"] = 4
15+
assert "a" not in lru_cache
16+
assert lru_cache["c"] == 3
17+
assert lru_cache["d"] == 4
18+
19+
20+
def test_zero_size_lru_cache():
21+
"""Confirm the LRUCache handles zero-size correctly."""
22+
lru_cache = LRUCache(0)
23+
lru_cache["a"] = 1
24+
assert "a" not in lru_cache
25+
26+
27+
def test_lru_cache_get_updates_lru():
28+
"""Confirm the LRUCache handles get calls correctly."""
29+
lru_cache = LRUCache(2)
30+
lru_cache["a"] = 1
31+
lru_cache["b"] = 2
32+
33+
# Access "a" via get(), making it MRU.
34+
assert lru_cache.get("a") == 1
35+
36+
# Add "c", which should evict "b" (LRU), not "a".
37+
lru_cache["c"] = 3
38+
39+
assert "a" in lru_cache
40+
assert "b" not in lru_cache
41+
assert "c" in lru_cache
42+
43+
44+
def test_lru_cache_get_missing():
45+
"""Confirm the LRUCache handles missing keys correctly."""
46+
lru_cache = LRUCache(2)
47+
assert lru_cache.get("missing") is None
48+
assert lru_cache.get("missing", "default") == "default"
49+
50+
51+
def test_lru_cache_clear():
52+
"""Confirm the LRUCache clears the cache properly."""
53+
lru_cache = LRUCache(2)
54+
lru_cache["a"] = 1
55+
lru_cache["b"] = 2
56+
assert len(lru_cache) == 2
57+
58+
lru_cache.clear()
59+
assert len(lru_cache) == 0
60+
assert "a" not in lru_cache
61+
assert "b" not in lru_cache
62+
# Ensure internal order is also cleared
63+
assert len(lru_cache._order) == 0
64+
65+
66+
def test_lru_cache_delitem():
67+
"""Confirm the LRUCache deletes individual items properly."""
68+
lru_cache = LRUCache(2)
69+
lru_cache["a"] = 1
70+
lru_cache["b"] = 2
71+
72+
del lru_cache["a"]
73+
assert "a" not in lru_cache
74+
assert len(lru_cache) == 1
75+
# Ensure it's removed from internal order
76+
assert "a" not in lru_cache._order
77+
78+
# Test that we can continue using the cache
79+
lru_cache["c"] = 3
80+
assert "c" in lru_cache
81+
assert "b" in lru_cache
82+
assert len(lru_cache) == 2

0 commit comments

Comments
 (0)