From 727e193d6dd40d3ed865f3333dac1acf8c6bb622 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 26 Aug 2024 13:51:54 +0300 Subject: [PATCH 1/4] feat: drop `cachetools` dependency in favor of simple local implementation --- google/auth/_cache.py | 42 +++++++++++++++++++++++++++++++++++++ google/auth/jwt.py | 7 +++---- noxfile.py | 1 - setup.py | 1 - testing/constraints-3.7.txt | 1 - tests/test__cache.py | 22 +++++++++++++++++++ 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 google/auth/_cache.py create mode 100644 tests/test__cache.py diff --git a/google/auth/_cache.py b/google/auth/_cache.py new file mode 100644 index 000000000..f03480230 --- /dev/null +++ b/google/auth/_cache.py @@ -0,0 +1,42 @@ +from collections import OrderedDict + + +class LRUCache(dict): + def __init__(self, maxsize): + super().__init__() + self._order = OrderedDict() + self.maxsize = maxsize + + def clear(self): + super().clear() + self._order.clear() + + def __getitem__(self, key): + value = super().__getitem__(key) + self._update(key) + return value + + def __setitem__(self, key, value): + maxsize = self.maxsize + if maxsize <= 0: + return + if key not in self: + while len(self) >= maxsize: + self.popitem() + super().__setitem__(key, value) + self._update(key) + + def __delitem__(self, key): + super().__delitem__(key) + del self._order[key] + + def popitem(self): + """Remove and return the least recently used key-value pair.""" + key, _ = self._order.popitem(last=False) + return key, super().pop(key) + + def _update(self, key): + try: + self._order.move_to_end(key) + except KeyError: + self._order[key] = None diff --git a/google/auth/jwt.py b/google/auth/jwt.py index 3b28bb518..b6fe60736 100644 --- a/google/auth/jwt.py +++ b/google/auth/jwt.py @@ -50,8 +50,7 @@ import json import urllib -import cachetools - +from google.auth import _cache from google.auth import _helpers from google.auth import _service_account_info from google.auth import crypt @@ -630,7 +629,7 @@ def __init__( token_lifetime (int): The amount of time in seconds for which the token is valid. Defaults to 1 hour. max_cache_size (int): The maximum number of JWT tokens to keep in - cache. Tokens are cached using :class:`cachetools.LRUCache`. + cache. Tokens are cached using :class:`google.auth._cache.LRUCache`. quota_project_id (Optional[str]): The project ID used for quota and billing. @@ -646,7 +645,7 @@ def __init__( additional_claims = {} self._additional_claims = additional_claims - self._cache = cachetools.LRUCache(maxsize=max_cache_size) + self._cache = _cache.LRUCache(maxsize=max_cache_size) @classmethod def _from_signer_and_info(cls, signer, info, **kwargs): diff --git a/noxfile.py b/noxfile.py index b2f9ae4ff..8a97f74cd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -97,7 +97,6 @@ def mypy(session): session.install("-e", ".") session.install( "mypy", - "types-cachetools", "types-certifi", "types-freezegun", "types-pyOpenSSL", diff --git a/setup.py b/setup.py index 51d946e5c..babb63b25 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,6 @@ DEPENDENCIES = ( - "cachetools>=2.0.0,<7.0", "pyasn1-modules>=0.2.1", # rsa==4.5 is the last version to support 2.7 # https://github.com/sybrenstuvel/python-rsa/issues/152#issuecomment-643470233 diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt index f3fd641a9..52ad3af91 100644 --- a/testing/constraints-3.7.txt +++ b/testing/constraints-3.7.txt @@ -5,7 +5,6 @@ # # e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev", # Then this file should have foo==1.14.0 -cachetools==2.0.0 pyasn1-modules==0.2.1 setuptools==40.3.0 rsa==3.1.4 diff --git a/tests/test__cache.py b/tests/test__cache.py new file mode 100644 index 000000000..6ac9326fb --- /dev/null +++ b/tests/test__cache.py @@ -0,0 +1,22 @@ +from google.auth._cache import LRUCache + + +def test_lru_cache(): + lru_cache = LRUCache(2) + lru_cache["a"] = 1 + lru_cache["b"] = 2 + assert lru_cache["a"] == 1 + lru_cache["c"] = 3 + assert "b" not in lru_cache + assert lru_cache["a"] == 1 + assert lru_cache["c"] == 3 + lru_cache["d"] = 4 + assert "a" not in lru_cache + assert lru_cache["c"] == 3 + assert lru_cache["d"] == 4 + + +def test_zero_size_lru_cache(): + lru_cache = LRUCache(0) + lru_cache["a"] = 1 + assert "a" not in lru_cache From f233c7c51de2d6f6459c8c9308e0abceb50f8599 Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Fri, 26 Dec 2025 10:52:09 -0500 Subject: [PATCH 2/4] fix: add `.get()` to LRUCache Co-authored-by: Aarni Koskela --- google/auth/_cache.py | 22 ++++++++++++++++++++++ tests/test__cache.py | 26 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/google/auth/_cache.py b/google/auth/_cache.py index f03480230..0a4a2af46 100644 --- a/google/auth/_cache.py +++ b/google/auth/_cache.py @@ -1,3 +1,17 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from collections import OrderedDict @@ -11,6 +25,14 @@ def clear(self): super().clear() self._order.clear() + def get(self, key, default=None): + try: + value = super().__getitem__(key) + self._update(key) + return value + except KeyError: + return default + def __getitem__(self, key): value = super().__getitem__(key) self._update(key) diff --git a/tests/test__cache.py b/tests/test__cache.py index 6ac9326fb..fc54a7487 100644 --- a/tests/test__cache.py +++ b/tests/test__cache.py @@ -2,6 +2,7 @@ def test_lru_cache(): + """Test the LRUCache for generally expected functionality and ordering.""" lru_cache = LRUCache(2) lru_cache["a"] = 1 lru_cache["b"] = 2 @@ -17,6 +18,31 @@ def test_lru_cache(): def test_zero_size_lru_cache(): + """Confirm the LRUCache handles zero-size correctly.""" lru_cache = LRUCache(0) lru_cache["a"] = 1 assert "a" not in lru_cache + + +def test_lru_cache_get_updates_lru(): + """Confirm the LRUCache handles get calls correctly.""" + lru_cache = LRUCache(2) + lru_cache["a"] = 1 + lru_cache["b"] = 2 + + # Access "a" via get(), making it MRU. + assert lru_cache.get("a") == 1 + + # Add "c", which should evict "b" (LRU), not "a". + lru_cache["c"] = 3 + + assert "a" in lru_cache + assert "b" not in lru_cache + assert "c" in lru_cache + + +def test_lru_cache_get_missing(): + """Confirm the LRUCache handles missing keys correctly.""" + lru_cache = LRUCache(2) + assert lru_cache.get("missing") is None + assert lru_cache.get("missing", "default") == "default" From bf9556b2ec6068f48c4c7213457417f4725377d9 Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Mon, 29 Dec 2025 06:38:12 -0500 Subject: [PATCH 3/4] fix: adds clear and delete item tests Adds tests to confirm the proper functionality of the `clear()` method and the `__delitem__()` method. Increases robustness and ensures code coverage. --- tests/test__cache.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test__cache.py b/tests/test__cache.py index fc54a7487..94bd49e4d 100644 --- a/tests/test__cache.py +++ b/tests/test__cache.py @@ -46,3 +46,37 @@ def test_lru_cache_get_missing(): lru_cache = LRUCache(2) assert lru_cache.get("missing") is None assert lru_cache.get("missing", "default") == "default" + + +def test_lru_cache_clear(): + """Confirm the LRUCache clears the cache properly.""" + lru_cache = LRUCache(2) + lru_cache["a"] = 1 + lru_cache["b"] = 2 + assert len(lru_cache) == 2 + + lru_cache.clear() + assert len(lru_cache) == 0 + assert "a" not in lru_cache + assert "b" not in lru_cache + # Ensure internal order is also cleared + assert len(lru_cache._order) == 0 + + +def test_lru_cache_delitem(): + """Confirm the LRUCache deletes individual items properly.""" + lru_cache = LRUCache(2) + lru_cache["a"] = 1 + lru_cache["b"] = 2 + + del lru_cache["a"] + assert "a" not in lru_cache + assert len(lru_cache) == 1 + # Ensure it's removed from internal order + assert "a" not in lru_cache._order + + # Test that we can continue using the cache + lru_cache["c"] = 3 + assert "c" in lru_cache + assert "b" in lru_cache + assert len(lru_cache) == 2 From b95e292c4aae5ed8005b7706db51e4c4cbd95b89 Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Mon, 29 Dec 2025 08:22:25 -0500 Subject: [PATCH 4/4] chore: updates linting (removes white space) --- tests/test__cache.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test__cache.py b/tests/test__cache.py index 94bd49e4d..be9f703fb 100644 --- a/tests/test__cache.py +++ b/tests/test__cache.py @@ -46,15 +46,15 @@ def test_lru_cache_get_missing(): lru_cache = LRUCache(2) assert lru_cache.get("missing") is None assert lru_cache.get("missing", "default") == "default" - - + + def test_lru_cache_clear(): """Confirm the LRUCache clears the cache properly.""" lru_cache = LRUCache(2) lru_cache["a"] = 1 lru_cache["b"] = 2 assert len(lru_cache) == 2 - + lru_cache.clear() assert len(lru_cache) == 0 assert "a" not in lru_cache @@ -68,13 +68,13 @@ def test_lru_cache_delitem(): lru_cache = LRUCache(2) lru_cache["a"] = 1 lru_cache["b"] = 2 - + del lru_cache["a"] assert "a" not in lru_cache assert len(lru_cache) == 1 # Ensure it's removed from internal order assert "a" not in lru_cache._order - + # Test that we can continue using the cache lru_cache["c"] = 3 assert "c" in lru_cache