Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ 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'
import os
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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<dir>/ab/cd/<28 hex>.val` for values and `%03d` for `FanoutCache` shards;
- the `RAW`, `BINARY`, `TEXT` and `PICKLE` serialization and the bytes `JSONDisk` writes;
Expand Down
69 changes: 39 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand All @@ -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)
```
Expand All @@ -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
}
}
```
Expand All @@ -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)
```
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
40 changes: 0 additions & 40 deletions diskcache/__init__.py → diskcache_community/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
The :doc:`tutorial` provides a helpful walkthrough of most methods.
"""

import os
import warnings

from .core import (
DEFAULT_SETTINGS,
ENOVAL,
Expand Down Expand Up @@ -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
Expand Down
File renamed without changes.
8 changes: 5 additions & 3 deletions diskcache/core.py → diskcache_community/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
<diskcache.Cache.push>`, the `key` or `value` may not be known when the
<diskcache_community.Cache.push>`, the `key` or `value` may not be known when the
item is stored in the cache.

:param key: key for item (default UNKNOWN)
Expand Down Expand Up @@ -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-')
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <diskcache.FanoutCache>`, ``DISK`` selects the
:class:`Disk <diskcache.Disk>` type or subclass used for serialization
as a class or a dotted import path such as ``'diskcache.SafeDisk'``,
:class:`FanoutCache <diskcache_community.FanoutCache>`, ``DISK`` selects the
:class:`Disk <diskcache_community.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
Expand Down
File renamed without changes.
10 changes: 5 additions & 5 deletions diskcache/persistent.py → diskcache_community/persistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <diskcache.Disk>`)
:class:`Disk <diskcache_community.Disk>`)

"""
self._cache = Cache(directory, disk=disk, eviction_policy='none')
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 <diskcache.Disk>`
The keyword argument `disk` selects the :class:`Disk <diskcache_community.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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading