diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea17bdb..508c221 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,18 +147,53 @@ jobs: - uses: actions/setup-python@v7 with: python-version: '3.14' - - name: Install diskcache + - name: Install diskcache-community run: pip install . - - name: Import diskcache without Django installed + - name: Import diskcache_community without Django installed working-directory: ${{ runner.temp }} run: | python -c "import importlib.util, sys; sys.exit(importlib.util.find_spec('django') is not None)" - python -W error -c "import diskcache; assert 'DjangoCache' not in diskcache.__all__, diskcache.__all__; print('diskcache', diskcache.__version__, 'imported without Django')" + python -W error -c "import diskcache_community as dc; assert 'DjangoCache' not in dc.__all__, dc.__all__; print('diskcache_community', dc.__version__, 'imported without Django')" + + coexist: + name: next to upstream diskcache 5.6.3 + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + DJANGO_SETTINGS_MODULE: tests.settings + PYTHONPATH: ${{ github.workspace }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + - name: Install diskcache-community and upstream diskcache 5.6.3 together + run: pip install . "diskcache==5.6.3" "django==5.2.*" pytest pytest-cov pytest-django pytest-xdist + - name: Create a separate environment with only diskcache 5.6.3 + run: | + python -m venv "$RUNNER_TEMP/venv-563" + "$RUNNER_TEMP/venv-563/bin/pip" install "diskcache==5.6.3" + - name: Run the coexistence and golden tests in both directions + env: + DISKCACHE_GOLDEN_563_PYTHON: ${{ runner.temp }}/venv-563/bin/python + run: pytest --no-cov -p no:cacheprovider tests/test_init.py tests/test_golden.py + - name: Uninstall either distribution and check the other still imports + working-directory: ${{ runner.temp }} + env: + PYTHONPATH: '' + run: | + pip check + pip uninstall -y diskcache + python -c "import diskcache_community as dc; print('diskcache_community', dc.__version__)" + pip install "diskcache==5.6.3" + pip uninstall -y diskcache-community + python -c "import diskcache; assert diskcache.__version__ == '5.6.3'; print('diskcache', diskcache.__version__)" + python -c "import importlib.util, sys; sys.exit(importlib.util.find_spec('diskcache_community') is not None)" ci-ok: name: ci-ok if: always() - needs: [lint, test, django, sqlite-dqs, no-django] + needs: [lint, test, django, sqlite-dqs, no-django, coexist] runs-on: ubuntu-latest steps: - name: Fail if any job did not succeed diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4402c8b..77165d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-python@v7 with: python-version: '3.12' - - name: Check that the tag matches diskcache.__version__ + - name: Check that the tag matches diskcache_community.__version__ if: github.ref_type == 'tag' run: | python - <<'PY' @@ -27,7 +27,7 @@ jobs: import re import sys - text = open('diskcache/__init__.py', encoding='utf-8').read() + text = open('diskcache_community/__init__.py', encoding='utf-8').read() version = re.search(r"^__version__ = '([^']+)'", text, re.M).group(1) tag = os.environ['GITHUB_REF_NAME'] if tag != 'v' + version: @@ -45,7 +45,7 @@ jobs: run: | python -m venv check check/bin/pip install -q "${{ github.workspace }}"/dist/*.whl - check/bin/python -W error -c "import diskcache; print(diskcache.__title__, diskcache.__version__)" + check/bin/python -W error -c "import diskcache_community as diskcache; print(diskcache.__title__, diskcache.__version__)" - uses: actions/upload-artifact@v7 with: name: dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a1ccb..ad7183e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ diskcache-community forked from DiskCache at upstream commit ebfa37c (3 March 20 ### Packaging +- **Breaking:** the package is imported as `diskcache_community` instead of `diskcache`, so it installs next to the upstream `diskcache` distribution without overwriting it, and a project can move to the fork while other dependencies still require `diskcache`. The public API, the `cache.db` schema, the file layout and the serialization are unchanged, so cache directories are shared with 5.6.3 in both directions. Django settings name `diskcache_community.DjangoCache` and `diskcache_community.SafeDisk`. The import-time warning about an upstream `diskcache` installed in the same environment is gone, and a CI job installs both distributions together, runs the golden tests in both directions and checks that uninstalling either leaves the other working (PR #22). - `LICENSE` now holds the full Apache License 2.0 text instead of the short boilerplate notice, so GitHub and package indexes detect the license; the copyright and fork notice stay in `NOTICE` (PR #21). ## 5.7.0 (2026-10-11) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 618dbaf..e6c2134 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,9 @@ Fixes imported from upstream pull requests keep their original commits and autho ## Compatibility contract -This fork publishes the same library under a new distribution name. The following stay as upstream left them, and a change to any of them is a compatibility break that needs a major version: +This fork publishes the same library under a new distribution name, `diskcache-community`, and a new import name, `diskcache_community`, so it installs next to the upstream `diskcache` package. The following stay as upstream left them, and a change to any of them is a compatibility break that needs a major version: -- the import name, `import diskcache`, and the public API exported by `diskcache/__init__.py`; +- the import name, `import diskcache_community` (it replaced `import diskcache` in 5.8.0, before any dependent used the fork), and the public API exported by `diskcache_community/__init__.py`; - the `cache.db` schema: the `Settings` and `Cache` tables, the value modes 1 to 4, and the index and trigger names; - the directory layout: `/ab/cd/<28 hex>.val` for values and `%03d` for `FanoutCache` shards; - the `RAW`, `BINARY`, `TEXT` and `PICKLE` serialization and the bytes `JSONDisk` writes; diff --git a/README.md b/README.md index 61542d6..6e2ecbd 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,10 @@ > [!IMPORTANT] > This is a community fork of [python-diskcache](https://github.com/grantjenks/python-diskcache), > whose last release was 5.6.3 in August 2023. It is published on PyPI as -> `diskcache-community` and still imported as `diskcache`. It carries the -> fix path for [CVE-2025-69872](https://github.com/advisories/GHSA-w8v5-vhqr-4h9v) -> and stays compatible with existing 5.6.3 cache directories. +> `diskcache-community` and imported as `diskcache_community`, so it installs +> next to the upstream `diskcache` package. It carries the fix path for +> [CVE-2025-69872](https://github.com/advisories/GHSA-w8v5-vhqr-4h9v) and +> reads and writes the same cache directories as 5.6.3. > See [Why this fork](#why-this-fork) and [Migrating from diskcache](#migrating-from-diskcache). ## Install @@ -40,14 +41,14 @@ pip install diskcache-community ``` -If the upstream `diskcache` package is already installed, remove it first. -Both distributions write to the same `diskcache/` package directory and -cannot coexist; see [Migrating from diskcache](#migrating-from-diskcache). +The package is imported as `diskcache_community` and never touches the +upstream `diskcache` package, so both can be installed in the same environment; +see [Migrating from diskcache](#migrating-from-diskcache). ## Usage ```python -from diskcache import Cache +from diskcache_community import Cache cache = Cache('cache') # the directory is created with mode 0700 cache.set('answer', 42, expire=3600, tag='math') @@ -69,7 +70,7 @@ Shard across several SQLite files for higher write throughput, and plug the same cache into Django: ```python -from diskcache import FanoutCache +from diskcache_community import FanoutCache cache = FanoutCache('cache', shards=8, timeout=1) ``` @@ -78,10 +79,10 @@ cache = FanoutCache('cache', shards=8, timeout=1) # settings.py CACHES = { 'default': { - 'BACKEND': 'diskcache.DjangoCache', + 'BACKEND': 'diskcache_community.DjangoCache', 'LOCATION': '/var/tmp/django_cache', 'SHARDS': 8, - 'DISK': 'diskcache.SafeDisk', # restricted unpickling, see below + 'DISK': 'diskcache_community.SafeDisk', # restricted unpickling, see below } } ``` @@ -91,7 +92,7 @@ only a fixed set of built-in types and keeps file reads inside the cache directory: ```python -from diskcache import Cache, SafeDisk +from diskcache_community import Cache, SafeDisk cache = Cache('cache', disk=SafeDisk) ``` @@ -120,13 +121,14 @@ anyone who can write into the cache directory can run code in the process that reads from it. The package still works, but scanners flag it and there is no fixed upstream version. -This fork publishes the same library as `diskcache-community` and fixes it in -two steps. Version 5.7.0 keeps the `cache.db` schema, the file layout, the -serialization modes and the memoize key format unchanged, so a 5.6.3 process -opens a directory written by this fork and the reverse. It adds the opt-in -`SafeDisk`, creates cache directories with mode `0700`, opens read-only -directories read-only, closes the per-thread SQLite connections it used to -leak, and survives `fork`. Version 6.0.0 will make safe reading the default. +This fork publishes the same library as `diskcache-community`. It keeps the +`cache.db` schema, the file layout, the serialization modes and the memoize +key format unchanged, so a 5.6.3 process opens a directory written by this +fork and the reverse. It adds the opt-in `SafeDisk`, creates cache directories +with mode `0700`, opens read-only directories read-only, closes the per-thread +SQLite connections it used to leak, and survives `fork`. The package is +imported as `diskcache_community`, so a project can move to it while other +dependencies in the same environment still use the upstream `diskcache`. The [changelog](CHANGELOG.md) lists every change and [SECURITY.md](SECURITY.md) describes what is and is not fixed. @@ -135,25 +137,32 @@ fork keeps his license, copyright and author credit; see [NOTICE](NOTICE). ## Migrating from diskcache -The import name and the public API are unchanged, so no code changes are -needed. Only the distribution changes: +The public API and the files on disk are unchanged; only the import name +differs. Install the fork and change the imports: ```sh -pip uninstall diskcache pip install diskcache-community ``` -Never keep both installed. Uninstalling either one deletes the shared -package directory, and the fork warns at import time when both are present. -If `diskcache` arrives through a dependency, exclude it. With uv: - -```toml -[tool.uv] -exclude-dependencies = ["diskcache"] +```python +import diskcache_community as diskcache # was: import diskcache +from diskcache_community import Cache # was: from diskcache import Cache ``` -With pip, install the dependent with `--no-deps` or exclude `diskcache` in a -constraints file. +In Django settings, use `'diskcache_community.DjangoCache'` and +`'diskcache_community.SafeDisk'`. Existing cache directories open as they +are, and a 5.6.3 process still opens directories written by the fork. + +The upstream `diskcache` package can stay installed, for example when another +dependency requires it. Each library uses the package it imports. A library +that wants to support both can import whichever is present: + +```python +try: + import diskcache_community as diskcache +except ImportError: + import diskcache +``` ## Documentation diff --git a/SECURITY.md b/SECURITY.md index d705d1c..8c7cd09 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,9 +17,9 @@ You will hear back within a week. A confirmed problem is fixed in the next relea Where this fork stands: - 5.7.0, the first release of this fork, reads pickles with the default `Disk` exactly as 5.6.3 does, so it opens every 5.6.3 cache. It adds an opt-in `SafeDisk` that unpickles only an allowlist of standard-library types, rejects anything outside it at write time and confines value paths to the cache directory; `Cache`, `FanoutCache`, `Deque` and `Index` take it through `disk=SafeDisk` and the Django backend through the `DISK` option. New cache directories are created with mode `0700`. -- 6.0.0 makes safe reading the default and turns the trusted-pickle behaviour into an explicit choice, with a migration guide. +- The default stays as it is. Existing caches hold objects outside any allowlist, such as instances of your own classes, Django responses and the keys of `memoize(typed=True)`, so `SafeDisk` remains an explicit choice. -Until you run a version where safe reading is the default or you have opted into it, treat the cache directory as trusted input: keep it on local storage that only the user running the process can write to, and do not share it with other accounts. +Unless you have opted into `SafeDisk`, treat the cache directory as trusted input: keep it on local storage that only the user running the process can write to, and do not share it with other accounts. A note on scanners: vulnerability scanners match on the distribution name, so `pip-audit`, Dependabot and similar tools report nothing for `diskcache-community` even though the default `Disk` in 5.7.0 behaves as it did in 5.6.3. A clean scan is not evidence that this issue is fixed. Use the list above instead, and the release notes of the version you run. diff --git a/diskcache/__init__.py b/diskcache_community/__init__.py similarity index 53% rename from diskcache/__init__.py rename to diskcache_community/__init__.py index 8b6d11c..a9d7c7d 100644 --- a/diskcache/__init__.py +++ b/diskcache_community/__init__.py @@ -5,9 +5,6 @@ The :doc:`tutorial` provides a helpful walkthrough of most methods. """ -import os -import warnings - from .core import ( DEFAULT_SETTINGS, ENOVAL, @@ -68,43 +65,6 @@ pass -def _warn_if_upstream_installed(site_dir=None): - """Warn when the upstream ``diskcache`` distribution is installed too. - - Both distributions install into the same ``diskcache`` package directory, - so the files on disk belong to whichever was installed last and - uninstalling either one removes them for both. Return True when the - warning was issued. - - """ - if site_dir is None: - site_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - try: - names = os.listdir(site_dir) - except OSError: - return False - - for name in names: - if name.startswith('diskcache-') and name.endswith( - ('.dist-info', '.egg-info') - ): - warnings.warn( - 'the diskcache distribution is installed next to' - ' diskcache-community; both provide the diskcache package' - ' directory, so uninstall diskcache (pip uninstall diskcache)' - ' and then reinstall diskcache-community', - RuntimeWarning, - stacklevel=2, - ) - return True - - return False - - -_warn_if_upstream_installed() - - __title__ = 'diskcache-community' __version__ = '5.7.0' __build__ = 0x050700 diff --git a/diskcache/cli.py b/diskcache_community/cli.py similarity index 100% rename from diskcache/cli.py rename to diskcache_community/cli.py diff --git a/diskcache/core.py b/diskcache_community/core.py similarity index 99% rename from diskcache/core.py rename to diskcache_community/core.py index df90353..47d54fa 100644 --- a/diskcache/core.py +++ b/diskcache_community/core.py @@ -390,7 +390,7 @@ def filename(self, key=UNKNOWN, value=UNKNOWN): The default implementation ignores the `key` and `value` parameters. In some scenarios, for example :meth:`Cache.push - `, the `key` or `value` may not be known when the + `, the `key` or `value` may not be known when the item is stored in the cache. :param key: key for item (default UNKNOWN) @@ -1081,7 +1081,9 @@ def __init__( try: assert issubclass(disk, Disk) except (TypeError, AssertionError): - raise ValueError('disk must subclass diskcache.Disk') from None + raise ValueError( + 'disk must subclass diskcache_community.Disk' + ) from None if directory is None: directory = tempfile.mkdtemp(prefix='diskcache-') @@ -2645,7 +2647,7 @@ def memoize( attribute. This is useful for introspection, for bypassing the cache, or for rewrapping the function with a different cache. - >>> from diskcache import Cache + >>> from diskcache_community import Cache >>> cache = Cache() >>> @cache.memoize(expire=1, tag='fib') ... def fibonacci(number): diff --git a/diskcache/djangocache.py b/diskcache_community/djangocache.py similarity index 98% rename from diskcache/djangocache.py rename to diskcache_community/djangocache.py index 857bfbe..6364476 100644 --- a/diskcache/djangocache.py +++ b/diskcache_community/djangocache.py @@ -23,9 +23,9 @@ def __init__(self, directory, params): The `params` mapping holds the keys of the Django ``CACHES`` setting. ``SHARDS`` and ``DATABASE_TIMEOUT`` configure the underlying - :class:`FanoutCache `, ``DISK`` selects the - :class:`Disk ` type or subclass used for serialization - as a class or a dotted import path such as ``'diskcache.SafeDisk'``, + :class:`FanoutCache `, ``DISK`` selects the + :class:`Disk ` type or subclass used for serialization + as a class or a dotted import path such as ``'diskcache_community.SafeDisk'``, and ``OPTIONS`` passes further settings through. :param str directory: cache directory diff --git a/diskcache/fanout.py b/diskcache_community/fanout.py similarity index 100% rename from diskcache/fanout.py rename to diskcache_community/fanout.py diff --git a/diskcache/persistent.py b/diskcache_community/persistent.py similarity index 99% rename from diskcache/persistent.py rename to diskcache_community/persistent.py index 9591be9..eda2740 100644 --- a/diskcache/persistent.py +++ b/diskcache_community/persistent.py @@ -85,7 +85,7 @@ def __init__(self, iterable=(), directory=None, maxlen=None, disk=Disk): :param directory: deque directory (default None) :param maxlen: max length (default None, no max) :param disk: `Disk` type or subclass for serialization (default - :class:`Disk `) + :class:`Disk `) """ self._cache = Cache(directory, disk=disk, eviction_policy='none') @@ -659,7 +659,7 @@ def transact(self): Transactions may be nested and may not be shared between threads. - >>> from diskcache import Deque + >>> from diskcache_community import Deque >>> deque = Deque() >>> deque += range(5) >>> with deque.transact(): # Atomically rotate elements. @@ -704,7 +704,7 @@ def __init__(self, *args, disk=Disk, **kwargs): Optional first argument may be string specifying directory where items are stored. When None or not given, temporary directory is created. - The keyword argument `disk` selects the :class:`Disk ` + The keyword argument `disk` selects the :class:`Disk ` type or subclass used for serialization. Every other keyword argument is stored as an item, so an item named ``disk`` has to be set after initialization. @@ -1177,7 +1177,7 @@ def memoize(self, name=None, typed=False, ignore=()): attribute. This is useful for introspection, for bypassing the cache, or for rewrapping the function with a different cache. - >>> from diskcache import Index + >>> from diskcache_community import Index >>> mapping = Index() >>> @mapping.memoize() ... def fibonacci(number): @@ -1227,7 +1227,7 @@ def transact(self): Transactions may be nested and may not be shared between threads. - >>> from diskcache import Index + >>> from diskcache_community import Index >>> mapping = Index() >>> with mapping.transact(): # Atomically increment two keys. ... mapping['total'] = mapping.get('total', 0) + 123.4 diff --git a/diskcache/recipes.py b/diskcache_community/recipes.py similarity index 97% rename from diskcache/recipes.py rename to diskcache_community/recipes.py index 48bae68..d1ce965 100644 --- a/diskcache/recipes.py +++ b/diskcache_community/recipes.py @@ -20,7 +20,7 @@ class Averager: Assumes the key will not be evicted. Set the eviction policy to 'none' on the cache to guarantee the key is not evicted. - >>> import diskcache + >>> import diskcache_community as diskcache >>> cache = diskcache.FanoutCache() >>> ave = Averager(cache, 'latency') >>> ave.add(0.080) @@ -71,7 +71,7 @@ class Lock: Assumes the key will not be evicted. Set the eviction policy to 'none' on the cache to guarantee the key is not evicted. - >>> import diskcache + >>> import diskcache_community as diskcache >>> cache = diskcache.Cache() >>> lock = Lock(cache, 'report-123') >>> lock.acquire() @@ -122,7 +122,7 @@ class RLock: Assumes the key will not be evicted. Set the eviction policy to 'none' on the cache to guarantee the key is not evicted. - >>> import diskcache + >>> import diskcache_community as diskcache >>> cache = diskcache.Cache() >>> rlock = RLock(cache, 'user-123') >>> rlock.acquire() @@ -193,7 +193,7 @@ class BoundedSemaphore: Assumes the key will not be evicted. Set the eviction policy to 'none' on the cache to guarantee the key is not evicted. - >>> import diskcache + >>> import diskcache_community as diskcache >>> cache = diskcache.Cache() >>> semaphore = BoundedSemaphore(cache, 'max-cons', value=2) >>> semaphore.acquire() @@ -266,7 +266,9 @@ def throttle( Assumes keys will not be evicted. Set the eviction policy to 'none' on the cache to guarantee the keys are not evicted. - >>> import diskcache, time + >>> import time + + >>> import diskcache_community as diskcache >>> cache = diskcache.Cache() >>> count = 0 >>> @throttle(cache, 2, 1) # 2 calls per 1 second @@ -323,7 +325,9 @@ def barrier(cache, lock_factory, name=None, expire=None, tag=None): Assumes keys will not be evicted. Set the eviction policy to 'none' on the cache to guarantee the keys are not evicted. - >>> import diskcache, time + >>> import time + + >>> import diskcache_community as diskcache >>> cache = diskcache.Cache() >>> @barrier(cache, Lock) ... def work(num): @@ -383,7 +387,7 @@ def memoize_stampede( attribute. This is useful for introspection, for bypassing the cache, or for rewrapping the function with a different cache. - >>> from diskcache import Cache + >>> from diskcache_community import Cache >>> cache = Cache() >>> @memoize_stampede(cache, expire=1) ... def fib(number): diff --git a/docs/api.rst b/docs/api.rst index a1fb6ed..eaf70c7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -13,7 +13,7 @@ Cache Read the :ref:`Cache tutorial ` for example usage. -.. autoclass:: diskcache.Cache +.. autoclass:: diskcache_community.Cache :members: :special-members: :exclude-members: __weakref__ @@ -23,7 +23,7 @@ FanoutCache Read the :ref:`FanoutCache tutorial ` for example usage. -.. autoclass:: diskcache.FanoutCache +.. autoclass:: diskcache_community.FanoutCache :members: :special-members: :exclude-members: __weakref__ @@ -33,14 +33,14 @@ DjangoCache Read the :ref:`DjangoCache tutorial ` for example usage. -.. autoclass:: diskcache.DjangoCache +.. autoclass:: diskcache_community.DjangoCache :members: :special-members: Deque ----- -.. autoclass:: diskcache.Deque +.. autoclass:: diskcache_community.Deque :members: :special-members: :exclude-members: __weakref__ @@ -48,7 +48,7 @@ Deque Index ----- -.. autoclass:: diskcache.Index +.. autoclass:: diskcache_community.Index :members: :special-members: :exclude-members: __weakref__ @@ -56,23 +56,23 @@ Index Recipes ------- -.. autoclass:: diskcache.Averager +.. autoclass:: diskcache_community.Averager :members: -.. autoclass:: diskcache.Lock +.. autoclass:: diskcache_community.Lock :members: -.. autoclass:: diskcache.RLock +.. autoclass:: diskcache_community.RLock :members: -.. autoclass:: diskcache.BoundedSemaphore +.. autoclass:: diskcache_community.BoundedSemaphore :members: -.. autodecorator:: diskcache.throttle +.. autodecorator:: diskcache_community.throttle -.. autodecorator:: diskcache.barrier +.. autodecorator:: diskcache_community.barrier -.. autodecorator:: diskcache.memoize_stampede +.. autodecorator:: diskcache_community.memoize_stampede .. _constants: @@ -81,7 +81,7 @@ Constants Read the :ref:`Settings tutorial ` for details. -.. data:: diskcache.DEFAULT_SETTINGS +.. data:: diskcache_community.DEFAULT_SETTINGS * `statistics` (int) default 0 - disabled when 0, enabled when 1. * `tag_index` (int) default 0 - disabled when 0, enabled when 1. @@ -104,7 +104,7 @@ Read the :ref:`Settings tutorial ` for details. * `disk_pickle_protocol` (int) default highest Pickle protocol - the Pickle protocol to use for data types that are not natively supported. -.. data:: diskcache.EVICTION_POLICY +.. data:: diskcache_community.EVICTION_POLICY * `least-recently-stored` (default) - evict least recently stored keys first. * `least-recently-used` - evict least recently used keys first. @@ -116,7 +116,7 @@ Disk Read the :ref:`Disk tutorial ` for details. -.. autoclass:: diskcache.Disk +.. autoclass:: diskcache_community.Disk :members: :special-members: :exclude-members: __weakref__ @@ -126,7 +126,7 @@ JSONDisk Read the :ref:`Disk tutorial ` for details. -.. autoclass:: diskcache.JSONDisk +.. autoclass:: diskcache_community.JSONDisk :members: :special-members: :exclude-members: __weakref__ @@ -136,29 +136,29 @@ SafeDisk Read the :ref:`SafeDisk tutorial ` for details. -.. autoclass:: diskcache.SafeDisk +.. autoclass:: diskcache_community.SafeDisk :members: :special-members: :exclude-members: __weakref__ -.. data:: diskcache.core.SAFE_PICKLE_NAMES +.. data:: diskcache_community.core.SAFE_PICKLE_NAMES Mapping of module name to the names that :class:`SafeDisk - ` lets pickle resolve while reading. + ` lets pickle resolve while reading. -.. data:: diskcache.core.SAFE_TYPES +.. data:: diskcache_community.core.SAFE_TYPES - The Python types that :class:`SafeDisk ` accepts + The Python types that :class:`SafeDisk ` accepts for writing; exactly the types the names above read back. Timeout ------- -.. autoexception:: diskcache.Timeout +.. autoexception:: diskcache_community.Timeout ReadOnlyError ------------- Read the :ref:`read-only tutorial ` for details. -.. autoexception:: diskcache.ReadOnlyError +.. autoexception:: diskcache_community.ReadOnlyError diff --git a/docs/cache-benchmarks.rst b/docs/cache-benchmarks.rst index 5b125e8..f55f5b9 100644 --- a/docs/cache-benchmarks.rst +++ b/docs/cache-benchmarks.rst @@ -102,8 +102,8 @@ Timings for diskcache.FanoutCache(shards=8, timeout=0.010) ========= ========= ========= ========= ========= ========= ========= ========= Notice the low overhead of the :class:`FanoutCache -`. Increasing the number of shards from four to eight -has a negligible impact on performance. +`. Increasing the number of shards from four +to eight has a negligible impact on performance. ========= ========= ========= ========= ========= ========= ========= ========= Timings for pylibmc.Client @@ -163,7 +163,8 @@ Delete .. image:: _static/core-p8-delete.png Again deletes require writes to disk. Only the :class:`FanoutCache -` performs well with one shard allocated per worker. +` performs well with one shard allocated per +worker. Timing Data ........... @@ -184,7 +185,7 @@ Timings for diskcache.Cache Notice the unacceptably high maximum store and delete latency. Without sharding, cache writers block each other. By default :class:`Cache -` objects raise a timeout error after sixty seconds. +` objects raise a timeout error after sixty seconds. ========= ========= ========= ========= ========= ========= ========= ========= Timings for diskcache.FanoutCache(shards=4, timeout=1.0) @@ -197,7 +198,7 @@ Timings for diskcache.FanoutCache(shards=4, timeout=1.0) Total 791992 72.634s ========= ========= ========= ========= ========= ========= ========= ========= -Here :class:`FanoutCache ` uses four shards to +Here :class:`FanoutCache ` uses four shards to distribute writes. That reduces the maximum latency by a factor of ten. Note the miss rate is variable due to the interleaved operations of concurrent workers. @@ -232,9 +233,9 @@ Timings for pylibmc.Client Memcached performance is low latency and stable even under heavy load. Notice that cache gets are three times slower in total as compared with -:class:`FanoutCache `. The superior performance of get -operations put the overall performance of :doc:`DiskCache ` ahead of -Memcached. +:class:`FanoutCache `. The superior +performance of get operations put the overall performance of :doc:`DiskCache +` ahead of Memcached. ========= ========= ========= ========= ========= ========= ========= ========= Timings for redis.StrictRedis diff --git a/docs/case-study-landing-page-caching.rst b/docs/case-study-landing-page-caching.rst index 677186e..27216fb 100644 --- a/docs/case-study-landing-page-caching.rst +++ b/docs/case-study-landing-page-caching.rst @@ -30,7 +30,7 @@ regenerates the page with a consistently slow latency. .. code-block:: python :emphasize-lines: 5 - import diskcache as dc + import diskcache_community as dc cache = dc.Cache() diff --git a/docs/case-study-web-crawler.rst b/docs/case-study-web-crawler.rst index c37e2a5..e5c5a01 100644 --- a/docs/case-study-web-crawler.rst +++ b/docs/case-study-web-crawler.rst @@ -73,12 +73,12 @@ dictionary. The results of our current code are ephemeral. All results are lost once the program terminates. To make the results persistent, we can use :doc:`DiskCache -` data structures and store the results in the local file -system. :doc:`DiskCache ` provides both :class:`Deque ` -and :class:`Index ` data structures which can replace our urls -and results variables. +` data structures and store the results in the local file system. +:doc:`DiskCache ` provides both :class:`Deque +` and :class:`Index ` +data structures which can replace our urls and results variables. - >>> from diskcache import Deque, Index + >>> from diskcache_community import Deque, Index >>> def crawl(): ... urls = Deque([0], 'data/urls') ... results = Index('data/results') diff --git a/docs/conf.py b/docs/conf.py index a789a68..b0901be 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ import sys sys.path.insert(0, os.path.abspath('..')) -import diskcache +import diskcache_community as diskcache # -- Project information ----------------------------------------------------- diff --git a/docs/djangocache-benchmarks.rst b/docs/djangocache-benchmarks.rst index 88d1fec..6319e31 100644 --- a/docs/djangocache-benchmarks.rst +++ b/docs/djangocache-benchmarks.rst @@ -2,9 +2,9 @@ DiskCache DjangoCache Benchmarks ================================ :doc:`DiskCache ` provides a Django-compatible cache API in -:class:`diskcache.DjangoCache`. A discussion of its options and abilities are -described in the :doc:`tutorial `. Here we try to assess its -performance compared to other Django cache backends. +:class:`diskcache_community.DjangoCache`. A discussion of its options and +abilities are described in the :doc:`tutorial `. Here we try to +assess its performance compared to other Django cache backends. Keys and Values --------------- @@ -102,9 +102,9 @@ Get .. image:: _static/djangocache-get.png -Under heavy load, :class:`DjangoCache ` gets are low -latency. At the 99th percentile they are on par with the Memcached cache -backend. +Under heavy load, :class:`DjangoCache ` gets +are low latency. At the 99th percentile they are on par with the Memcached +cache backend. Set ... @@ -121,9 +121,9 @@ Delete .. image:: _static/djangocache-delete.png Like sets, deletes require writes to disk. Though :class:`DjangoCache -` is the slowest, it remains competitive with latency -less than five milliseconds. Remember that unlike Local-memory, Memached, and -Redis, it persists all cached data. +` is the slowest, it remains competitive with +latency less than five milliseconds. Remember that unlike Local-memory, +Memached, and Redis, it persists all cached data. Timing Data ........... @@ -185,12 +185,12 @@ Timings for diskcache Total 791992 67.842s ========= ========= ========= ========= ========= ========= ========= ========= -:class:`DjangoCache ` defaults to using eight shards -with a 10 millisecond timeout. Notice that cache get operations are in +:class:`DjangoCache ` defaults to using eight +shards with a 10 millisecond timeout. Notice that cache get operations are in aggregate more than twice as fast as Memcached. And total cache time for all operations is comparable. The higher set and delete latencies are due to the -retry behavior of :class:`DjangoCache ` objects. If -lower latency is required then the retry behavior can be disabled. +retry behavior of :class:`DjangoCache ` +objects. If lower latency is required then the retry behavior can be disabled. ========= ========= ========= ========= ========= ========= ========= ========= Timings for filebased @@ -205,4 +205,5 @@ Timings for filebased Notice the higher cache miss rate. That's a result of the cache's random culling strategy. Get and set operations also take three to twenty times longer -in aggregate as compared with :class:`DjangoCache `. +in aggregate as compared with :class:`DjangoCache +`. diff --git a/docs/index.rst b/docs/index.rst index 763a474..c2fc223 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,16 +9,16 @@ processes, evicts by LRU, LFU and other policies, and ships ``Deque`` and This documentation describes `diskcache-community`_, a community fork of `python-diskcache`_. The fork is published on PyPI as ``diskcache-community`` -and imported as ``diskcache``. It keeps the public API, the ``cache.db`` -schema and the file layout of the last upstream release, 5.6.3, so existing -cache directories keep working, and it carries the fix path for -CVE-2025-69872 through the opt-in :class:`SafeDisk `. -The README in the repository explains why the fork exists and how to migrate. +and imported as ``diskcache_community``, so it installs next to the upstream +``diskcache`` package. It keeps the public API, the ``cache.db`` schema and +the file layout of the last upstream release, 5.6.3, so existing cache +directories keep working, and it carries the fix path for CVE-2025-69872 +through the opt-in :class:`SafeDisk `. The README +in the repository explains why the fork exists and how to migrate. -Install it with pip, after removing the upstream ``diskcache`` package if it -is present:: +Install it with pip and import ``diskcache_community`` where you imported +``diskcache``:: - $ pip uninstall diskcache $ pip install diskcache-community Start with the :doc:`tutorial` and keep the :doc:`api` at hand. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1f7cedf..fbf3262 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -63,11 +63,12 @@ can help, please open an issue in the `DiskCache Issue Tracker`_. Cache ----- -The core of :doc:`DiskCache ` is :class:`diskcache.Cache` which -represents a disk and file backed cache. As a Cache, it supports a familiar -Python mapping interface with additional cache and performance parameters. +The core of :doc:`DiskCache ` is :class:`diskcache_community.Cache` +which represents a disk and file backed cache. As a Cache, it supports a +familiar Python mapping interface with additional cache and performance +parameters. - >>> from diskcache import Cache + >>> from diskcache_community import Cache >>> cache = Cache() Initialization expects a directory path reference. If the directory path does @@ -88,7 +89,7 @@ Calling :meth:`close <.Cache.close>` closes the calling thread's connection and the connections of threads that have exited; connections that other running threads use stay open until those threads call ``close`` or the Cache object is garbage collected. Cache objects can be used in a `with` statement -to safeguard calling :meth:`close `. +to safeguard calling :meth:`close `. Cache objects also support process forking. Before a fork, the forking thread's connections are closed in the parent, where they reopen on the next @@ -122,8 +123,8 @@ Set an item, get a value, and delete a key using the usual operators: True >>> del cache['key'] -There's also a :meth:`set ` method with additional keyword -parameters: `expire`, `read`, and `tag`. +There's also a :meth:`set ` method with +additional keyword parameters: `expire`, `read`, and `tag`. >>> from io import BytesIO >>> cache.set('key', BytesIO(b'value'), expire=5, read=True, tag='data') @@ -131,8 +132,8 @@ parameters: `expire`, `read`, and `tag`. In the example above: the key expires in 5 seconds, the value is read as a file-like object, and tag metadata is stored with the key. Another method, -:meth:`get ` supports querying extra information with -`default`, `read`, `expire_time`, and `tag` keyword parameters. +:meth:`get ` supports querying extra information +with `default`, `read`, `expire_time`, and `tag` keyword parameters. >>> result = cache.get('key', read=True, expire_time=True, tag=True) >>> reader, timestamp, tag = result @@ -161,9 +162,9 @@ the cache. >>> cache.touch('does-not-exist', expire=1) False -Like :meth:`set `, the method :meth:`add -` can be used to insert an item in the cache. The item is -inserted only if the key is not already present. +Like :meth:`set `, the method :meth:`add +` can be used to insert an item in the cache. +The item is inserted only if the key is not already present. >>> cache.add(b'test', 123) True @@ -175,7 +176,8 @@ inserted only if the key is not already present. 123 Item values can also be incremented and decremented using :meth:`incr -` and :meth:`decr ` methods. +` and :meth:`decr +` methods. >>> cache.incr(b'test') 124 @@ -198,9 +200,10 @@ decrementing a missing key will raise a :exc:`KeyError`. Increment and decrement operations are atomic and assume the value may be stored in a SQLite integer column. SQLite supports 64-bit signed integers. -Like :meth:`delete ` and :meth:`get -`, the method :meth:`pop ` can be -used to delete an item in the cache and return its value. +Like :meth:`delete ` and :meth:`get +`, the method :meth:`pop +` can be used to delete an item in the cache and +return its value. >>> cache.pop('alice') 1 @@ -217,10 +220,10 @@ used to delete an item in the cache and return its value. >>> print(tag) admin -The :meth:`pop ` operation is atomic and using :meth:`incr -` together is an accurate method for counting and dumping -statistics in long-running systems. Unlike :meth:`get ` -the `read` argument is not supported. +The :meth:`pop ` operation is atomic and using +:meth:`incr ` together is an accurate method +for counting and dumping statistics in long-running systems. Unlike :meth:`get +` the `read` argument is not supported. .. _tutorial-culling: @@ -241,14 +244,15 @@ Another four methods remove items from the cache:: >>> cache.expire() 10 -:meth:`Expire ` removes all expired keys from the -cache. Resetting the :ref:`cull_limit ` to zero will disable -culling during :meth:`set ` and :meth:`add -` operations. Because culling is performed lazily, the -reported length of the cache includes expired items. Iteration likewise -includes expired items because it is a read-only operation. To exclude expired -items you must explicitly call :meth:`expire ` which -works regardless of the :ref:`cull_limit `. +:meth:`Expire ` removes all expired keys from +the cache. Resetting the :ref:`cull_limit ` to zero will +disable culling during :meth:`set ` and +:meth:`add ` operations. Because culling is +performed lazily, the reported length of the cache includes expired items. +Iteration likewise includes expired items because it is a read-only operation. +To exclude expired items you must explicitly call :meth:`expire +` which works regardless of the +:ref:`cull_limit `. >>> for num in range(100): ... _ = cache.set(num, num, tag='odd' if num % 2 else 'even') @@ -257,10 +261,10 @@ works regardless of the :ref:`cull_limit `. .. _tutorial-tag-index: -:meth:`Evict ` removes all the keys with a matching -tag. The default tag is ``None``. Tag values may be any of integer, float, -string, bytes and None. To accelerate the eviction of items by tag, an index -can be created. To do so, initialize the cache with ``tag_index=True``. +:meth:`Evict ` removes all the keys with a +matching tag. The default tag is ``None``. Tag values may be any of integer, +float, string, bytes and None. To accelerate the eviction of items by tag, an +index can be created. To do so, initialize the cache with ``tag_index=True``. >>> cache.clear() 50 @@ -282,9 +286,10 @@ But prefer initializing the cache with a tag index rather than explicitly creating or dropping the tag index. To manually enforce the cache's size limit, use the :meth:`cull -` method. :meth:`Cull ` begins by -removing expired items from the cache and then uses the eviction policy to -remove items until the cache volume is less than the size limit. +` method. :meth:`Cull +` begins by removing expired items from the +cache and then uses the eviction policy to remove items until the cache volume +is less than the size limit. >>> cache.clear() 50 @@ -303,12 +308,14 @@ remove items until the cache volume is less than the size limit. Some users may defer all culling to a cron-like process by setting the :ref:`cull_limit ` to zero and manually calling :meth:`cull -` to remove items. Like :meth:`evict -` and :meth:`expire `, calls to -:meth:`cull ` will work regardless of the :ref:`cull_limit +` to remove items. Like :meth:`evict +` and :meth:`expire +`, calls to :meth:`cull +` will work regardless of the :ref:`cull_limit `. -:meth:`Clear ` simply removes all items from the cache. +:meth:`Clear ` simply removes all items from +the cache. >>> cache.clear() > 0 True @@ -381,16 +388,16 @@ and :meth:`get <.Cache.get>`, methods :meth:`push <.Cache.push>`, :meth:`pull expiration time and tag. Lastly, three methods support metadata about the cache. The first is -:meth:`volume ` which returns the estimated total size -in bytes of the cache directory on disk. +:meth:`volume ` which returns the estimated +total size in bytes of the cache directory on disk. >>> cache.volume() < int(1e5) True .. _tutorial-statistics: -The second is :meth:`stats ` which returns cache hits -and misses. Cache statistics must first be enabled. +The second is :meth:`stats ` which returns +cache hits and misses. Cache statistics must first be enabled. >>> cache.stats(enable=True) (0, 0) @@ -407,9 +414,9 @@ Cache statistics are useful when evaluating different :ref:`eviction policies incur an extra overhead on cache lookups. Increment and decrement operations are not counted in cache statistics. -The third is :meth:`check ` which verifies cache -consistency. It can also fix inconsistencies and reclaim unused space. The -return value is a list of warnings. +The third is :meth:`check ` which verifies +cache consistency. It can also fix inconsistencies and reclaim unused space. +The return value is a list of warnings. >>> warnings = cache.check() @@ -431,41 +438,46 @@ To permanently delete the cache, recursively remove the cache's directory. FanoutCache ----------- -Built atop :class:`Cache ` is :class:`diskcache.FanoutCache` -which automatically `shards` the underlying database. `Sharding`_ is the -practice of horizontally partitioning data. Here it is used to decrease -blocking writes. While readers and writers do not block each other, writers -block other writers. Therefore a shard for every concurrent writer is -suggested. This will depend on your scenario. The default value is 8. +Built atop :class:`Cache ` is +:class:`diskcache_community.FanoutCache` which automatically `shards` the +underlying database. `Sharding`_ is the practice of horizontally partitioning +data. Here it is used to decrease blocking writes. While readers and writers do +not block each other, writers block other writers. Therefore a shard for every +concurrent writer is suggested. This will depend on your scenario. The default +value is 8. Another parameter, `timeout`, sets a limit on how long to wait for database transactions. Transactions are used for every operation that writes to the -database. When the timeout expires, a :exc:`diskcache.Timeout` error is raised -internally. This `timeout` parameter is also present on -:class:`diskcache.Cache`. When a :exc:`Timeout ` error -occurs in :class:`Cache ` methods, the exception may be raised -to the caller. In contrast, :class:`FanoutCache ` +database. When the timeout expires, a :exc:`diskcache_community.Timeout` error +is raised internally. This `timeout` parameter is also present on +:class:`diskcache_community.Cache`. When a :exc:`Timeout +` error occurs in :class:`Cache +` methods, the exception may be raised to the +caller. In contrast, :class:`FanoutCache ` catches all timeout errors and aborts the operation. As a result, :meth:`set -` and :meth:`delete ` -methods may silently fail. - -Most methods that handle :exc:`Timeout ` exceptions also -include a `retry` keyword parameter (default ``False``) to automatically repeat -attempts that timeout. The mapping interface operators: :meth:`cache[key] -`, :meth:`cache[key] = value -`, and :meth:`del cache[key] -` automatically retry operations when -:exc:`Timeout ` errors occur. :class:`FanoutCache -` will never raise a :exc:`Timeout ` -exception. The default `timeout` is 0.010 (10 milliseconds). - - >>> from diskcache import FanoutCache +` and :meth:`delete +` methods may silently fail. + +Most methods that handle :exc:`Timeout ` +exceptions also include a `retry` keyword parameter (default ``False``) to +automatically repeat attempts that timeout. The mapping interface operators: +:meth:`cache[key] `, +:meth:`cache[key] = value `, and +:meth:`del cache[key] ` +automatically retry operations when :exc:`Timeout +` errors occur. :class:`FanoutCache +` will never raise a :exc:`Timeout +` exception. The default `timeout` is 0.010 (10 +milliseconds). + + >>> from diskcache_community import FanoutCache >>> cache = FanoutCache(shards=4, timeout=1) The example above creates a cache in a temporary directory with four shards and a one second timeout. Operations will attempt to abort if they take longer than -one second. The remaining API of :class:`FanoutCache ` -matches :class:`Cache ` as described above. +one second. The remaining API of :class:`FanoutCache +` matches :class:`Cache +` as described above. The :class:`.FanoutCache` :ref:`size_limit ` is used as the total size of the cache. The size limit of individual cache shards is the total size @@ -475,10 +487,10 @@ of 256 megabytes. Items that are larger than the size limit are immediately culled. Caches have an additional feature: :meth:`memoizing -` decorator. The decorator wraps a callable and -caches arguments and return values. +` decorator. The decorator wraps a +callable and caches arguments and return values. - >>> from diskcache import FanoutCache + >>> from diskcache_community import FanoutCache >>> cache = FanoutCache() >>> @cache.memoize(typed=True, expire=1, tag='fib') ... def fibonacci(number): @@ -505,7 +517,7 @@ TypeError will occur:: TypeError: name cannot be callable Observe the lack of parenthenses after :meth:`memoize -` above. +` above. The ``ignore`` argument leaves arguments out of the cache key. An integer ignores the argument at that position and a string ignores the parameter with @@ -525,23 +537,23 @@ arguments that do not affect the result, like a database session: DjangoCache ----------- -:class:`diskcache.DjangoCache` uses :class:`FanoutCache -` to provide a Django-compatible cache interface. With -:doc:`DiskCache ` installed, you can use :class:`DjangoCache -` in your settings file. +:class:`diskcache_community.DjangoCache` uses :class:`FanoutCache +` to provide a Django-compatible cache +interface. With :doc:`DiskCache ` installed, you can use +:class:`DjangoCache ` in your settings file. .. code-block:: python CACHES = { 'default': { - 'BACKEND': 'diskcache.DjangoCache', + 'BACKEND': 'diskcache_community.DjangoCache', 'LOCATION': '/path/to/cache/directory', 'TIMEOUT': 300, # ^-- Django setting for default timeout of each key. 'SHARDS': 8, 'DATABASE_TIMEOUT': 0.010, # 10 milliseconds # ^-- Timeout for each DjangoCache database transaction. - 'DISK': 'diskcache.Disk', + 'DISK': 'diskcache_community.Disk', # ^-- Disk type for serialization, see SafeDisk below. 'OPTIONS': { 'size_limit': 2 ** 30 # 1 gigabyte @@ -549,24 +561,26 @@ DjangoCache }, } -As with :class:`FanoutCache ` above, these settings -create a Django-compatible cache with eight shards and a 10ms timeout. You can -pass further settings via the ``OPTIONS`` mapping as shown in the Django -documentation. ``DISK`` names the :class:`Disk ` type or -subclass used for serialization, as a class or a dotted import path; see -:ref:`SafeDisk ` for the restricted alternative. Only the -``BACKEND`` and ``LOCATION`` keys are necessary in the above example. The other -keys simply display their default value. :class:`DjangoCache ` will never raise a -:exc:`Timeout ` exception. But unlike :class:`FanoutCache -`, the keyword parameter `retry` defaults to ``True`` -for :class:`DjangoCache ` methods. - -The API of :class:`DjangoCache ` is a superset of the -functionality described in the `Django documentation on caching`_ and includes -many :class:`FanoutCache ` features. - -:class:`DjangoCache ` also works well with `X-Sendfile` -and `X-Accel-Redirect` headers. +As with :class:`FanoutCache ` above, these +settings create a Django-compatible cache with eight shards and a 10ms timeout. +You can pass further settings via the ``OPTIONS`` mapping as shown in the +Django documentation. ``DISK`` names the :class:`Disk +` type or subclass used for serialization, as a class +or a dotted import path; see :ref:`SafeDisk ` for the +restricted alternative. Only the ``BACKEND`` and ``LOCATION`` keys are +necessary in the above example. The other keys simply display their default +value. :class:`DjangoCache ` will never raise +a :exc:`Timeout ` exception. But unlike +:class:`FanoutCache `, the keyword parameter +`retry` defaults to ``True`` for :class:`DjangoCache +` methods. + +The API of :class:`DjangoCache ` is a superset +of the functionality described in the `Django documentation on caching`_ and +includes many :class:`FanoutCache ` features. + +:class:`DjangoCache ` also works well with +`X-Sendfile` and `X-Accel-Redirect` headers. .. code-block:: python @@ -581,26 +595,26 @@ and `X-Accel-Redirect` headers. except KeyError: # Handle cache miss. -When values are :meth:`set ` using ``read=True`` -they are guaranteed to be stored in files. The full path is available on the -file handle in the `name` attribute. Remember to also include the -`Content-Type` header if known. +When values are :meth:`set ` using +``read=True`` they are guaranteed to be stored in files. The full path is +available on the file handle in the `name` attribute. Remember to also include +the `Content-Type` header if known. .. _`Django documentation on caching`: https://docs.djangoproject.com/en/3.2/topics/cache/#the-low-level-cache-api Deque ----- -:class:`diskcache.Deque` (pronounced "deck") uses a :class:`Cache -` to provide a `collections.deque +:class:`diskcache_community.Deque` (pronounced "deck") uses a :class:`Cache +` to provide a `collections.deque `_-compatible double-ended queue. Deques are a generalization of stacks and queues with fast access and editing at both front and back sides. :class:`Deque -` objects use the :meth:`push <.Cache.push>`, :meth:`pull -<.Cache.pull>`, and :meth:`peek <.Cache.peek>` methods of :class:`Cache -<.Cache>` objects but never evict or expire items. +` objects use the :meth:`push <.Cache.push>`, +:meth:`pull <.Cache.pull>`, and :meth:`peek <.Cache.peek>` methods of +:class:`Cache <.Cache>` objects but never evict or expire items. - >>> from diskcache import Deque + >>> from diskcache_community import Deque >>> deque = Deque(range(5, 10)) >>> deque.pop() 9 @@ -620,25 +634,26 @@ access and editing at both front and back sides. :class:`Deque >>> list(thing) ['c', 'd', 'e'] -:class:`Deque ` objects provide an efficient and safe means of -cross-thread and cross-process communication. :class:`Deque ` -objects are also useful in scenarios where contents should remain persistent or -limitations prohibit holding all items in memory at the same time. The deque -uses a fixed amount of memory regardless of the size or number of items stored -inside it. +:class:`Deque ` objects provide an efficient and +safe means of cross-thread and cross-process communication. :class:`Deque +` objects are also useful in scenarios where +contents should remain persistent or limitations prohibit holding all items in +memory at the same time. The deque uses a fixed amount of memory regardless of +the size or number of items stored inside it. Index ----- -:class:`diskcache.Index` uses a :class:`Cache ` to provide a -`mutable mapping +:class:`diskcache_community.Index` uses a :class:`Cache +` to provide a `mutable mapping `_ and `ordered dictionary `_ -interface. :class:`Index ` objects inherit all the benefits of -:class:`Cache ` objects but never evict or expire items. +interface. :class:`Index ` objects inherit all the +benefits of :class:`Cache ` objects but never evict +or expire items. - >>> from diskcache import Index + >>> from diskcache_community import Index >>> index = Index([('a', 1), ('b', 2), ('c', 3)]) >>> 'b' in index True @@ -653,12 +668,12 @@ interface. :class:`Index ` objects inherit all the benefits of >>> other.popitem(last=False) ('b', 2) -:class:`Index ` objects provide an efficient and safe means of -cross-thread and cross-process communication. :class:`Index ` -objects are also useful in scenarios where contents should remain persistent or -limitations prohibit holding all items in memory at the same time. The index -uses a fixed amount of memory regardless of the size or number of items stored -inside it. +:class:`Index ` objects provide an efficient and +safe means of cross-thread and cross-process communication. :class:`Index +` objects are also useful in scenarios where +contents should remain persistent or limitations prohibit holding all items in +memory at the same time. The index uses a fixed amount of memory regardless of +the size or number of items stored inside it. .. _tutorial-transactions: @@ -743,32 +758,32 @@ Settings -------- A variety of settings are available to improve performance. These values are -stored in the database for durability and to communicate between -processes. Each value is cached in an attribute with matching name. Attributes -are updated using :meth:`reset `. Attributes are set +stored in the database for durability and to communicate between processes. +Each value is cached in an attribute with matching name. Attributes are updated +using :meth:`reset `. Attributes are set during initialization when passed as keyword arguments. * `size_limit`, default one gigabyte. The maximum on-disk size of the cache. * `cull_limit`, default ten. The maximum number of keys to cull when adding a new item. Set to zero to disable automatic culling. Some systems may disable automatic culling in exchange for a cron-like job that regularly calls - :meth:`cull ` in a separate process. + :meth:`cull ` in a separate process. * `statistics`, default False, disabled. The setting to collect :ref:`cache statistics `. * `tag_index`, default False, disabled. The setting to create a database :ref:`tag index ` for :meth:`evict - `. + `. * `eviction_policy`, default "least-recently-stored". The setting to determine :ref:`eviction policy `. -The :meth:`reset ` method accepts an optional -second argument that updates the corresponding value in the database. The -return value is the latest retrieved from the database. Notice that attributes -are updated lazily. Prefer idioms like :meth:`len -`, :meth:`volume -`, and :meth:`keyword arguments -` rather than using :meth:`reset -` directly. +The :meth:`reset ` method accepts an +optional second argument that updates the corresponding value in the database. +The return value is the latest retrieved from the database. Notice that +attributes are updated lazily. Prefer idioms like :meth:`len +`, :meth:`volume +`, and :meth:`keyword arguments +` rather than using :meth:`reset +` directly. >>> cache = Cache(size_limit=int(4e9)) >>> print(cache.size_limit) @@ -787,7 +802,7 @@ are updated lazily. Prefer idioms like :meth:`len More settings correspond to :ref:`Disk ` attributes. Each of these may be specified when initializing the :ref:`Cache `. Changing these values will update the unprefixed attribute -on the :class:`Disk ` object. +on the :class:`Disk ` object. * `disk_min_file_size`, default 32 kilobytes. The minimum size to store a value in a file. @@ -805,9 +820,9 @@ pragma documentation`_ for more details. * `sqlite_synchronous`, default 1, "NORMAL". Each of these settings can passed to :class:`DjangoCache -` via the ``OPTIONS`` key mapping. Always measure before -and after changing the default values. Default settings are programmatically -accessible at :data:`diskcache.DEFAULT_SETTINGS`. +` via the ``OPTIONS`` key mapping. Always +measure before and after changing the default values. Default settings are +programmatically accessible at :data:`diskcache_community.DEFAULT_SETTINGS`. .. _`SQLite pragma documentation`: https://www.sqlite.org/pragma.html @@ -859,16 +874,16 @@ to initialize the cache. Disk ---- -:class:`diskcache.Disk` objects are responsible for serializing and +:class:`diskcache_community.Disk` objects are responsible for serializing and deserializing data stored in the cache. Serialization behavior differs between keys and values. In particular, keys are always stored in the cache metadata database while values are sometimes stored separately in files. -To customize serialization, you may pass in a :class:`Disk ` -subclass to initialize the cache. All clients accessing the cache are expected -to use the same serialization. The default implementation uses Pickle and the -example below uses compressed JSON, available for convenience as -:class:`JSONDisk `. +To customize serialization, you may pass in a :class:`Disk +` subclass to initialize the cache. All clients +accessing the cache are expected to use the same serialization. The default +implementation uses Pickle and the example below uses compressed JSON, +available for convenience as :class:`JSONDisk `. .. code-block:: python @@ -912,11 +927,12 @@ will require identical types. SafeDisk ........ -The default :class:`Disk ` reads pickled keys and values back -with :func:`pickle.load`, which runs whatever code a pickle asks for. Anyone -who can write into the cache directory can therefore run code in every -process that reads from it (CVE-2025-69872). :class:`SafeDisk -` keeps the same on-disk format but closes that path: +The default :class:`Disk ` reads pickled keys and +values back with :func:`pickle.load`, which runs whatever code a pickle asks +for. Anyone who can write into the cache directory can therefore run code in +every process that reads from it (CVE-2025-69872). :class:`SafeDisk +` keeps the same on-disk format but closes that +path: * Pickles are read with an unpickler that resolves only an allowlist of standard-library constructors: ``None``, ``bool``, ``int``, ``float``, @@ -935,30 +951,32 @@ that opens the directory chooses for itself: .. code-block:: python - from diskcache import Cache, Deque, FanoutCache, Index, SafeDisk + from diskcache_community import Cache, Deque, FanoutCache, Index, SafeDisk cache = Cache('/tmp/mycache', disk=SafeDisk) fanout = FanoutCache('/tmp/myfanout', disk=SafeDisk) deque = Deque(directory='/tmp/mydeque', disk=SafeDisk) index = Index('/tmp/myindex', disk=SafeDisk) -For :class:`DjangoCache `, set ``'DISK': -'diskcache.SafeDisk'`` next to ``SHARDS`` and ``DATABASE_TIMEOUT`` in the -``CACHES`` setting. Sub-containers created through :meth:`FanoutCache.cache -`, :meth:`deque ` -and :meth:`index ` use the fanout cache's disk -unless given their own. - -A directory written with :class:`SafeDisk ` opens with the -default :class:`Disk ` and the reverse, as long as the values -are within the allowlist. Some uses need the default :class:`Disk -` because their objects are outside it: instances of your own -classes, Django model instances and ``HttpResponse`` objects, datetimes whose -``tzinfo`` is a ``zoneinfo.ZoneInfo`` rather than a ``datetime.timezone``, and -:meth:`memoize ` with ``typed=True``, which puts -``type`` objects into the cache key. Every process that writes to a shared -directory with the default :class:`Disk ` must be trusted, and -a reader using :class:`SafeDisk ` raises +For :class:`DjangoCache `, set ``'DISK': +'diskcache_community.SafeDisk'`` next to ``SHARDS`` and ``DATABASE_TIMEOUT`` in +the ``CACHES`` setting. Sub-containers created through :meth:`FanoutCache.cache +`, :meth:`deque +` and :meth:`index +` use the fanout cache's disk unless +given their own. + +A directory written with :class:`SafeDisk ` opens +with the default :class:`Disk ` and the reverse, as +long as the values are within the allowlist. Some uses need the default +:class:`Disk ` because their objects are outside it: +instances of your own classes, Django model instances and ``HttpResponse`` +objects, datetimes whose ``tzinfo`` is a ``zoneinfo.ZoneInfo`` rather than a +``datetime.timezone``, and :meth:`memoize ` +with ``typed=True``, which puts ``type`` objects into the cache key. Every +process that writes to a shared directory with the default :class:`Disk +` must be trusted, and a reader using +:class:`SafeDisk ` raises :exc:`pickle.UnpicklingError` on those rows. .. _tutorial-read-only: @@ -986,27 +1004,27 @@ directory: >>> snapshot['key'] 'value' -Reads, iteration, ``len``, :meth:`volume `, -:meth:`check `, memoized calls that hit and the -read-only methods of :class:`FanoutCache `, -:class:`Deque ` and :class:`Index ` all -work. Every method that would write raises :exc:`ReadOnlyError -`, a subclass of :exc:`sqlite3.OperationalError`, -before anything is written: +Reads, iteration, ``len``, :meth:`volume `, +:meth:`check `, memoized calls that hit and +the read-only methods of :class:`FanoutCache +`, :class:`Deque ` +and :class:`Index ` all work. Every method that +would write raises :exc:`ReadOnlyError `, a +subclass of :exc:`sqlite3.OperationalError`, before anything is written: - >>> snapshot['other'] = 'value' + >>> snapshot['other'] = 'value' # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - diskcache.core.ReadOnlyError: cache in '/tmp/mycachedir' is read-only + ReadOnlyError: cache in '/tmp/mycachedir' is read-only >>> snapshot.close() -A read-only cache cannot record hits, misses or access times, so -:meth:`stats ` returns the stored counters without -enabling or resetting them, the ``least-recently-used`` and -``least-frequently-used`` policies read without updating, and expired items -are skipped instead of removed. The ``read_only`` argument is not a setting -and is never persisted; passing it through ``FanoutCache(directory, -read_only=True)`` or the Django ``OPTIONS`` works the same way. +A read-only cache cannot record hits, misses or access times, so :meth:`stats +` returns the stored counters without enabling +or resetting them, the ``least-recently-used`` and ``least-frequently-used`` +policies read without updating, and expired items are skipped instead of +removed. The ``read_only`` argument is not a setting and is never persisted; +passing it through ``FanoutCache(directory, read_only=True)`` or the Django +``OPTIONS`` works the same way. Two details of the file system. When the directory is writable, SQLite may create the ``cache.db-shm`` and ``cache.db-wal`` files next to a read-only @@ -1020,19 +1038,20 @@ Caveats Though :doc:`DiskCache ` has a dictionary-like interface, Python's `hash protocol`_ is not used. Neither the `__hash__` nor `__eq__` methods are used for lookups. Instead lookups depend on the serialization method defined by -:class:`Disk ` objects. For strings, bytes, integers, and -floats, equality matches Python's definition. But large integers and all other -types will be converted to bytes and the bytes representation will define +:class:`Disk ` objects. For strings, bytes, integers, +and floats, equality matches Python's definition. But large integers and all +other types will be converted to bytes and the bytes representation will define equality. -The default :class:`diskcache.Disk` serialization uses pickling for both keys -and values. Unfortunately, pickling produces inconsistencies sometimes when -applied to container data types like tuples. Two equal tuples may serialize to -different bytes objects using pickle. The likelihood of differences is reduced -by using `pickletools.optimize` but still inconsistencies occur (`#54`_). The -inconsistent serialized pickle values is particularly problematic when applied -to the key in the cache. Consider using an alternative Disk type, like -:class:`JSONDisk `, for consistent serialization of keys. +The default :class:`diskcache_community.Disk` serialization uses pickling for +both keys and values. Unfortunately, pickling produces inconsistencies +sometimes when applied to container data types like tuples. Two equal tuples +may serialize to different bytes objects using pickle. The likelihood of +differences is reduced by using `pickletools.optimize` but still +inconsistencies occur (`#54`_). The inconsistent serialized pickle values is +particularly problematic when applied to the key in the cache. Consider using +an alternative Disk type, like :class:`JSONDisk +`, for consistent serialization of keys. SQLite is used to synchronize database access between threads and processes and as such inherits all SQLite caveats. Most notably SQLite is `not recommended`_ @@ -1060,11 +1079,11 @@ thread-pool executor asynchronously. For example:: asyncio.run(set_async('test-key', 'test-value')) -The cache :meth:`volume ` is based on the size of the -database that stores metadata and the size of the values stored in files. It -does not account the size of directories themselves or other filesystem -metadata. If directory count or size is a concern then consider implementing an -alternative :class:`Disk `. +The cache :meth:`volume ` is based on the +size of the database that stores metadata and the size of the values stored in +files. It does not account the size of directories themselves or other +filesystem metadata. If directory count or size is a concern then consider +implementing an alternative :class:`Disk `. .. _`#54`: https://github.com/grantjenks/python-diskcache/issues/54 .. _`hash protocol`: https://docs.python.org/library/functions.html#hash diff --git a/pyproject.toml b/pyproject.toml index ddf05c0..4d1e1d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ Tracker = "https://github.com/secondwindoss/diskcache-community/issues" Upstream = "https://github.com/grantjenks/python-diskcache" [tool.setuptools] -packages = ["diskcache"] +packages = ["diskcache_community"] [tool.setuptools.dynamic] -version = { attr = "diskcache.__version__" } +version = { attr = "diskcache_community.__version__" } diff --git a/tests/benchmark_core.py b/tests/benchmark_core.py index 7d64595..2e9f1cc 100644 --- a/tests/benchmark_core.py +++ b/tests/benchmark_core.py @@ -1,4 +1,4 @@ -"""Benchmark diskcache.Cache +"""Benchmark diskcache_community.Cache $ export PYTHONPATH=/Users/grantj/repos/python-diskcache $ python tests/benchmark_core.py -p 1 > tests/timings_core_p1.txt @@ -28,11 +28,11 @@ # Disk Cache Benchmarks ############################################################################### -import diskcache # noqa +import diskcache_community as diskcache # noqa caches.append( ( - 'diskcache.Cache', + 'diskcache_community.Cache', diskcache.Cache, ('tmp',), {}, @@ -40,7 +40,7 @@ ) caches.append( ( - 'diskcache.FanoutCache(shards=4, timeout=1.0)', + 'diskcache_community.FanoutCache(shards=4, timeout=1.0)', diskcache.FanoutCache, ('tmp',), {'shards': 4, 'timeout': 1.0}, @@ -48,7 +48,7 @@ ) caches.append( ( - 'diskcache.FanoutCache(shards=8, timeout=0.010)', + 'diskcache_community.FanoutCache(shards=8, timeout=0.010)', diskcache.FanoutCache, ('tmp',), {'shards': 8, 'timeout': 0.010}, diff --git a/tests/benchmark_djangocache.py b/tests/benchmark_djangocache.py index 61a80bf..f48cbfd 100644 --- a/tests/benchmark_djangocache.py +++ b/tests/benchmark_djangocache.py @@ -1,4 +1,4 @@ -"""Benchmark diskcache.DjangoCache +"""Benchmark diskcache_community.DjangoCache $ export PYTHONPATH=/Users/grantj/repos/python-diskcache $ python tests/benchmark_djangocache.py > tests/timings_djangocache.txt diff --git a/tests/benchmark_incr.py b/tests/benchmark_incr.py index 4f758aa..5abdf71 100644 --- a/tests/benchmark_incr.py +++ b/tests/benchmark_incr.py @@ -6,7 +6,7 @@ import shutil import time -import diskcache as dc +import diskcache_community as dc from .utils import secs diff --git a/tests/benchmark_kv_store.py b/tests/benchmark_kv_store.py index 7015470..4b8feea 100644 --- a/tests/benchmark_kv_store.py +++ b/tests/benchmark_kv_store.py @@ -5,7 +5,7 @@ from IPython import get_ipython -import diskcache +import diskcache_community as diskcache ipython = get_ipython() assert ipython is not None, 'No IPython! Run with $ ipython ...' diff --git a/tests/golden/make_fixtures.py b/tests/golden/make_fixtures.py index aa51f9d..7848f84 100644 --- a/tests/golden/make_fixtures.py +++ b/tests/golden/make_fixtures.py @@ -1,8 +1,8 @@ """Write the golden fixture caches and pack them into the archive. Run this with an interpreter whose ``diskcache`` is the upstream 5.6.3 wheel, -from a working directory outside the repository, so that the repository's -``diskcache/`` source tree does not shadow the wheel:: +from a working directory outside the repository, so that the repository +cannot shadow the wheel:: $ cd /tmp $ /path/to/venv-5.6.3/bin/python -I \\ @@ -28,7 +28,10 @@ import time from pathlib import Path -import diskcache +if __name__ == '__main__': + import diskcache +else: + import diskcache_community as diskcache REPO_ROOT = Path(__file__).resolve().parents[2] if str(REPO_ROOT) not in sys.path: diff --git a/tests/golden/verify.py b/tests/golden/verify.py index 48c893d..72a5dfc 100644 --- a/tests/golden/verify.py +++ b/tests/golden/verify.py @@ -3,10 +3,11 @@ Every function opens a fixture cache read-only (apart from the access-time bookkeeping of the eviction policies) and raises AssertionError on the first -mismatch. The module runs under the current code when imported by pytest and -under the upstream 5.6.3 wheel when executed as a script from outside the -repository, which is how the writer checks its own output and how the reverse -test reads caches written by the current code. +mismatch. The module runs under the current code, ``diskcache_community``, when +imported by pytest, and under the upstream 5.6.3 wheel, ``diskcache``, when +executed as a script from outside the repository or imported by the fixture +writer, which is how the writer checks its own output and how the reverse test +reads caches written by the current code. """ import argparse @@ -15,7 +16,10 @@ import sys from pathlib import Path -import diskcache +if __name__ == '__main__' or 'diskcache' in sys.modules: + import diskcache +else: + import diskcache_community as diskcache REPO_ROOT = Path(__file__).resolve().parents[2] if str(REPO_ROOT) not in sys.path: diff --git a/tests/issue_109.py b/tests/issue_109.py index a649c58..71d7b46 100644 --- a/tests/issue_109.py +++ b/tests/issue_109.py @@ -3,7 +3,7 @@ import time -import diskcache as dc +import diskcache_community as dc def main(): diff --git a/tests/plot_early_recompute.py b/tests/plot_early_recompute.py index 1508c45..558cc37 100644 --- a/tests/plot_early_recompute.py +++ b/tests/plot_early_recompute.py @@ -7,7 +7,7 @@ import threading import time -import diskcache as dc +import diskcache_community as dc def make_timer(times): diff --git a/tests/settings.py b/tests/settings.py index 7dcb598..98a2b62 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -132,7 +132,7 @@ CACHES = { 'default': { - 'BACKEND': 'diskcache.DjangoCache', + 'BACKEND': 'diskcache_community.DjangoCache', 'LOCATION': CACHE_DIR, }, } diff --git a/tests/settings_benchmark.py b/tests/settings_benchmark.py index c734e68..7663ced 100644 --- a/tests/settings_benchmark.py +++ b/tests/settings_benchmark.py @@ -2,7 +2,7 @@ CACHES = { 'default': { - 'BACKEND': 'diskcache.DjangoCache', + 'BACKEND': 'diskcache_community.DjangoCache', 'LOCATION': CACHE_DIR, # noqa }, 'memcached': { @@ -33,7 +33,7 @@ }, }, 'diskcache': { - 'BACKEND': 'diskcache.DjangoCache', + 'BACKEND': 'diskcache_community.DjangoCache', 'LOCATION': 'tmp', }, } diff --git a/tests/stress_test_core.py b/tests/stress_test_core.py index 2b2578b..78c231c 100644 --- a/tests/stress_test_core.py +++ b/tests/stress_test_core.py @@ -1,4 +1,4 @@ -"""Stress test diskcache.core.Cache.""" +"""Stress test diskcache_community.core.Cache.""" import collections as co import multiprocessing as mp @@ -11,7 +11,12 @@ import time import warnings -from diskcache import Cache, EmptyDirWarning, Timeout, UnknownFileWarning +from diskcache_community import ( + Cache, + EmptyDirWarning, + Timeout, + UnknownFileWarning, +) from .utils import display diff --git a/tests/stress_test_deque.py b/tests/stress_test_deque.py index 845b2c2..1f9584e 100644 --- a/tests/stress_test_deque.py +++ b/tests/stress_test_deque.py @@ -1,10 +1,10 @@ -"""Stress test diskcache.persistent.Deque.""" +"""Stress test diskcache_community.persistent.Deque.""" import collections as co import functools as ft import random -import diskcache as dc +import diskcache_community as dc OPERATIONS = 1000 SEED = 0 diff --git a/tests/stress_test_deque_mp.py b/tests/stress_test_deque_mp.py index f3b8a48..3d915c3 100644 --- a/tests/stress_test_deque_mp.py +++ b/tests/stress_test_deque_mp.py @@ -1,11 +1,11 @@ -"""Stress test diskcache.persistent.Deque.""" +"""Stress test diskcache_community.persistent.Deque.""" import itertools as it import multiprocessing as mp import random import time -import diskcache as dc +import diskcache_community as dc OPERATIONS = 1000 SEED = 0 diff --git a/tests/stress_test_fanout.py b/tests/stress_test_fanout.py index e78dda5..75f266a 100644 --- a/tests/stress_test_fanout.py +++ b/tests/stress_test_fanout.py @@ -1,4 +1,4 @@ -"""Stress test diskcache.core.Cache.""" +"""Stress test diskcache_community.core.Cache.""" import multiprocessing as mp import os @@ -10,7 +10,11 @@ import time import warnings -from diskcache import EmptyDirWarning, FanoutCache, UnknownFileWarning +from diskcache_community import ( + EmptyDirWarning, + FanoutCache, + UnknownFileWarning, +) from .utils import display diff --git a/tests/stress_test_index.py b/tests/stress_test_index.py index e7ba3f6..ea72880 100644 --- a/tests/stress_test_index.py +++ b/tests/stress_test_index.py @@ -1,10 +1,10 @@ -"""Stress test diskcache.persistent.Index.""" +"""Stress test diskcache_community.persistent.Index.""" import collections as co import itertools as it import random -import diskcache as dc +import diskcache_community as dc KEYS = 100 OPERATIONS = 25000 diff --git a/tests/stress_test_index_mp.py b/tests/stress_test_index_mp.py index 06ed102..950d1f8 100644 --- a/tests/stress_test_index_mp.py +++ b/tests/stress_test_index_mp.py @@ -1,11 +1,11 @@ -"""Stress test diskcache.persistent.Index.""" +"""Stress test diskcache_community.persistent.Index.""" import itertools as it import multiprocessing as mp import random import time -import diskcache as dc +import diskcache_community as dc KEYS = 100 OPERATIONS = 10000 diff --git a/tests/test_core.py b/tests/test_core.py index 2c8113d..5ccba7d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,4 @@ -"""Test diskcache.core.Cache.""" +"""Test diskcache_community.core.Cache.""" import _thread import errno @@ -22,7 +22,7 @@ import pytest -import diskcache as dc +import diskcache_community as dc pytestmark = pytest.mark.filterwarnings('ignore', category=dc.EmptyDirWarning) @@ -243,7 +243,7 @@ def test_get_keyerror4(cache): cache.reset('statistics', True) cache[0] = b'abcd' * 2**20 - with mock.patch('diskcache.core.open', func): + with mock.patch('diskcache_community.core.open', func): with pytest.raises((IOError, KeyError, OSError)): cache[0] @@ -1260,9 +1260,9 @@ def test_key_roundtrip(cache): def test_constant(): - import diskcache.core + import diskcache_community.core - assert repr(diskcache.core.ENOVAL) == 'ENOVAL' + assert repr(diskcache_community.core.ENOVAL) == 'ENOVAL' def test_copy(): @@ -1466,7 +1466,7 @@ def test_cleanup_dirs(cache): def test_disk_write_os_error(cache): func = mock.Mock(side_effect=[OSError] * 10) - with mock.patch('diskcache.core.open', func): + with mock.patch('diskcache_community.core.open', func): with pytest.raises(OSError): cache[0] = '\0' * 2**20 diff --git a/tests/test_deque.py b/tests/test_deque.py index f997a86..ef3e892 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -1,4 +1,4 @@ -"""Test diskcache.persistent.Deque.""" +"""Test diskcache_community.persistent.Deque.""" import pickle import shutil @@ -7,8 +7,8 @@ import pytest -import diskcache as dc -from diskcache.core import ENOVAL +import diskcache_community as dc +from diskcache_community.core import ENOVAL def rmdir(directory): diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 070c6d4..f743401 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -967,11 +967,11 @@ def __getstate__(self): @override_settings( CACHES=caches_setting_for_tests( - BACKEND='diskcache.DjangoCache', + BACKEND='diskcache_community.DjangoCache', ) ) class DiskCacheTests(BaseCacheTests, TestCase): - """Specific test cases for diskcache.DjangoCache.""" + """Specific test cases for diskcache_community.DjangoCache.""" def setUp(self): super().setUp() diff --git a/tests/test_doctest.py b/tests/test_doctest.py index 822d8db..b34524f 100644 --- a/tests/test_doctest.py +++ b/tests/test_doctest.py @@ -1,34 +1,34 @@ import doctest -import diskcache.core -import diskcache.djangocache -import diskcache.fanout -import diskcache.persistent -import diskcache.recipes +import diskcache_community.core +import diskcache_community.djangocache +import diskcache_community.fanout +import diskcache_community.persistent +import diskcache_community.recipes def test_core(): - failures, _ = doctest.testmod(diskcache.core) + failures, _ = doctest.testmod(diskcache_community.core) assert failures == 0 def test_djangocache(): - failures, _ = doctest.testmod(diskcache.djangocache) + failures, _ = doctest.testmod(diskcache_community.djangocache) assert failures == 0 def test_fanout(): - failures, _ = doctest.testmod(diskcache.fanout) + failures, _ = doctest.testmod(diskcache_community.fanout) assert failures == 0 def test_persistent(): - failures, _ = doctest.testmod(diskcache.persistent) + failures, _ = doctest.testmod(diskcache_community.persistent) assert failures == 0 def test_recipes(): - failures, _ = doctest.testmod(diskcache.recipes) + failures, _ = doctest.testmod(diskcache_community.recipes) assert failures == 0 diff --git a/tests/test_fanout.py b/tests/test_fanout.py index dbb78f1..fac3b26 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -1,4 +1,4 @@ -"""Test diskcache.fanout.FanoutCache.""" +"""Test diskcache_community.fanout.FanoutCache.""" import collections as co import hashlib @@ -18,7 +18,7 @@ import pytest -import diskcache as dc +import diskcache_community as dc warnings.simplefilter('error') warnings.simplefilter('ignore', category=dc.EmptyDirWarning) diff --git a/tests/test_index.py b/tests/test_index.py index 742daf3..f2a7014 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,4 +1,4 @@ -"""Test diskcache.persistent.Index.""" +"""Test diskcache_community.persistent.Index.""" import pickle import shutil @@ -6,7 +6,7 @@ import pytest -import diskcache as dc +import diskcache_community as dc def rmdir(directory): diff --git a/tests/test_init.py b/tests/test_init.py index acb0291..d4b711d 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,42 +1,60 @@ -"""Test the package-level import checks.""" +"""Test the package identity and coexistence with upstream diskcache.""" -import warnings +import importlib.util +import os.path as op +import subprocess as sp +import sys import pytest -import diskcache as dc +import diskcache_community as dc +COEXIST_SCRIPT = """ +import os.path as op +import sys -def test_warns_when_upstream_distribution_is_installed(tmp_path): - (tmp_path / 'diskcache-5.6.3.dist-info').mkdir() +import diskcache +import diskcache_community - with pytest.warns(RuntimeWarning, match='pip uninstall diskcache'): - assert dc._warn_if_upstream_installed(str(tmp_path)) +directory = sys.argv[1] +assert diskcache.__version__ == '5.6.3', diskcache.__version__ +assert op.dirname(diskcache.__file__) != op.dirname(diskcache_community.__file__) +with diskcache_community.Cache(op.join(directory, 'by-fork')) as cache: + cache['key'] = {'written': 'fork'} +with diskcache.Cache(op.join(directory, 'by-fork')) as cache: + assert cache['key'] == {'written': 'fork'} -def test_warns_for_legacy_egg_info(tmp_path): - (tmp_path / 'diskcache-5.6.3-py3.12.egg-info').mkdir() +with diskcache.Cache(op.join(directory, 'by-upstream')) as cache: + cache['key'] = {'written': 'upstream'} +with diskcache_community.Cache(op.join(directory, 'by-upstream')) as cache: + assert cache['key'] == {'written': 'upstream'} - with pytest.warns(RuntimeWarning): - assert dc._warn_if_upstream_installed(str(tmp_path)) +print('coexist ok') +""" -def test_silent_when_only_the_fork_is_installed(tmp_path): - (tmp_path / 'diskcache_community-5.6.3.dist-info').mkdir() - (tmp_path / 'diskcache').mkdir() +def test_identity(): + assert dc.__name__ == 'diskcache_community' + assert dc.__title__ == 'diskcache-community' - with warnings.catch_warnings(): - warnings.simplefilter('error') - assert not dc._warn_if_upstream_installed(str(tmp_path)) +def test_does_not_provide_the_upstream_package(): + spec = importlib.util.find_spec('diskcache') + if spec is None: + return + ours = op.dirname(op.abspath(dc.__file__)) + assert op.dirname(op.abspath(spec.origin)) != ours -def test_silent_when_the_directory_is_missing(tmp_path): - with warnings.catch_warnings(): - warnings.simplefilter('error') - assert not dc._warn_if_upstream_installed(str(tmp_path / 'none')) - -def test_silent_in_this_environment(): - with warnings.catch_warnings(): - warnings.simplefilter('error') - assert not dc._warn_if_upstream_installed() +def test_coexists_with_upstream(tmp_path): + if importlib.util.find_spec('diskcache') is None: + pytest.skip('the upstream diskcache distribution is not installed') + result = sp.run( + [sys.executable, '-c', COEXIST_SCRIPT, str(tmp_path)], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert 'coexist ok' in result.stdout diff --git a/tests/test_readonly.py b/tests/test_readonly.py index ce99219..f4fede2 100644 --- a/tests/test_readonly.py +++ b/tests/test_readonly.py @@ -10,7 +10,7 @@ import pytest -import diskcache as dc +import diskcache_community as dc POSIX = os.name != 'nt' ROOT = getattr(os, 'geteuid', lambda: 1)() == 0 @@ -371,7 +371,7 @@ def test_read_only_django(directory): if not settings.configured: pytest.skip('Django settings are not configured') - from diskcache import DjangoCache + from diskcache_community import DjangoCache cache = DjangoCache(directory, {}) cache.set('a', 1) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index c197570..3dbb620 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -1,4 +1,4 @@ -"""Test diskcache.recipes.""" +"""Test diskcache_community.recipes.""" import shutil import threading @@ -6,7 +6,7 @@ import pytest -import diskcache as dc +import diskcache_community as dc @pytest.fixture diff --git a/tests/test_safedisk.py b/tests/test_safedisk.py index 64f27d8..c6153e6 100644 --- a/tests/test_safedisk.py +++ b/tests/test_safedisk.py @@ -1,4 +1,4 @@ -"""Test diskcache.SafeDisk.""" +"""Test diskcache_community.SafeDisk.""" import collections as co import datetime as dt @@ -17,8 +17,8 @@ import pytest -import diskcache as dc -from diskcache.core import MODE_BINARY, MODE_PICKLE, SafeUnpickler +import diskcache_community as dc +from diskcache_community.core import MODE_BINARY, MODE_PICKLE, SafeUnpickler pytestmark = pytest.mark.filterwarnings('ignore', category=dc.EmptyDirWarning) @@ -619,7 +619,7 @@ def test_django_disk_option(directory): pytest.importorskip('django') for params in ( - {'DISK': 'diskcache.SafeDisk'}, + {'DISK': 'diskcache_community.SafeDisk'}, {'DISK': dc.SafeDisk}, {'OPTIONS': {'disk': dc.SafeDisk}}, ): @@ -639,4 +639,4 @@ def test_django_disk_option(directory): default.close() with pytest.raises(ImportError): - dc.DjangoCache(directory, {'DISK': 'diskcache.NoSuchDisk'}) + dc.DjangoCache(directory, {'DISK': 'diskcache_community.NoSuchDisk'}) diff --git a/tox.ini b/tox.ini index 49e4784..cdd093b 100644 --- a/tox.ini +++ b/tox.ini @@ -15,11 +15,11 @@ setenv= PYTHONPATH={toxinidir} [testenv:blue] -commands=blue {toxinidir}/diskcache {toxinidir}/tests +commands=blue {toxinidir}/diskcache_community {toxinidir}/tests deps=blue [testenv:bluecheck] -commands=blue --check {toxinidir}/diskcache {toxinidir}/tests +commands=blue --check {toxinidir}/diskcache_community {toxinidir}/tests deps=blue [testenv:doc8] @@ -33,23 +33,23 @@ commands=make html deps=-r{toxinidir}/docs/requirements.txt [testenv:flake8] -commands=flake8 {toxinidir}/diskcache {toxinidir}/tests +commands=flake8 {toxinidir}/diskcache_community {toxinidir}/tests deps=flake8 [testenv:isort] -commands=isort {toxinidir}/diskcache {toxinidir}/tests +commands=isort {toxinidir}/diskcache_community {toxinidir}/tests deps=isort [testenv:isortcheck] -commands=isort --check {toxinidir}/diskcache {toxinidir}/tests +commands=isort --check {toxinidir}/diskcache_community {toxinidir}/tests deps=isort [testenv:mypy] -commands=mypy {toxinidir}/diskcache +commands=mypy {toxinidir}/diskcache_community deps=mypy [testenv:pylint] -commands=pylint {toxinidir}/diskcache +commands=pylint {toxinidir}/diskcache_community deps= django==4.2.* pylint<3.3 @@ -68,7 +68,7 @@ addopts= --cov-branch --cov-fail-under=98 --cov-report=term-missing - --cov=diskcache + --cov=diskcache_community --doctest-glob="*.rst" --ignore docs/case-study-web-crawler.rst --ignore docs/sf-python-2017-meetup-talk.rst