diskcache-community 5.8.2__tar.gz → 5.8.3__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.
- {diskcache_community-5.8.2/diskcache_community.egg-info → diskcache_community-5.8.3}/PKG-INFO +1 -1
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/__init__.py +2 -2
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/_types.pyi +10 -2
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/arrays.py +13 -6
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/core.py +101 -42
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/core.pyi +2 -1
- {diskcache_community-5.8.2 → diskcache_community-5.8.3/diskcache_community.egg-info}/PKG-INFO +1 -1
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_core.py +44 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_safedisk.py +91 -15
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_safedisk_numpy.py +35 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_safedisk_types.py +39 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/LICENSE +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/NOTICE +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/README.md +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/__init__.pyi +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/arrays.pyi +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/cli.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/djangocache.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/djangocache.pyi +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/fanout.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/fanout.pyi +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/persistent.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/persistent.pyi +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/py.typed +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/recipes.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community/recipes.pyi +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community.egg-info/SOURCES.txt +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community.egg-info/dependency_links.txt +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community.egg-info/top_level.txt +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/pyproject.toml +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/setup.cfg +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_default_disk.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_deque.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_djangocache.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_doctest.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_entry_errors.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_fanout.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_golden.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_index.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_init.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_readonly.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_recipes.py +0 -0
- {diskcache_community-5.8.2 → diskcache_community-5.8.3}/tests/test_safejsondisk.py +0 -0
|
@@ -72,8 +72,8 @@ except Exception: # pylint: disable=broad-except # pragma: no cover
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
__title__ = 'diskcache-community'
|
|
75
|
-
__version__ = '5.8.
|
|
76
|
-
__build__ =
|
|
75
|
+
__version__ = '5.8.3'
|
|
76
|
+
__build__ = 0x050803
|
|
77
77
|
__author__ = 'Grant Jenks'
|
|
78
78
|
__license__ = 'Apache 2.0'
|
|
79
79
|
__copyright__ = 'Copyright 2016-2023 Grant Jenks'
|
|
@@ -173,10 +173,18 @@ class BaseCache(Protocol):
|
|
|
173
173
|
retry: bool = ...,
|
|
174
174
|
) -> bool: ...
|
|
175
175
|
def incr(
|
|
176
|
-
self,
|
|
176
|
+
self,
|
|
177
|
+
key: Any,
|
|
178
|
+
delta: int = ...,
|
|
179
|
+
default: int | None = ...,
|
|
180
|
+
retry: bool = ...,
|
|
177
181
|
) -> int: ...
|
|
178
182
|
def decr(
|
|
179
|
-
self,
|
|
183
|
+
self,
|
|
184
|
+
key: Any,
|
|
185
|
+
delta: int = ...,
|
|
186
|
+
default: int | None = ...,
|
|
187
|
+
retry: bool = ...,
|
|
180
188
|
) -> int: ...
|
|
181
189
|
def get( # noqa: PLR0913
|
|
182
190
|
self,
|
|
@@ -93,7 +93,9 @@ def _pack_array(numpy, array):
|
|
|
93
93
|
The buffer is the array's own memory, so pickle copies it once, into
|
|
94
94
|
the row, as a bytearray. An array that is not contiguous or not
|
|
95
95
|
writable is copied first, since pickle stores a read-only buffer as
|
|
96
|
-
bytes, which the reader refuses.
|
|
96
|
+
bytes, which the reader refuses. The buffer is a flat view of bytes,
|
|
97
|
+
since numpy exports no buffer for ``datetime64`` and ``timedelta64``
|
|
98
|
+
data; the dtype is in the header.
|
|
97
99
|
|
|
98
100
|
"""
|
|
99
101
|
# pylint: disable=no-member
|
|
@@ -114,11 +116,16 @@ def _pack_array(numpy, array):
|
|
|
114
116
|
header = buffer.getvalue()
|
|
115
117
|
_read_header(numpy, header, array.nbytes)
|
|
116
118
|
except (ValueError, pickle.UnpicklingError) as error:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
message = 'SafeDisk cannot store an array of dtype %s' % (array.dtype,)
|
|
120
|
+
if array.dtype.hasobject:
|
|
121
|
+
message += (
|
|
122
|
+
'; a pandas DataFrame holds its labels and text columns in'
|
|
123
|
+
' such arrays, so store its records or columns instead'
|
|
124
|
+
)
|
|
125
|
+
raise TypeError(message) from error
|
|
126
|
+
|
|
127
|
+
data = array.reshape(-1, order='A').view(numpy.uint8)
|
|
128
|
+
return header, pickle.PickleBuffer(data)
|
|
122
129
|
|
|
123
130
|
|
|
124
131
|
def _unpack_array(header, data):
|
|
@@ -557,9 +557,11 @@ SAFE_PICKLE_NAMES = {
|
|
|
557
557
|
'uuid': frozenset({'UUID'}),
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
-
# Python types that SafeDisk accepts for writing
|
|
561
|
-
# names above read back.
|
|
562
|
-
#
|
|
560
|
+
# Python types that SafeDisk accepts for writing without pickling them first:
|
|
561
|
+
# exactly the types that the names above read back. Anything else is pickled
|
|
562
|
+
# and accepted only if the reader resolves every name the pickle looks up, so
|
|
563
|
+
# a subclass is refused, because pickle records the subclass, unless it
|
|
564
|
+
# pickles as one of these types.
|
|
563
565
|
|
|
564
566
|
SAFE_TYPES = frozenset(
|
|
565
567
|
{
|
|
@@ -593,15 +595,35 @@ SAFE_TYPES = frozenset(
|
|
|
593
595
|
}
|
|
594
596
|
)
|
|
595
597
|
|
|
598
|
+
# Classes that SafeDisk accepts as objects, such as the classes that
|
|
599
|
+
# memoize(typed=True) puts into keys and the factory of a defaultdict: the
|
|
600
|
+
# classes in SAFE_TYPES that pickle as a name the reader resolves. NoneType
|
|
601
|
+
# pickles as a call to builtins.type, and bool as builtins.bool, which the
|
|
602
|
+
# reader refuses.
|
|
603
|
+
|
|
604
|
+
_SAFE_CLASSES = SAFE_TYPES - {type(None), bool}
|
|
605
|
+
|
|
596
606
|
_SAFE_MAPPINGS = (dict, co.Counter, co.OrderedDict, co.defaultdict)
|
|
597
607
|
_SAFE_SEQUENCES = (list, tuple, set, frozenset, co.deque)
|
|
598
608
|
_SAFE_TZ_TYPES = (type(None), dt.timezone)
|
|
599
609
|
|
|
600
|
-
# Packages whose classes SafeDisk.with_types refuses to allow
|
|
601
|
-
#
|
|
602
|
-
#
|
|
610
|
+
# Packages whose classes SafeDisk.with_types refuses to allow, because a
|
|
611
|
+
# crafted pickle of one can corrupt memory or run code. numpy restores the
|
|
612
|
+
# internal flags of a dtype from its pickle without checking them, so a
|
|
613
|
+
# crafted pickle can make an array read raw bytes as object pointers. scipy's
|
|
614
|
+
# sparse matrices and pyahocorasick's automatons keep index data that their
|
|
615
|
+
# compiled code trusts, bitstruct's compiled formats crash on state they did
|
|
616
|
+
# not build, and torch builds some objects by compiling or running code.
|
|
617
|
+
|
|
618
|
+
_UNSAFE_MODULES = ('numpy', 'ahocorasick', 'bitstruct', 'scipy', 'torch')
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _unsafe_reason(package):
|
|
622
|
+
"""Return why with_types refuses classes of the unsafe `package`."""
|
|
623
|
+
if package == 'numpy':
|
|
624
|
+
return '; use with_numpy() to store arrays'
|
|
625
|
+
return ': a crafted pickle of a %s object is not safe to read' % package
|
|
603
626
|
|
|
604
|
-
_UNSAFE_MODULES = ('numpy',)
|
|
605
627
|
|
|
606
628
|
_PICKLE_STRING_OPS = frozenset(
|
|
607
629
|
{'BINUNICODE', 'BINUNICODE8', 'SHORT_BINUNICODE', 'UNICODE'}
|
|
@@ -838,10 +860,11 @@ class SafeDisk(Disk):
|
|
|
838
860
|
``FanoutCache(directory, disk=SafeDisk)``, ``Deque(disk=SafeDisk)``,
|
|
839
861
|
``Index(disk=SafeDisk)`` or the ``DISK`` option of the Django backend.
|
|
840
862
|
|
|
841
|
-
Objects outside the allowlist, such as instances of your own classes
|
|
842
|
-
Django model instances and responses,
|
|
843
|
-
|
|
844
|
-
|
|
863
|
+
Objects outside the allowlist, such as instances of your own classes and
|
|
864
|
+
Django model instances and responses, need either the default
|
|
865
|
+
:class:`Disk` or a disk from :meth:`with_types` that allows them. The
|
|
866
|
+
allowed classes themselves can be stored too, so ``memoize(typed=True)``
|
|
867
|
+
works.
|
|
845
868
|
|
|
846
869
|
"""
|
|
847
870
|
|
|
@@ -868,7 +891,9 @@ class SafeDisk(Disk):
|
|
|
868
891
|
do nothing beyond storing their fields, such as dataclasses, named
|
|
869
892
|
tuples, enums and pydantic models. Reading looks classes up only in
|
|
870
893
|
modules that are already imported, so import them before reading.
|
|
871
|
-
|
|
894
|
+
Classes of numpy, scipy, torch, bitstruct and pyahocorasick are
|
|
895
|
+
refused, because a crafted pickle of one can corrupt memory or run
|
|
896
|
+
code; use :meth:`with_numpy` to store arrays.
|
|
872
897
|
|
|
873
898
|
>>> import collections
|
|
874
899
|
>>> Point = collections.namedtuple('Point', 'x y')
|
|
@@ -882,17 +907,18 @@ class SafeDisk(Disk):
|
|
|
882
907
|
:return: SafeDisk subclass to pass as ``disk=``
|
|
883
908
|
:raises TypeError: if an entry of `types` is not a class
|
|
884
909
|
:raises ValueError: if an entry of `modules` is not a module name, or
|
|
885
|
-
a type or module
|
|
910
|
+
a type or module belongs to one of those packages
|
|
886
911
|
|
|
887
912
|
"""
|
|
888
913
|
for entry in types:
|
|
889
914
|
if not isinstance(entry, type):
|
|
890
915
|
raise TypeError('with_types takes classes, not %r' % (entry,))
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
916
|
+
for unsafe in _UNSAFE_MODULES:
|
|
917
|
+
if _in_modules(entry.__module__, (unsafe,)):
|
|
918
|
+
raise ValueError(
|
|
919
|
+
'with_types refuses the class %r%s'
|
|
920
|
+
% (entry, _unsafe_reason(unsafe))
|
|
921
|
+
)
|
|
896
922
|
|
|
897
923
|
if isinstance(modules, str):
|
|
898
924
|
modules = (modules,)
|
|
@@ -903,15 +929,14 @@ class SafeDisk(Disk):
|
|
|
903
929
|
part.isidentifier() for part in prefix.split('.')
|
|
904
930
|
):
|
|
905
931
|
raise ValueError('%r is not a module name' % (entry,))
|
|
906
|
-
|
|
907
|
-
_in_modules(prefix, (unsafe,))
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
)
|
|
932
|
+
for unsafe in _UNSAFE_MODULES:
|
|
933
|
+
if _in_modules(prefix, (unsafe,)) or _in_modules(
|
|
934
|
+
unsafe, (prefix,)
|
|
935
|
+
):
|
|
936
|
+
raise ValueError(
|
|
937
|
+
'with_types refuses the module %r%s'
|
|
938
|
+
% (entry, _unsafe_reason(unsafe))
|
|
939
|
+
)
|
|
915
940
|
prefixes.append(prefix)
|
|
916
941
|
|
|
917
942
|
return _derive_disk(
|
|
@@ -933,7 +958,7 @@ class SafeDisk(Disk):
|
|
|
933
958
|
value except in keys, and checks each header before it reads an
|
|
934
959
|
array back. (Plain :class:`SafeDisk` refuses numpy pickles, because
|
|
935
960
|
unpickling an array is not memory-safe.) numpy scalars keep their
|
|
936
|
-
type.
|
|
961
|
+
type, and arrays of ``datetime64`` and ``timedelta64`` are stored too.
|
|
937
962
|
Object arrays, ``ndarray`` subclasses and other numpy scalars raise
|
|
938
963
|
:exc:`TypeError` when written. It combines with :meth:`with_types`
|
|
939
964
|
and :meth:`with_options` in any order.
|
|
@@ -966,22 +991,29 @@ class SafeDisk(Disk):
|
|
|
966
991
|
return arrays.unpickler(self, file)
|
|
967
992
|
return self._plain_unpickler(file)
|
|
968
993
|
|
|
969
|
-
def _extended(self):
|
|
970
|
-
return bool(self.safe_types or self.safe_modules)
|
|
971
|
-
|
|
972
994
|
def _check_object(self, obj, what):
|
|
973
|
-
"""Raise TypeError unless the reader would accept `obj` back.
|
|
995
|
+
"""Raise TypeError unless the reader would accept `obj` back.
|
|
996
|
+
|
|
997
|
+
Outside SAFE_TYPES, an object is accepted when its pickle looks up
|
|
998
|
+
only names the reader resolves, such as a dict subclass that pickles
|
|
999
|
+
as a plain dict, which is then what the reader builds.
|
|
1000
|
+
|
|
1001
|
+
"""
|
|
974
1002
|
try:
|
|
975
1003
|
self._check(obj, what)
|
|
976
1004
|
except TypeError:
|
|
977
|
-
|
|
978
|
-
raise
|
|
1005
|
+
pass
|
|
979
1006
|
else:
|
|
980
1007
|
return
|
|
981
1008
|
|
|
982
|
-
|
|
983
|
-
pickle.dumps(obj, protocol=self.pickle_protocol)
|
|
984
|
-
)
|
|
1009
|
+
try:
|
|
1010
|
+
data = pickle.dumps(obj, protocol=self.pickle_protocol)
|
|
1011
|
+
except (pickle.PicklingError, AttributeError) as error:
|
|
1012
|
+
raise TypeError(
|
|
1013
|
+
'SafeDisk cannot store %s: %s' % (what, error)
|
|
1014
|
+
) from error
|
|
1015
|
+
|
|
1016
|
+
self._check_pickle(data, obj, what)
|
|
985
1017
|
|
|
986
1018
|
def _check_pickle(self, data, obj, what):
|
|
987
1019
|
"""Raise TypeError unless the reader would accept the pickle `data`."""
|
|
@@ -1002,17 +1034,38 @@ class SafeDisk(Disk):
|
|
|
1002
1034
|
except pickle.UnpicklingError:
|
|
1003
1035
|
raise TypeError(
|
|
1004
1036
|
'SafeDisk cannot store %s of type %s.%s: it pickles'
|
|
1005
|
-
' %s.%s, which the allowlist refuses
|
|
1006
|
-
' SafeDisk.with_types()'
|
|
1037
|
+
' %s.%s, which the allowlist refuses%s'
|
|
1007
1038
|
% (
|
|
1008
1039
|
what,
|
|
1009
1040
|
type(obj).__module__,
|
|
1010
1041
|
type(obj).__qualname__,
|
|
1011
1042
|
module,
|
|
1012
1043
|
name,
|
|
1044
|
+
self._refusal_hint(module, name),
|
|
1013
1045
|
)
|
|
1014
1046
|
) from None
|
|
1015
1047
|
|
|
1048
|
+
def _refusal_hint(self, module, name):
|
|
1049
|
+
"""Return how to store what pickles the refused `module`.`name`."""
|
|
1050
|
+
if _in_modules(module, ('numpy',)):
|
|
1051
|
+
if self.safe_numpy:
|
|
1052
|
+
return '; with_numpy() stores only arrays and numpy scalars'
|
|
1053
|
+
return '; store arrays with SafeDisk.with_numpy()'
|
|
1054
|
+
|
|
1055
|
+
for unsafe in _UNSAFE_MODULES:
|
|
1056
|
+
if _in_modules(module, (unsafe,)):
|
|
1057
|
+
return '; with_types() refuses %s classes' % unsafe
|
|
1058
|
+
|
|
1059
|
+
obj = sys.modules.get(module)
|
|
1060
|
+
for part in name.split('.'):
|
|
1061
|
+
obj = getattr(obj, part, None)
|
|
1062
|
+
|
|
1063
|
+
if not isinstance(obj, type):
|
|
1064
|
+
return '; it is not a class, and with_types() allows only classes'
|
|
1065
|
+
if module in ('builtins', '__builtin__'):
|
|
1066
|
+
return ''
|
|
1067
|
+
return '; allow it with SafeDisk.with_types()'
|
|
1068
|
+
|
|
1016
1069
|
def _check(self, obj, what):
|
|
1017
1070
|
"""Raise TypeError unless every object in `obj` is in SAFE_TYPES."""
|
|
1018
1071
|
seen = set()
|
|
@@ -1023,6 +1076,8 @@ class SafeDisk(Disk):
|
|
|
1023
1076
|
type_item = type(item)
|
|
1024
1077
|
|
|
1025
1078
|
if type_item not in SAFE_TYPES:
|
|
1079
|
+
if isinstance(item, type) and item in _SAFE_CLASSES:
|
|
1080
|
+
continue
|
|
1026
1081
|
raise TypeError(
|
|
1027
1082
|
'SafeDisk cannot store %s of type %s.%s'
|
|
1028
1083
|
% (what, type_item.__module__, type_item.__qualname__)
|
|
@@ -1034,7 +1089,7 @@ class SafeDisk(Disk):
|
|
|
1034
1089
|
seen.add(id(item))
|
|
1035
1090
|
if type_item is co.defaultdict:
|
|
1036
1091
|
factory = item.default_factory
|
|
1037
|
-
if factory is not None and factory not in
|
|
1092
|
+
if factory is not None and factory not in _SAFE_CLASSES:
|
|
1038
1093
|
raise TypeError(
|
|
1039
1094
|
'SafeDisk cannot store %s with defaultdict'
|
|
1040
1095
|
' factory %r' % (what, factory)
|
|
@@ -1996,8 +2051,12 @@ class Cache:
|
|
|
1996
2051
|
cur = getattr(self._local, 'cur', None)
|
|
1997
2052
|
|
|
1998
2053
|
if cur is None:
|
|
1999
|
-
|
|
2000
|
-
|
|
2054
|
+
# Through _con, so a subclass that overrides it, to open another
|
|
2055
|
+
# database file say, gets a cursor on that connection.
|
|
2056
|
+
con = self._con
|
|
2057
|
+
cur = getattr(self._local, 'cur', None)
|
|
2058
|
+
if cur is None or cur.connection is not con:
|
|
2059
|
+
cur = self._local.cur = con.cursor()
|
|
2001
2060
|
|
|
2002
2061
|
return cur
|
|
2003
2062
|
|
|
@@ -8,6 +8,7 @@ from typing import (
|
|
|
8
8
|
Final,
|
|
9
9
|
Generator,
|
|
10
10
|
Generic,
|
|
11
|
+
Iterable,
|
|
11
12
|
Literal,
|
|
12
13
|
NoReturn,
|
|
13
14
|
overload,
|
|
@@ -192,7 +193,7 @@ class SafeDisk(Disk):
|
|
|
192
193
|
safe_numpy: bool
|
|
193
194
|
@classmethod
|
|
194
195
|
def with_types(
|
|
195
|
-
cls, *types: type, modules:
|
|
196
|
+
cls, *types: type, modules: str | Iterable[str] = ...
|
|
196
197
|
) -> type[Self]: ...
|
|
197
198
|
@classmethod
|
|
198
199
|
def with_numpy(cls) -> type[Self]: ...
|
|
@@ -2319,6 +2319,50 @@ def test_get_after_close_uses_new_cursor(cache):
|
|
|
2319
2319
|
assert cache._local.cur is not cur
|
|
2320
2320
|
|
|
2321
2321
|
|
|
2322
|
+
class OtherDatabaseCache(dc.Cache):
|
|
2323
|
+
"""Cache written against 5.6.3 that keeps its rows in other.db."""
|
|
2324
|
+
|
|
2325
|
+
@property
|
|
2326
|
+
def _con(self):
|
|
2327
|
+
con = getattr(self._local, 'con', None)
|
|
2328
|
+
if con is None:
|
|
2329
|
+
con = self._local.con = sqlite3.connect(
|
|
2330
|
+
op.join(self._directory, 'other.db'),
|
|
2331
|
+
timeout=self._timeout,
|
|
2332
|
+
isolation_level=None,
|
|
2333
|
+
)
|
|
2334
|
+
return con
|
|
2335
|
+
|
|
2336
|
+
|
|
2337
|
+
def test_get_uses_cursor_of_overridden_con():
|
|
2338
|
+
directory = tempfile.mkdtemp()
|
|
2339
|
+
try:
|
|
2340
|
+
with OtherDatabaseCache(directory) as cache:
|
|
2341
|
+
cache.set('a', 1)
|
|
2342
|
+
assert cache.get('a') == 1
|
|
2343
|
+
assert cache._local.cur.connection is cache._con
|
|
2344
|
+
|
|
2345
|
+
other = []
|
|
2346
|
+
|
|
2347
|
+
def get_in_thread():
|
|
2348
|
+
other.append(cache.get('a'))
|
|
2349
|
+
cache.close()
|
|
2350
|
+
|
|
2351
|
+
thread = threading.Thread(target=get_in_thread)
|
|
2352
|
+
thread.start()
|
|
2353
|
+
thread.join()
|
|
2354
|
+
assert other == [1]
|
|
2355
|
+
|
|
2356
|
+
with OtherDatabaseCache(directory) as cache:
|
|
2357
|
+
assert cache.get('a') == 1
|
|
2358
|
+
assert cache.get('b') is None
|
|
2359
|
+
|
|
2360
|
+
assert op.exists(op.join(directory, 'other.db'))
|
|
2361
|
+
assert not op.exists(op.join(directory, 'cache.db'))
|
|
2362
|
+
finally:
|
|
2363
|
+
shutil.rmtree(directory, ignore_errors=True)
|
|
2364
|
+
|
|
2365
|
+
|
|
2322
2366
|
def test_get_reads_only_value_columns_without_metadata(cache):
|
|
2323
2367
|
cache.set('a', 1, tag='t')
|
|
2324
2368
|
statements = []
|
|
@@ -18,7 +18,12 @@ import zoneinfo
|
|
|
18
18
|
import pytest
|
|
19
19
|
|
|
20
20
|
import diskcache_community as dc
|
|
21
|
-
from diskcache_community.core import
|
|
21
|
+
from diskcache_community.core import (
|
|
22
|
+
MODE_BINARY,
|
|
23
|
+
MODE_PICKLE,
|
|
24
|
+
SAFE_TYPES,
|
|
25
|
+
SafeUnpickler,
|
|
26
|
+
)
|
|
22
27
|
|
|
23
28
|
pytestmark = pytest.mark.filterwarnings('ignore', category=dc.EmptyDirWarning)
|
|
24
29
|
|
|
@@ -104,6 +109,10 @@ SAFE_VALUES = [
|
|
|
104
109
|
decimal.Decimal('-1e100'),
|
|
105
110
|
fractions.Fraction(1, 3),
|
|
106
111
|
uuid.UUID('12345678-1234-5678-1234-567812345678'),
|
|
112
|
+
co.defaultdict(int, {'a': 1}),
|
|
113
|
+
# The classes themselves, as memoize(typed=True) puts them into keys.
|
|
114
|
+
*sorted(SAFE_TYPES - {type(None), bool}, key=repr),
|
|
115
|
+
('f', 1, 'a', int, str),
|
|
107
116
|
]
|
|
108
117
|
|
|
109
118
|
HASHABLE_VALUES = [
|
|
@@ -121,14 +130,18 @@ UNSAFE_VALUES = [
|
|
|
121
130
|
(1, (2, (Custom(),))),
|
|
122
131
|
co.deque([Custom()]),
|
|
123
132
|
co.defaultdict(Custom),
|
|
133
|
+
co.defaultdict(bool),
|
|
124
134
|
dt.datetime(2020, 1, 2, tzinfo=CustomTz()),
|
|
125
135
|
dt.time(1, tzinfo=CustomTz()),
|
|
126
136
|
dt.datetime(2020, 1, 2, tzinfo=zoneinfo.ZoneInfo('UTC')),
|
|
127
137
|
type,
|
|
128
|
-
|
|
138
|
+
type(None),
|
|
139
|
+
bool,
|
|
140
|
+
(1, type(None)),
|
|
141
|
+
Custom,
|
|
129
142
|
len,
|
|
130
143
|
Poison,
|
|
131
|
-
|
|
144
|
+
lambda: None,
|
|
132
145
|
Exception('boom'),
|
|
133
146
|
]
|
|
134
147
|
|
|
@@ -330,21 +343,84 @@ def test_key_rejected(cache):
|
|
|
330
343
|
assert len(cache) == 0
|
|
331
344
|
|
|
332
345
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return value
|
|
346
|
+
@pytest.mark.parametrize('disk', [dc.SafeDisk, dc.SafeDisk.with_types()])
|
|
347
|
+
def test_typed_memoize(directory, disk):
|
|
348
|
+
with dc.Cache(directory, disk=disk) as cache:
|
|
337
349
|
|
|
338
|
-
|
|
339
|
-
identity(
|
|
350
|
+
@cache.memoize(typed=True)
|
|
351
|
+
def identity(value):
|
|
352
|
+
return value
|
|
353
|
+
|
|
354
|
+
assert identity(1) == 1
|
|
355
|
+
assert identity(1.0) == 1.0
|
|
356
|
+
assert identity('a') == 'a'
|
|
357
|
+
assert identity(1) == 1
|
|
358
|
+
assert len(cache) == 3
|
|
359
|
+
|
|
360
|
+
with pytest.raises(TypeError, match='SafeDisk cannot store key'):
|
|
361
|
+
identity(None)
|
|
362
|
+
|
|
363
|
+
with pytest.raises(TypeError, match='SafeDisk cannot store key'):
|
|
364
|
+
identity(True)
|
|
365
|
+
|
|
366
|
+
assert len(cache) == 3
|
|
367
|
+
|
|
368
|
+
with dc.Cache(directory, disk=dc.SafeDisk) as cache:
|
|
369
|
+
assert sorted(key[-1].__name__ for key in cache) == [
|
|
370
|
+
'float',
|
|
371
|
+
'int',
|
|
372
|
+
'str',
|
|
373
|
+
]
|
|
340
374
|
|
|
341
|
-
@cache.memoize()
|
|
342
|
-
def plain(value):
|
|
343
|
-
return value
|
|
344
375
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
376
|
+
class PlainDict(dict):
|
|
377
|
+
"""A dict subclass that pickles as a dict, as DRF's ReturnDict does."""
|
|
378
|
+
|
|
379
|
+
def __reduce__(self):
|
|
380
|
+
return dict, (dict(self),)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@pytest.mark.parametrize(
|
|
384
|
+
'disk',
|
|
385
|
+
[dc.SafeDisk, dc.SafeDisk.with_types(), dc.SafeDisk.with_types(Custom)],
|
|
386
|
+
)
|
|
387
|
+
def test_objects_that_pickle_as_allowed_names_are_stored(directory, disk):
|
|
388
|
+
with dc.Cache(directory, disk=disk) as cache:
|
|
389
|
+
cache['dict'] = PlainDict(a=1)
|
|
390
|
+
cache[PlainDict(b=2).get('b')] = [PlainDict(c=3)]
|
|
391
|
+
cache['object'] = object()
|
|
392
|
+
|
|
393
|
+
assert cache['dict'] == {'a': 1}
|
|
394
|
+
assert type(cache['dict']) is dict
|
|
395
|
+
assert type(cache[2][0]) is dict
|
|
396
|
+
assert type(cache['object']) is object
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def test_refusal_messages(cache):
|
|
400
|
+
def refusal(value):
|
|
401
|
+
with pytest.raises(TypeError, match='SafeDisk cannot store') as info:
|
|
402
|
+
cache['key'] = value
|
|
403
|
+
return str(info.value)
|
|
404
|
+
|
|
405
|
+
assert refusal(Custom()).endswith(
|
|
406
|
+
'it pickles %s.Custom, which the allowlist refuses; allow it with'
|
|
407
|
+
' SafeDisk.with_types()' % __name__
|
|
408
|
+
)
|
|
409
|
+
assert refusal(_execute).endswith(
|
|
410
|
+
'it pickles %s._execute, which the allowlist refuses; it is not a'
|
|
411
|
+
' class, and with_types() allows only classes' % __name__
|
|
412
|
+
)
|
|
413
|
+
assert refusal((1, type(None))).endswith(
|
|
414
|
+
'it pickles builtins.type, which the allowlist refuses'
|
|
415
|
+
)
|
|
416
|
+
assert refusal([bool]).endswith(
|
|
417
|
+
'it pickles builtins.bool, which the allowlist refuses'
|
|
418
|
+
)
|
|
419
|
+
assert refusal(co.defaultdict(bool)).endswith(
|
|
420
|
+
'it pickles builtins.bool, which the allowlist refuses'
|
|
421
|
+
)
|
|
422
|
+
assert refusal(lambda: None).startswith('SafeDisk cannot store value: ')
|
|
423
|
+
assert len(cache) == 0
|
|
348
424
|
|
|
349
425
|
|
|
350
426
|
def test_read_values_untouched(cache):
|
|
@@ -187,6 +187,13 @@ def raw(header, body=b''):
|
|
|
187
187
|
np.zeros((0, 3)),
|
|
188
188
|
np.arange(4.0).reshape(2, 2).T,
|
|
189
189
|
np.arange(3, dtype='>i4'),
|
|
190
|
+
# numpy exports no buffer for these, so the data goes as bytes.
|
|
191
|
+
np.array(['2026-01-01T00:00', 'NaT'], dtype='datetime64[ns]'),
|
|
192
|
+
np.arange(6).astype('datetime64[D]').reshape(2, 3).T,
|
|
193
|
+
np.array(np.datetime64('2026-01-01')),
|
|
194
|
+
np.arange(3).astype('>m8[s]'),
|
|
195
|
+
np.array([(1, 2)], dtype=[('a', 'i4'), ('t', 'M8[us]')]),
|
|
196
|
+
np.zeros(0, dtype='m8[ns]'),
|
|
190
197
|
],
|
|
191
198
|
ids=lambda array: '%s-%s' % (array.dtype, array.shape),
|
|
192
199
|
)
|
|
@@ -910,6 +917,34 @@ def test_safedisk_still_refuses_numpy(directory):
|
|
|
910
917
|
assert len(cache) == 0
|
|
911
918
|
|
|
912
919
|
|
|
920
|
+
@pytest.mark.parametrize(
|
|
921
|
+
'disk', [dc.SafeDisk, dc.SafeDisk.with_types(co.UserDict)]
|
|
922
|
+
)
|
|
923
|
+
def test_numpy_refusals_point_to_with_numpy(directory, disk):
|
|
924
|
+
with dc.Cache(directory, disk=disk) as cache:
|
|
925
|
+
for value in (np.arange(2.0), np.dtype('f8'), [np.float64(1.5)]):
|
|
926
|
+
with pytest.raises(TypeError) as info:
|
|
927
|
+
cache['key'] = value
|
|
928
|
+
assert str(info.value).endswith(
|
|
929
|
+
'store arrays with SafeDisk.with_numpy()'
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def test_with_numpy_refusal_of_other_numpy_objects(directory):
|
|
934
|
+
with dc.Cache(directory, disk=ArrayDisk) as cache:
|
|
935
|
+
with pytest.raises(TypeError) as info:
|
|
936
|
+
cache['key'] = {'dtype': np.dtype('f8')}
|
|
937
|
+
assert str(info.value).endswith(
|
|
938
|
+
'with_numpy() stores only arrays and numpy scalars'
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def test_object_array_refusal_mentions_dataframes(directory):
|
|
943
|
+
with dc.Cache(directory, disk=ArrayDisk) as cache:
|
|
944
|
+
with pytest.raises(TypeError, match='DataFrame .* records'):
|
|
945
|
+
cache['key'] = np.array(['a', None], dtype=object)
|
|
946
|
+
|
|
947
|
+
|
|
913
948
|
def test_safejsondisk_refuses_with_numpy():
|
|
914
949
|
with pytest.raises(TypeError, match='SafeJSONDisk'):
|
|
915
950
|
dc.SafeJSONDisk.with_numpy()
|
|
@@ -11,6 +11,7 @@ import struct
|
|
|
11
11
|
import sys
|
|
12
12
|
import tempfile
|
|
13
13
|
import tracemalloc
|
|
14
|
+
import types
|
|
14
15
|
|
|
15
16
|
import pytest
|
|
16
17
|
|
|
@@ -443,6 +444,44 @@ def test_numpy_is_refused(directory):
|
|
|
443
444
|
cache['value'] = value
|
|
444
445
|
|
|
445
446
|
|
|
447
|
+
@pytest.mark.parametrize(
|
|
448
|
+
'package, module',
|
|
449
|
+
[
|
|
450
|
+
('ahocorasick', 'ahocorasick'),
|
|
451
|
+
('bitstruct', 'bitstruct.c'),
|
|
452
|
+
('scipy', 'scipy.sparse._csr'),
|
|
453
|
+
('torch', 'torch.fx.graph_module'),
|
|
454
|
+
],
|
|
455
|
+
)
|
|
456
|
+
def test_unsafe_packages_are_refused(package, module):
|
|
457
|
+
entry = type('Thing', (), {'__module__': module})
|
|
458
|
+
|
|
459
|
+
with pytest.raises(ValueError, match='refuses the class .*not safe'):
|
|
460
|
+
dc.SafeDisk.with_types(entry)
|
|
461
|
+
|
|
462
|
+
for prefix in (package, module, package + '.'):
|
|
463
|
+
with pytest.raises(ValueError, match='refuses the module .*not safe'):
|
|
464
|
+
dc.SafeDisk.with_types(modules=prefix)
|
|
465
|
+
|
|
466
|
+
# A package whose name only starts with the same letters is allowed.
|
|
467
|
+
dc.SafeDisk.with_types(modules=package + 'x')
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_refusal_names_an_unsafe_package(directory, monkeypatch):
|
|
471
|
+
module = types.ModuleType('bitstruct')
|
|
472
|
+
module.Format = type('Format', (), {'__module__': 'bitstruct'})
|
|
473
|
+
monkeypatch.setitem(sys.modules, 'bitstruct', module)
|
|
474
|
+
|
|
475
|
+
with dc.Cache(directory, disk=dc.SafeDisk) as cache:
|
|
476
|
+
with pytest.raises(TypeError) as info:
|
|
477
|
+
cache['value'] = [module.Format()]
|
|
478
|
+
|
|
479
|
+
assert str(info.value).endswith(
|
|
480
|
+
'it pickles bitstruct.Format, which the allowlist refuses;'
|
|
481
|
+
' with_types() refuses bitstruct classes'
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
446
485
|
def test_modules_are_never_imported_by_a_read():
|
|
447
486
|
name = 'tests.safedisk_models.side_effect'
|
|
448
487
|
data = stack_global(name, 'Thing')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{diskcache_community-5.8.2 → diskcache_community-5.8.3}/diskcache_community.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|