diskcache-community 5.8.1__tar.gz → 5.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {diskcache_community-5.8.1/diskcache_community.egg-info → diskcache_community-5.8.2}/PKG-INFO +4 -3
  2. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/README.md +2 -1
  3. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/__init__.py +4 -2
  4. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/__init__.pyi +2 -0
  5. diskcache_community-5.8.2/diskcache_community/arrays.py +322 -0
  6. diskcache_community-5.8.2/diskcache_community/arrays.pyi +8 -0
  7. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/core.py +379 -96
  8. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/core.pyi +9 -1
  9. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/recipes.py +1 -1
  10. {diskcache_community-5.8.1 → diskcache_community-5.8.2/diskcache_community.egg-info}/PKG-INFO +4 -3
  11. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community.egg-info/SOURCES.txt +5 -1
  12. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/pyproject.toml +1 -1
  13. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_core.py +116 -5
  14. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_default_disk.py +20 -0
  15. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_entry_errors.py +2 -0
  16. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_golden.py +7 -0
  17. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_readonly.py +10 -0
  18. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_recipes.py +20 -0
  19. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_safedisk.py +15 -0
  20. diskcache_community-5.8.2/tests/test_safedisk_numpy.py +1403 -0
  21. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_safedisk_types.py +35 -0
  22. diskcache_community-5.8.2/tests/test_safejsondisk.py +390 -0
  23. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/LICENSE +0 -0
  24. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/NOTICE +0 -0
  25. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/_types.pyi +0 -0
  26. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/cli.py +0 -0
  27. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/djangocache.py +0 -0
  28. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/djangocache.pyi +0 -0
  29. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/fanout.py +0 -0
  30. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/fanout.pyi +0 -0
  31. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/persistent.py +0 -0
  32. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/persistent.pyi +0 -0
  33. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/py.typed +0 -0
  34. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community/recipes.pyi +0 -0
  35. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community.egg-info/dependency_links.txt +0 -0
  36. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/diskcache_community.egg-info/top_level.txt +0 -0
  37. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/setup.cfg +0 -0
  38. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_deque.py +0 -0
  39. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_djangocache.py +0 -0
  40. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_doctest.py +0 -0
  41. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_fanout.py +0 -0
  42. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_index.py +0 -0
  43. {diskcache_community-5.8.1 → diskcache_community-5.8.2}/tests/test_init.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diskcache-community
3
- Version: 5.8.1
3
+ Version: 5.8.2
4
4
  Summary: Disk Cache -- Disk and file backed persistent cache.
5
5
  Author: Grant Jenks
6
6
  Maintainer: diskcache-community contributors
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
25
- Requires-Python: >=3.9
25
+ Requires-Python: >=3.8
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  License-File: NOTICE
@@ -142,7 +142,8 @@ cache.
142
142
  semaphores and throttling
143
143
  - Thread-safe and process-safe, with fork-safe connection handling
144
144
  - Read-only mode, automatic when the directory is not writable
145
- - Opt-in `SafeDisk` for caches whose directory other users can write to
145
+ - Opt-in `SafeDisk` and `SafeJSONDisk` for caches whose directory other
146
+ users can write to, with `SafeDisk.with_numpy()` for numpy arrays
146
147
  - Pure Python on top of the standard library `sqlite3`; no server, no compiler
147
148
  - Tested on CPython 3.10 to 3.14 on Linux, macOS and Windows
148
149
  - Gets in about 2.6 µs and sets in about 19 µs on an Apple M5 Pro in the
@@ -112,7 +112,8 @@ cache.
112
112
  semaphores and throttling
113
113
  - Thread-safe and process-safe, with fork-safe connection handling
114
114
  - Read-only mode, automatic when the directory is not writable
115
- - Opt-in `SafeDisk` for caches whose directory other users can write to
115
+ - Opt-in `SafeDisk` and `SafeJSONDisk` for caches whose directory other
116
+ users can write to, with `SafeDisk.with_numpy()` for numpy arrays
116
117
  - Pure Python on top of the standard library `sqlite3`; no server, no compiler
117
118
  - Tested on CPython 3.10 to 3.14 on Linux, macOS and Windows
118
119
  - Gets in about 2.6 µs and sets in about 19 µs on an Apple M5 Pro in the
@@ -16,6 +16,7 @@ from .core import (
16
16
  JSONDisk,
17
17
  ReadOnlyError,
18
18
  SafeDisk,
19
+ SafeJSONDisk,
19
20
  Timeout,
20
21
  UnknownFileWarning,
21
22
  configure,
@@ -50,6 +51,7 @@ __all__ = [
50
51
  'RLock',
51
52
  'ReadOnlyError',
52
53
  'SafeDisk',
54
+ 'SafeJSONDisk',
53
55
  'Timeout',
54
56
  'UNKNOWN',
55
57
  'UnknownFileWarning',
@@ -70,8 +72,8 @@ except Exception: # pylint: disable=broad-except # pragma: no cover
70
72
 
71
73
 
72
74
  __title__ = 'diskcache-community'
73
- __version__ = '5.8.1'
74
- __build__ = 0x050801
75
+ __version__ = '5.8.2'
76
+ __build__ = 0x050802
75
77
  __author__ = 'Grant Jenks'
76
78
  __license__ = 'Apache 2.0'
77
79
  __copyright__ = 'Copyright 2016-2023 Grant Jenks'
@@ -8,6 +8,7 @@ from diskcache_community.core import EmptyDirWarning as EmptyDirWarning
8
8
  from diskcache_community.core import JSONDisk as JSONDisk
9
9
  from diskcache_community.core import ReadOnlyError as ReadOnlyError
10
10
  from diskcache_community.core import SafeDisk as SafeDisk
11
+ from diskcache_community.core import SafeJSONDisk as SafeJSONDisk
11
12
  from diskcache_community.core import Timeout as Timeout
12
13
  from diskcache_community.core import UnknownFileWarning as UnknownFileWarning
13
14
  from diskcache_community.core import configure as configure
@@ -40,6 +41,7 @@ __all__ = [
40
41
  'RLock',
41
42
  'ReadOnlyError',
42
43
  'SafeDisk',
44
+ 'SafeJSONDisk',
43
45
  'Timeout',
44
46
  'UNKNOWN',
45
47
  'UnknownFileWarning',
@@ -0,0 +1,322 @@
1
+ """Disk that stores numpy arrays without pickling them."""
2
+
3
+ import io
4
+ import math
5
+ import pickle
6
+ import warnings
7
+
8
+ from .core import SAFE_PICKLE_NAMES, Disk, SafeUnpickler, _ReaderMissing
9
+
10
+ # Pickle protocol 5 is the first that stores a buffer in place, so that
11
+ # writing an array copies it once, into the row, and reading a file-backed
12
+ # entry needs no second copy of it.
13
+ _MIN_PROTOCOL = 5
14
+
15
+ # The numpy scalar types that are stored, by the name written to the row, with
16
+ # the Python type that ``item()`` gives for each and the reader insists on. The
17
+ # names do not vary by platform or numpy version, unlike the types' own names.
18
+ _SCALARS = {
19
+ 'bool': ('bool_', bool),
20
+ 'int8': ('int8', int),
21
+ 'int16': ('int16', int),
22
+ 'int32': ('int32', int),
23
+ 'int64': ('int64', int),
24
+ 'uint8': ('uint8', int),
25
+ 'uint16': ('uint16', int),
26
+ 'uint32': ('uint32', int),
27
+ 'uint64': ('uint64', int),
28
+ 'float16': ('float16', float),
29
+ 'float32': ('float32', float),
30
+ 'float64': ('float64', float),
31
+ 'complex64': ('complex64', complex),
32
+ 'complex128': ('complex128', complex),
33
+ 'str': ('str_', str),
34
+ 'bytes': ('bytes_', bytes),
35
+ }
36
+
37
+
38
+ def _import_numpy():
39
+ # pylint: disable=import-error,import-outside-toplevel
40
+ try:
41
+ import numpy
42
+ except ImportError as error:
43
+ raise _ReaderMissing(
44
+ 'SafeDisk needs numpy to read this entry'
45
+ ) from error
46
+ return numpy
47
+
48
+
49
+ def _read_header(numpy, header, size):
50
+ """Return shape, Fortran order and dtype from the ``.npy`` `header`.
51
+
52
+ Raise UnpicklingError unless the header is consistent with `size` bytes
53
+ of data. The sizes are checked because a forged shape must not decide
54
+ what gets read. Anything that goes wrong while parsing, including a
55
+ MemoryError from the Python parser on a crafted header, which
56
+ ``on_error='miss'`` would not treat as an unreadable entry, is reported
57
+ as UnpicklingError.
58
+
59
+ """
60
+ # pylint: disable=broad-except,no-member
61
+ try:
62
+ reader = io.BytesIO(header)
63
+ version = numpy.lib.format.read_magic(reader)
64
+ if version == (1, 0):
65
+ fields = numpy.lib.format.read_array_header_1_0(reader)
66
+ elif version == (2, 0):
67
+ fields = numpy.lib.format.read_array_header_2_0(reader)
68
+ else:
69
+ raise ValueError('unsupported .npy version %r' % (version,))
70
+ except Exception as error:
71
+ raise pickle.UnpicklingError(
72
+ 'SafeDisk refuses the array header'
73
+ ) from error
74
+
75
+ shape, fortran, dtype = fields
76
+ count = math.prod(shape)
77
+
78
+ # A zero-width dtype would let any shape match any amount of data.
79
+ if (
80
+ dtype.hasobject
81
+ or reader.tell() != len(header)
82
+ or dtype.itemsize * count != size
83
+ or (count and not dtype.itemsize)
84
+ ):
85
+ raise pickle.UnpicklingError('SafeDisk refuses the array header')
86
+
87
+ return shape, fortran, dtype
88
+
89
+
90
+ def _pack_array(numpy, array):
91
+ """Return the ``.npy`` header of `array` and a buffer of its data.
92
+
93
+ The buffer is the array's own memory, so pickle copies it once, into
94
+ the row, as a bytearray. An array that is not contiguous or not
95
+ writable is copied first, since pickle stores a read-only buffer as
96
+ bytes, which the reader refuses.
97
+
98
+ """
99
+ # pylint: disable=no-member
100
+ flags = array.flags
101
+ if not (flags.writeable and (flags.c_contiguous or flags.f_contiguous)):
102
+ array = array.copy(order='A')
103
+
104
+ buffer = io.BytesIO()
105
+ try:
106
+ with warnings.catch_warnings():
107
+ # numpy warns about dtypes it would save as pickles or in a
108
+ # format the reader refuses; both are refused below.
109
+ warnings.simplefilter('ignore')
110
+ # Version 1 holds every header the reader accepts: numpy refuses
111
+ # a header over 10000 bytes, and version 1 holds 65535.
112
+ fields = numpy.lib.format.header_data_from_array_1_0(array)
113
+ numpy.lib.format.write_array_header_1_0(buffer, fields)
114
+ header = buffer.getvalue()
115
+ _read_header(numpy, header, array.nbytes)
116
+ except (ValueError, pickle.UnpicklingError) as error:
117
+ raise TypeError(
118
+ 'SafeDisk cannot store an array of dtype %s' % (array.dtype,)
119
+ ) from error
120
+
121
+ return header, pickle.PickleBuffer(array)
122
+
123
+
124
+ def _unpack_array(header, data):
125
+ """Return the array in the bytearray `data`, which it keeps as its data.
126
+
127
+ A view takes no second copy of the array, and stays writable because
128
+ `data` is. `header` and `data` may have been tampered with.
129
+
130
+ """
131
+ # pylint: disable=broad-except
132
+ numpy = _import_numpy()
133
+ shape, fortran, dtype = _read_header(numpy, header, len(data))
134
+
135
+ try:
136
+ if not math.prod(shape):
137
+ return numpy.empty(shape, dtype)
138
+ array = numpy.frombuffer(data, dtype)
139
+ return array.reshape(shape, order='F' if fortran else 'C')
140
+ except Exception as error:
141
+ raise pickle.UnpicklingError(
142
+ 'SafeDisk refuses the array data'
143
+ ) from error
144
+
145
+
146
+ def _scalar_name(scalar):
147
+ """Return the name to store numpy `scalar` under, or raise TypeError."""
148
+ dtype = scalar.dtype
149
+ name = {'U': 'str', 'S': 'bytes'}.get(dtype.kind, dtype.name)
150
+ if name not in _SCALARS:
151
+ raise TypeError(
152
+ 'SafeDisk cannot store a scalar of type numpy.%s'
153
+ % (type(scalar).__name__,)
154
+ )
155
+ return name
156
+
157
+
158
+ def _unpack_scalar(name, value):
159
+ """Return the numpy scalar `value` of type `name`, which may be forged.
160
+
161
+ The value must be of the Python type the name is built from: numpy
162
+ would otherwise build something else, such as ``bytes_(n)``, which is
163
+ `n` zero bytes.
164
+
165
+ """
166
+ # pylint: disable=broad-except,unidiomatic-typecheck
167
+ entry = _SCALARS.get(name) if type(name) is str else None
168
+ if entry is None or type(value) is not entry[1]:
169
+ raise pickle.UnpicklingError('SafeDisk refuses a scalar')
170
+
171
+ numpy = _import_numpy()
172
+
173
+ try:
174
+ return getattr(numpy, entry[0])(value)
175
+ except Exception as error:
176
+ raise pickle.UnpicklingError('SafeDisk refuses a scalar') from error
177
+
178
+
179
+ class _ArrayPickler(pickle.Pickler):
180
+ """Pickler that writes arrays and numpy scalars as persistent IDs."""
181
+
182
+ def __init__(self, file, protocol, numpy):
183
+ super().__init__(file, protocol=protocol)
184
+ self._numpy = numpy
185
+ self._arrays = {}
186
+ self._scalar_names = {}
187
+ self.reduced = False
188
+
189
+ def reducer_override(self, obj):
190
+ """Note that an object was reduced, which can look up a name.
191
+
192
+ CPython skips this hook only for None, booleans and exact ints,
193
+ floats, bytes, strings, dicts, sets, frozensets, lists and tuples,
194
+ none of which pickle a name, so every pickle that looks one up is
195
+ noted here and scanned.
196
+
197
+ """
198
+ # pylint: disable=unused-argument
199
+ self.reduced = True
200
+ return NotImplemented
201
+
202
+ def persistent_id(self, obj):
203
+ # pylint: disable=unidiomatic-typecheck
204
+ numpy = self._numpy
205
+ if isinstance(obj, numpy.ndarray):
206
+ if type(obj) is not numpy.ndarray:
207
+ raise TypeError(
208
+ 'SafeDisk cannot store a subclass of ndarray: %s.%s'
209
+ % (type(obj).__module__, type(obj).__qualname__)
210
+ )
211
+ # pickle's own memo skips persistent IDs, and a value may hand it
212
+ # temporary arrays. The buffer in the ID keeps `obj` alive, so
213
+ # its id is not reused while this memo holds the ID.
214
+ pid = self._arrays.get(id(obj))
215
+ if pid is None:
216
+ pid = self._arrays[id(obj)] = ('npy',) + _pack_array(
217
+ numpy, obj
218
+ )
219
+ return pid
220
+ if isinstance(obj, numpy.generic):
221
+ kind = type(obj)
222
+ name = self._scalar_names.get(kind)
223
+ if name is None:
224
+ name = self._scalar_names[kind] = _scalar_name(obj)
225
+ value = obj.item()
226
+ if isinstance(value, numpy.generic):
227
+ raise TypeError(
228
+ 'SafeDisk cannot store a scalar of type numpy.%s'
229
+ % (kind.__name__,)
230
+ )
231
+ return ('scalar', name, value)
232
+ return None
233
+
234
+
235
+ class _ArrayUnpickler(SafeUnpickler):
236
+ """Unpickler that reads back what _ArrayPickler wrote.
237
+
238
+ Array state is rebuilt from the checked ``.npy`` header alone: the dtype
239
+ slot of a trailing ``BUILD`` accepts only a ``numpy.dtype``, which
240
+ neither the allowlist nor a persistent ID can produce.
241
+
242
+ """
243
+
244
+ def __init__(self, *args, **kwargs):
245
+ super().__init__(*args, **kwargs)
246
+ self._arrays = {}
247
+ self._strings = {}
248
+
249
+ def persistent_load(self, pid):
250
+ # pylint: disable=unidiomatic-typecheck
251
+ if type(pid) is tuple and len(pid) == 3:
252
+ if pid[0] == 'scalar':
253
+ value = pid[2]
254
+ if type(value) not in (str, bytes):
255
+ return _unpack_scalar(pid[1], value)
256
+ # A string the pickle memoized is copied into a numpy scalar
257
+ # once, however many IDs name it, as an array buffer is.
258
+ entry = self._strings.get(id(value))
259
+ if entry is None:
260
+ entry = (value, _unpack_scalar(pid[1], value))
261
+ self._strings[id(value)] = entry
262
+ return entry[1]
263
+ if pid[0] == 'npy':
264
+ header, payload = pid[1], pid[2]
265
+ if type(header) is bytes and type(payload) is bytearray:
266
+ # Decoding each buffer once keeps what a row can decode to
267
+ # within its own size, and shared arrays shared. Holding
268
+ # the pair keeps their ids from being reused.
269
+ key = (id(header), id(payload))
270
+ entry = self._arrays.get(key)
271
+ if entry is None:
272
+ array = _unpack_array(header, payload)
273
+ entry = (header, payload, array)
274
+ self._arrays[key] = entry
275
+ return entry[2]
276
+ raise pickle.UnpicklingError('SafeDisk refuses a persistent ID')
277
+
278
+
279
+ def unpickler(disk, file):
280
+ """Return the unpickler of `disk`, a SafeDisk with ``safe_numpy`` set."""
281
+ return _ArrayUnpickler(
282
+ file,
283
+ SAFE_PICKLE_NAMES,
284
+ disk.safe_types,
285
+ disk.safe_modules,
286
+ )
287
+
288
+
289
+ def store(disk, numpy, value, key):
290
+ """Return the Cache table fields of `value`, which may hold numpy data.
291
+
292
+ `disk` is a SafeDisk with ``safe_numpy`` set and `numpy` the imported
293
+ module. A value with no numpy in it is stored as an ordinary pickle.
294
+
295
+ """
296
+ # pylint: disable=protected-access
297
+ try:
298
+ disk._check(value, 'value')
299
+ except TypeError:
300
+ pass
301
+ else:
302
+ return Disk.store(disk, value, False, key=key)
303
+
304
+ buffer = io.BytesIO()
305
+ protocol = max(disk.pickle_protocol, _MIN_PROTOCOL)
306
+ pickler = _ArrayPickler(buffer, protocol, numpy)
307
+
308
+ try:
309
+ pickler.dump(value)
310
+ except (pickle.PicklingError, AttributeError) as error:
311
+ raise TypeError(
312
+ 'SafeDisk cannot store value: %s' % (error,)
313
+ ) from error
314
+
315
+ data = buffer.getvalue()
316
+
317
+ # Only a reduced object can look up a name, and scanning the pickle
318
+ # would copy every array in it.
319
+ if pickler.reduced:
320
+ disk._check_pickle(data, value, 'value')
321
+
322
+ return disk._store_pickle(data, value, key)
@@ -0,0 +1,8 @@
1
+ from typing import IO, Any
2
+
3
+ from diskcache_community.core import SafeDisk, SafeUnpickler
4
+
5
+ def unpickler(disk: SafeDisk, file: IO[bytes]) -> SafeUnpickler: ...
6
+ def store(
7
+ disk: SafeDisk, numpy: Any, value: Any, key: Any
8
+ ) -> tuple[int, int, str | None, Any]: ...