diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml deleted file mode 100644 index 7128f382..00000000 --- a/.github/workflows/lock.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: 'Lock threads' - -on: - schedule: - - cron: '0 0 * * *' - -jobs: - lock: - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v2 - with: - github-token: ${{ github.token }} - issue-lock-inactive-days: 14 - pr-lock-inactive-days: 14 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2e9324b9..67eb1121 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -24,24 +24,17 @@ jobs: fail-fast: false matrix: include: - - {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39} - - {name: Mac, python: '3.9', os: macos-latest, tox: py39} + - {name: Linux, python: '3.11', os: ubuntu-latest, tox: py311} - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} + - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} - - {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing} + - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311} + - {name: Typing, python: '3.12', os: ubuntu-latest, tox: typing} steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - - name: install external dependencies Mac - if: matrix.os == 'macos-latest' - run: brew install libmemcached memcached redis - - name: Setup Docker on macOS - if: matrix.os == 'macos-latest' - uses: docker-practice/actions-setup-docker@master - timeout-minutes: 12 - name: install external dependencies Linux if: matrix.os == 'ubuntu-latest' run: | diff --git a/.gitignore b/.gitignore index 91b2d55e..6b15c65d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,9 @@ htmlcov/ .xprocess .vscode .python-version +/.idea/.gitignore +/.idea/cachelib.iml +/.idea/misc.xml +/.idea/modules.xml +/.idea/inspectionProfiles/profiles_settings.xml +/.idea/vcs.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 658d56ea..524db21a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,28 +2,28 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.15.0 hooks: - id: pyupgrade args: ["--py37-plus"] - - repo: https://github.com/asottile/reorder_python_imports - rev: v3.9.0 + - repo: https://github.com/asottile/reorder-python-imports + rev: v3.12.0 hooks: - id: reorder-python-imports args: ["--application-directories", "src"] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 7.0.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-implicit-str-concat - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: fix-byte-order-marker - id: trailing-whitespace diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0c363636..8e2f4e9a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,4 +1,8 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.10" python: install: - requirements: requirements/docs.txt diff --git a/requirements/dev.txt b/requirements/dev.txt index 7c457ba3..0d503063 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -6,29 +6,27 @@ # alabaster==0.7.12 # via sphinx -async-timeout==4.0.2 +async-timeout==4.0.3 # via redis -attrs==21.2.0 - # via pytest babel==2.9.1 # via sphinx -boto3==1.26.104 +boto3==1.34.38 # via -r tests.in -botocore==1.29.104 +botocore==1.34.38 # via # boto3 # s3transfer build==0.8.0 # via pip-tools -cachetools==5.3.0 +cachetools==5.3.2 # via tox -certifi==2022.12.7 +certifi==2023.7.22 # via requests cffi==1.15.1 # via cryptography cfgv==3.3.1 # via pre-commit -chardet==5.1.0 +chardet==5.2.0 # via tox charset-normalizer==2.0.7 # via requests @@ -36,17 +34,19 @@ click==8.0.3 # via pip-tools colorama==0.4.6 # via tox -cryptography==39.0.1 +cryptography==42.0.0 # via # types-pyopenssl # types-redis -distlib==0.3.6 +distlib==0.3.7 # via virtualenv docutils==0.18.1 # via # sphinx # sphinx-tabs -filelock==3.10.0 +exceptiongroup==1.2.0 + # via pytest +filelock==3.13.1 # via # tox # virtualenv @@ -54,11 +54,13 @@ identify==2.3.3 # via pre-commit idna==3.3 # via requests -imagesize==1.2.0 +imagesize==1.4.1 + # via sphinx +importlib-metadata==6.8.0 # via sphinx iniconfig==1.1.1 # via pytest -jinja2==3.0.2 +jinja2==3.1.3 # via sphinx jmespath==1.0.1 # via @@ -66,13 +68,13 @@ jmespath==1.0.1 # botocore markupsafe==2.0.1 # via jinja2 -mypy==1.1.1 +mypy==1.8.0 # via -r typing.in mypy-extensions==1.0.0 # via mypy nodeenv==1.6.0 # via pre-commit -packaging==23.0 +packaging==23.2 # via # build # pallets-sphinx-themes @@ -80,41 +82,39 @@ packaging==23.0 # pytest # sphinx # tox -pallets-sphinx-themes==2.0.3 +pallets-sphinx-themes==2.1.1 # via -r docs.in pep517==0.12.0 # via build -pip-tools==6.12.3 +pip-tools==7.3.0 # via -r dev.in -platformdirs==3.1.1 +platformdirs==4.2.0 # via # tox # virtualenv -pluggy==1.0.0 +pluggy==1.3.0 # via # pytest # tox -pre-commit==3.2.1 +pre-commit==3.6.0 # via -r dev.in psutil==5.8.0 # via pytest-xprocess -py==1.10.0 - # via pytest-xprocess pycparser==2.21 # via cffi -pygments==2.10.0 +pygments==2.15.0 # via # sphinx # sphinx-tabs pylibmc==1.6.3 # via -r tests.in -pyproject-api==1.5.1 +pyproject-api==1.6.1 # via tox -pytest==7.2.2 +pytest==8.0.0 # via # -r tests.in # pytest-xprocess -pytest-xprocess==0.22.2 +pytest-xprocess==0.23.0 # via -r tests.in python-dateutil==2.8.2 # via botocore @@ -122,26 +122,26 @@ pytz==2021.3 # via babel pyyaml==6.0 # via pre-commit -redis==4.5.1 +redis==5.0.1 # via -r tests.in -requests==2.26.0 +requests==2.31.0 # via sphinx -s3transfer==0.6.0 +s3transfer==0.10.0 # via boto3 six==1.16.0 # via python-dateutil snowballstemmer==2.1.0 # via sphinx -sphinx==5.1.1 +sphinx==7.2.6 # via # -r docs.in # pallets-sphinx-themes # sphinx-issues # sphinx-tabs # sphinxcontrib-log-cabinet -sphinx-issues==3.0.1 +sphinx-issues==4.0.0 # via -r docs.in -sphinx-tabs==3.4.1 +sphinx-tabs==3.4.5 # via -r docs.in sphinxcontrib-applehelp==1.0.2 # via sphinx @@ -155,30 +155,39 @@ sphinxcontrib-log-cabinet==1.0.1 # via -r docs.in sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.10 # via sphinx tomli==2.0.1 - # via pep517 -tox==4.4.8 + # via + # build + # mypy + # pep517 + # pip-tools + # pyproject-api + # pytest + # tox +tox==4.12.1 # via -r dev.in types-pyopenssl==23.0.0.2 # via types-redis -types-redis==4.5.4.1 +types-redis==4.6.0.20240106 # via -r typing.in -typing-extensions==3.10.0.2 +typing-extensions==4.8.0 # via mypy -urllib3==1.26.7 +urllib3==1.26.18 # via # botocore # requests -uwsgi==2.0.21 +uwsgi==2.0.23 # via -r tests.in -virtualenv==20.21.0 +virtualenv==20.25.0 # via # pre-commit # tox wheel==0.38.1 # via pip-tools +zipp==3.17.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/docs.txt b/requirements/docs.txt index ce7161d9..a7769ff5 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -8,7 +8,7 @@ alabaster==0.7.12 # via sphinx babel==2.9.1 # via sphinx -certifi==2022.12.7 +certifi==2023.7.22 # via requests charset-normalizer==2.0.7 # via requests @@ -18,9 +18,11 @@ docutils==0.18.1 # sphinx-tabs idna==3.3 # via requests -imagesize==1.2.0 +imagesize==1.4.1 # via sphinx -jinja2==3.0.2 +importlib-metadata==7.0.1 + # via sphinx +jinja2==3.1.3 # via sphinx markupsafe==2.0.1 # via jinja2 @@ -28,9 +30,9 @@ packaging==21.2 # via # pallets-sphinx-themes # sphinx -pallets-sphinx-themes==2.0.3 +pallets-sphinx-themes==2.1.1 # via -r docs.in -pygments==2.10.0 +pygments==2.15.0 # via # sphinx # sphinx-tabs @@ -38,20 +40,20 @@ pyparsing==2.4.7 # via packaging pytz==2021.3 # via babel -requests==2.26.0 +requests==2.31.0 # via sphinx snowballstemmer==2.1.0 # via sphinx -sphinx==5.1.1 +sphinx==7.2.6 # via # -r docs.in # pallets-sphinx-themes # sphinx-issues # sphinx-tabs # sphinxcontrib-log-cabinet -sphinx-issues==3.0.1 +sphinx-issues==4.0.0 # via -r docs.in -sphinx-tabs==3.4.1 +sphinx-tabs==3.4.5 # via -r docs.in sphinxcontrib-applehelp==1.0.2 # via sphinx @@ -65,7 +67,9 @@ sphinxcontrib-log-cabinet==1.0.1 # via -r docs.in sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.10 # via sphinx -urllib3==1.26.7 +urllib3==1.26.18 # via requests +zipp==3.17.0 + # via importlib-metadata diff --git a/requirements/typing.txt b/requirements/typing.txt index 0d4f7edf..7f9c8388 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -6,19 +6,21 @@ # cffi==1.15.1 # via cryptography -cryptography==39.0.1 +cryptography==42.0.0 # via # types-pyopenssl # types-redis -mypy==1.1.1 +mypy==1.8.0 # via -r typing.in mypy-extensions==1.0.0 # via mypy pycparser==2.21 # via cffi +tomli==2.0.1 + # via mypy types-pyopenssl==23.0.0.2 # via types-redis -types-redis==4.5.4.1 +types-redis==4.6.0.20240106 # via -r typing.in -typing-extensions==3.10.0.2 +typing-extensions==4.9.0 # via mypy diff --git a/setup.cfg b/setup.cfg index d6fe0618..9a9741ee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ classifiers = packages = find: package_dir = = src include_package_data = true -python_requires = >= 3.7 +python_requires = >= 3.8 [options.packages.find] where = src diff --git a/src/cachelib/__init__.py b/src/cachelib/__init__.py index 90d3363c..2d477612 100644 --- a/src/cachelib/__init__.py +++ b/src/cachelib/__init__.py @@ -19,4 +19,4 @@ "DynamoDbCache", "MongoDbCache", ] -__version__ = "0.10.2" +__version__ = "0.11.0" diff --git a/src/cachelib/file.py b/src/cachelib/file.py index e4d08b6b..02a3b0e1 100644 --- a/src/cachelib/file.py +++ b/src/cachelib/file.py @@ -148,10 +148,7 @@ def _remove_older(self) -> bool: exc_info=True, ) fname_sorted = ( - fname - for _, fname in sorted( - exp_fname_tuples, key=lambda item: item[0] # type: ignore - ) + fname for _, fname in sorted(exp_fname_tuples, key=lambda item: item[0]) ) for fname in fname_sorted: try: diff --git a/src/cachelib/memcached.py b/src/cachelib/memcached.py index 4fbcce68..7c2f70ad 100644 --- a/src/cachelib/memcached.py +++ b/src/cachelib/memcached.py @@ -9,7 +9,6 @@ class MemcachedCache(BaseCache): - """A cache that uses memcached as backend. The first argument can either be an object that resembles the API of a diff --git a/src/cachelib/redis.py b/src/cachelib/redis.py index 8a0b0da8..8e38115b 100644 --- a/src/cachelib/redis.py +++ b/src/cachelib/redis.py @@ -37,8 +37,8 @@ def __init__( password: _t.Optional[str] = None, db: int = 0, default_timeout: int = 300, - key_prefix: _t.Optional[str] = None, - **kwargs: _t.Any + key_prefix: _t.Optional[_t.Union[str, _t.Callable[[], str]]] = None, + **kwargs: _t.Any, ): BaseCache.__init__(self, default_timeout) if host is None: @@ -57,6 +57,11 @@ def __init__( self._read_client = self._write_client = host self.key_prefix = key_prefix or "" + def _get_prefix(self) -> str: + return ( + self.key_prefix if isinstance(self.key_prefix, str) else self.key_prefix() + ) + def _normalize_timeout(self, timeout: _t.Optional[int]) -> int: """Normalize timeout by setting it to default of 300 if not defined (None) or -1 if explicitly set to zero. @@ -69,11 +74,13 @@ def _normalize_timeout(self, timeout: _t.Optional[int]) -> int: return timeout def get(self, key: str) -> _t.Any: - return self.serializer.loads(self._read_client.get(self.key_prefix + key)) + return self.serializer.loads( + self._read_client.get(f"{self._get_prefix()}{key}") + ) def get_many(self, *keys: str) -> _t.List[_t.Any]: if self.key_prefix: - prefixed_keys = [self.key_prefix + key for key in keys] + prefixed_keys = [f"{self._get_prefix()}{key}" for key in keys] else: prefixed_keys = list(keys) return [self.serializer.loads(x) for x in self._read_client.mget(prefixed_keys)] @@ -82,20 +89,24 @@ def set(self, key: str, value: _t.Any, timeout: _t.Optional[int] = None) -> _t.A timeout = self._normalize_timeout(timeout) dump = self.serializer.dumps(value) if timeout == -1: - result = self._write_client.set(name=self.key_prefix + key, value=dump) + result = self._write_client.set( + name=f"{self._get_prefix()}{key}", value=dump + ) else: result = self._write_client.setex( - name=self.key_prefix + key, value=dump, time=timeout + name=f"{self._get_prefix()}{key}", value=dump, time=timeout ) return result def add(self, key: str, value: _t.Any, timeout: _t.Optional[int] = None) -> _t.Any: timeout = self._normalize_timeout(timeout) dump = self.serializer.dumps(value) - created = self._write_client.setnx(name=self.key_prefix + key, value=dump) + created = self._write_client.setnx( + name=f"{self._get_prefix()}{key}", value=dump + ) # handle case where timeout is explicitly set to zero if created and timeout != -1: - self._write_client.expire(name=self.key_prefix + key, time=timeout) + self._write_client.expire(name=f"{self._get_prefix()}{key}", time=timeout) return created def set_many( @@ -109,33 +120,32 @@ def set_many( for key, value in mapping.items(): dump = self.serializer.dumps(value) if timeout == -1: - pipe.set(name=self.key_prefix + key, value=dump) + pipe.set(name=f"{self._get_prefix()}{key}", value=dump) else: - pipe.setex(name=self.key_prefix + key, value=dump, time=timeout) + pipe.setex(name=f"{self._get_prefix()}{key}", value=dump, time=timeout) results = pipe.execute() - res = zip(mapping.keys(), results) # noqa: B905 - return [k for k, was_set in res if was_set] + return [k for k, was_set in zip(mapping.keys(), results) if was_set] def delete(self, key: str) -> bool: - return bool(self._write_client.delete(self.key_prefix + key)) + return bool(self._write_client.delete(f"{self._get_prefix()}{key}")) def delete_many(self, *keys: str) -> _t.List[_t.Any]: if not keys: return [] if self.key_prefix: - prefixed_keys = [self.key_prefix + key for key in keys] + prefixed_keys = [f"{self._get_prefix()}{key}" for key in keys] else: prefixed_keys = [k for k in keys] self._write_client.delete(*prefixed_keys) return [k for k in prefixed_keys if not self.has(k)] def has(self, key: str) -> bool: - return bool(self._read_client.exists(self.key_prefix + key)) + return bool(self._read_client.exists(f"{self._get_prefix()}{key}")) def clear(self) -> bool: status = 0 if self.key_prefix: - keys = self._read_client.keys(self.key_prefix + "*") + keys = self._read_client.keys(self._get_prefix() + "*") if keys: status = self._write_client.delete(*keys) else: @@ -143,7 +153,7 @@ def clear(self) -> bool: return bool(status) def inc(self, key: str, delta: int = 1) -> _t.Any: - return self._write_client.incr(name=self.key_prefix + key, amount=delta) + return self._write_client.incr(name=f"{self._get_prefix()}{key}", amount=delta) def dec(self, key: str, delta: int = 1) -> _t.Any: - return self._write_client.incr(name=self.key_prefix + key, amount=-delta) + return self._write_client.incr(name=f"{self._get_prefix()}{key}", amount=-delta) diff --git a/src/cachelib/simple.py b/src/cachelib/simple.py index 14302cfc..b1b713b6 100644 --- a/src/cachelib/simple.py +++ b/src/cachelib/simple.py @@ -6,7 +6,6 @@ class SimpleCache(BaseCache): - """Simple memory cache for single process environments. This class exists mainly for the development server and is not 100% thread safe. It tries to use as many atomic operations as possible and no locks for simplicity @@ -40,10 +39,7 @@ def _remove_expired(self, now: float) -> None: def _remove_older(self) -> None: k_ordered = ( - k - for k, v in sorted( - self._cache.items(), key=lambda item: item[1][0] # type: ignore - ) + k for k, v in sorted(self._cache.items(), key=lambda item: item[1][0]) ) for k in k_ordered: self._cache.pop(k, None) diff --git a/tests/test_redis_cache.py b/tests/test_redis_cache.py index e6e10d67..26d178b6 100644 --- a/tests/test_redis_cache.py +++ b/tests/test_redis_cache.py @@ -38,6 +38,15 @@ def _factory(self, *args, **kwargs): request.cls.cache_factory = _factory +def my_callable_key() -> str: + return "bacon" + + @pytest.mark.usefixtures("redis_server") class TestRedisCache(CommonTests, ClearTests, HasTests): - pass + def test_callable_key(self): + cache = self.cache_factory() + assert cache.set(my_callable_key, "sausages") + assert cache.get(my_callable_key) == "sausages" + assert cache.set(lambda: "spam", "sausages") + assert cache.get(lambda: "spam") == "sausages" diff --git a/tox.ini b/tox.ini index 6fbb1b2e..89b54352 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{39,38,37,310} + py{38,39,310,311} style typing docs