pickle-debug 1.0__tar.gz → 1.1__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.
- {pickle_debug-1.0/src/pickle_debug.egg-info → pickle_debug-1.1}/PKG-INFO +19 -4
- {pickle_debug-1.0 → pickle_debug-1.1}/README.md +18 -3
- {pickle_debug-1.0 → pickle_debug-1.1}/pyproject.toml +1 -1
- {pickle_debug-1.0 → pickle_debug-1.1}/src/pickle_debug/__init__.py +60 -2
- {pickle_debug-1.0 → pickle_debug-1.1/src/pickle_debug.egg-info}/PKG-INFO +19 -4
- {pickle_debug-1.0 → pickle_debug-1.1}/src/pickle_debug.egg-info/SOURCES.txt +2 -1
- {pickle_debug-1.0 → pickle_debug-1.1}/src/test/patch_test.py +4 -3
- {pickle_debug-1.0 → pickle_debug-1.1}/src/test/ttest.py +9 -9
- pickle_debug-1.1/src/test/unpicklable_test.py +46 -0
- {pickle_debug-1.0 → pickle_debug-1.1}/LICENSE +0 -0
- {pickle_debug-1.0 → pickle_debug-1.1}/setup.cfg +0 -0
- {pickle_debug-1.0 → pickle_debug-1.1}/src/pickle_debug.egg-info/dependency_links.txt +0 -0
- {pickle_debug-1.0 → pickle_debug-1.1}/src/pickle_debug.egg-info/top_level.txt +0 -0
- {pickle_debug-1.0 → pickle_debug-1.1}/src/test/pd_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pickle_debug
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1
|
|
4
4
|
Summary: Store expensive objects from debugging
|
|
5
5
|
Author: Gerard
|
|
6
6
|
Author-email: gweatherby@uchc.edu
|
|
@@ -77,6 +77,19 @@ Decorator. Returns a wrapper (using `functools.wraps`) that:
|
|
|
77
77
|
Results are written to a temporary file and then renamed, so an interrupted run does not leave a partial
|
|
78
78
|
cache entry.
|
|
79
79
|
|
|
80
|
+
#### Unpicklable attributes
|
|
81
|
+
If an object (in the result or in the arguments) has attributes that cannot be pickled, such as locks,
|
|
82
|
+
open files, sockets or lambdas, those attributes are left out and the rest of the object is cached. This
|
|
83
|
+
applies to nested objects too, and covers both `__dict__` and `__slots__` attributes. Each dropped
|
|
84
|
+
attribute is logged at `DEBUG` level.
|
|
85
|
+
|
|
86
|
+
An object loaded from the cache does not have the dropped attributes, so accessing them raises
|
|
87
|
+
`AttributeError`. If the code you are debugging needs them, recreate them after the call.
|
|
88
|
+
|
|
89
|
+
Only attributes are filtered. An unpicklable value at the top level, or directly inside a list, tuple,
|
|
90
|
+
dict or set, still raises an exception. So does an object with a custom `__reduce__` or a `__getstate__`
|
|
91
|
+
that does not return a dict.
|
|
92
|
+
|
|
80
93
|
### `reset()`
|
|
81
94
|
Delete the whole cache directory. It refuses (raises `RuntimeError`) if the directory contains anything
|
|
82
95
|
other than `.pickle` and `.tmp` files, to avoid deleting unrelated data if `enable()` was pointed at the
|
|
@@ -84,8 +97,10 @@ wrong place. It does nothing if caching is not enabled or the directory does not
|
|
|
84
97
|
|
|
85
98
|
## Caveats
|
|
86
99
|
|
|
87
|
-
- **Arguments and results must be picklable
|
|
88
|
-
|
|
100
|
+
- **Arguments and results must be picklable**, apart from object attributes, which are left out (see
|
|
101
|
+
[Unpicklable attributes](#unpicklable-attributes)). The arguments are pickled to build the key, and the
|
|
102
|
+
result is pickled to store it. Because dropped attributes are not part of the key, two arguments that
|
|
103
|
+
differ only in unpicklable attributes share a cache entry.
|
|
89
104
|
- **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
|
|
90
105
|
different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
|
|
91
106
|
- **The key does not include the function's code or any files it reads.** If you change the function or
|
|
@@ -99,4 +114,4 @@ wrong place. It does nothing if caching is not enabled or the directory does not
|
|
|
99
114
|
## Logging
|
|
100
115
|
|
|
101
116
|
The module logs to the `pickle_debug` logger at `DEBUG` level when it enables caching, reads from the
|
|
102
|
-
cache,
|
|
117
|
+
cache, stores a result, or leaves an unpicklable attribute out.
|
|
@@ -65,6 +65,19 @@ Decorator. Returns a wrapper (using `functools.wraps`) that:
|
|
|
65
65
|
Results are written to a temporary file and then renamed, so an interrupted run does not leave a partial
|
|
66
66
|
cache entry.
|
|
67
67
|
|
|
68
|
+
#### Unpicklable attributes
|
|
69
|
+
If an object (in the result or in the arguments) has attributes that cannot be pickled, such as locks,
|
|
70
|
+
open files, sockets or lambdas, those attributes are left out and the rest of the object is cached. This
|
|
71
|
+
applies to nested objects too, and covers both `__dict__` and `__slots__` attributes. Each dropped
|
|
72
|
+
attribute is logged at `DEBUG` level.
|
|
73
|
+
|
|
74
|
+
An object loaded from the cache does not have the dropped attributes, so accessing them raises
|
|
75
|
+
`AttributeError`. If the code you are debugging needs them, recreate them after the call.
|
|
76
|
+
|
|
77
|
+
Only attributes are filtered. An unpicklable value at the top level, or directly inside a list, tuple,
|
|
78
|
+
dict or set, still raises an exception. So does an object with a custom `__reduce__` or a `__getstate__`
|
|
79
|
+
that does not return a dict.
|
|
80
|
+
|
|
68
81
|
### `reset()`
|
|
69
82
|
Delete the whole cache directory. It refuses (raises `RuntimeError`) if the directory contains anything
|
|
70
83
|
other than `.pickle` and `.tmp` files, to avoid deleting unrelated data if `enable()` was pointed at the
|
|
@@ -72,8 +85,10 @@ wrong place. It does nothing if caching is not enabled or the directory does not
|
|
|
72
85
|
|
|
73
86
|
## Caveats
|
|
74
87
|
|
|
75
|
-
- **Arguments and results must be picklable
|
|
76
|
-
|
|
88
|
+
- **Arguments and results must be picklable**, apart from object attributes, which are left out (see
|
|
89
|
+
[Unpicklable attributes](#unpicklable-attributes)). The arguments are pickled to build the key, and the
|
|
90
|
+
result is pickled to store it. Because dropped attributes are not part of the key, two arguments that
|
|
91
|
+
differ only in unpicklable attributes share a cache entry.
|
|
77
92
|
- **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
|
|
78
93
|
different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
|
|
79
94
|
- **The key does not include the function's code or any files it reads.** If you change the function or
|
|
@@ -87,4 +102,4 @@ wrong place. It does nothing if caching is not enabled or the directory does not
|
|
|
87
102
|
## Logging
|
|
88
103
|
|
|
89
104
|
The module logs to the `pickle_debug` logger at `DEBUG` level when it enables caching, reads from the
|
|
90
|
-
cache,
|
|
105
|
+
cache, stores a result, or leaves an unpicklable attribute out.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import hashlib
|
|
3
3
|
import importlib.metadata
|
|
4
|
+
import io
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import pickle
|
|
@@ -46,12 +47,69 @@ def reset() -> None:
|
|
|
46
47
|
raise RuntimeError(f"Unable to remove cache {_CACHE_DIRECTORY}")
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
class _FilteringPickler(pickle.Pickler):
|
|
51
|
+
"""Pickler that leaves out object attributes which can't be pickled"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, file, memo: dict[int, list] | None = None):
|
|
54
|
+
# C Pickler doesn't expose its protocol, so keep our own copy
|
|
55
|
+
self._protocol = pickle.DEFAULT_PROTOCOL
|
|
56
|
+
super().__init__(file, self._protocol)
|
|
57
|
+
# id -> [value, picklable], shared with nested test picklers. Holding value keeps id from being reused.
|
|
58
|
+
# picklable is True while test in progress to handle cycles
|
|
59
|
+
self._picklable_memo = {} if memo is None else memo
|
|
60
|
+
|
|
61
|
+
def _picklable(self, value: Any) -> bool:
|
|
62
|
+
key = id(value)
|
|
63
|
+
if key not in self._picklable_memo:
|
|
64
|
+
entry = self._picklable_memo[key] = [value, True]
|
|
65
|
+
try:
|
|
66
|
+
_FilteringPickler(io.BytesIO(), self._picklable_memo).dump(value)
|
|
67
|
+
except Exception:
|
|
68
|
+
entry[1] = False
|
|
69
|
+
return self._picklable_memo[key][1]
|
|
70
|
+
|
|
71
|
+
def _filter(self, obj: Any, state: dict) -> dict:
|
|
72
|
+
kept = {}
|
|
73
|
+
for name, value in state.items():
|
|
74
|
+
if self._picklable(value):
|
|
75
|
+
kept[name] = value
|
|
76
|
+
else:
|
|
77
|
+
pickle_debug_logger.debug(f"leaving unpicklable {type(obj).__qualname__}.{name} out of cache")
|
|
78
|
+
return kept
|
|
79
|
+
|
|
80
|
+
def reducer_override(self, obj):
|
|
81
|
+
if isinstance(obj, type) or not hasattr(obj, '__dict__') and not hasattr(type(obj), '__slots__'):
|
|
82
|
+
return NotImplemented
|
|
83
|
+
try:
|
|
84
|
+
reduced = obj.__reduce_ex__(self._protocol)
|
|
85
|
+
except Exception:
|
|
86
|
+
return NotImplemented
|
|
87
|
+
if not isinstance(reduced, tuple) or len(reduced) < 3:
|
|
88
|
+
return NotImplemented
|
|
89
|
+
state = reduced[2]
|
|
90
|
+
if isinstance(state, dict):
|
|
91
|
+
state = self._filter(obj, state)
|
|
92
|
+
elif (isinstance(state, tuple) and len(state) == 2
|
|
93
|
+
and all(s is None or isinstance(s, dict) for s in state)):
|
|
94
|
+
state = tuple(None if s is None else self._filter(obj, s) for s in state)
|
|
95
|
+
else:
|
|
96
|
+
return NotImplemented
|
|
97
|
+
return reduced[:2] + (state,) + reduced[3:]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _dumps(obj: Any) -> bytes:
|
|
101
|
+
buffer = io.BytesIO()
|
|
102
|
+
_FilteringPickler(buffer).dump(obj)
|
|
103
|
+
return buffer.getvalue()
|
|
104
|
+
|
|
105
|
+
|
|
49
106
|
def _save(path: Path, result: Any) -> None:
|
|
50
107
|
"""Write via temporary file so an interrupted write doesn't leave partial result"""
|
|
108
|
+
data = _dumps(result)
|
|
51
109
|
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix='.tmp')
|
|
52
110
|
try:
|
|
53
111
|
with os.fdopen(fd, 'wb') as f:
|
|
54
|
-
|
|
112
|
+
f.write(data)
|
|
55
113
|
os.replace(tmp, path)
|
|
56
114
|
except BaseException:
|
|
57
115
|
Path(tmp).unlink(missing_ok=True)
|
|
@@ -66,7 +124,7 @@ def cached(func: Callable) -> Callable:
|
|
|
66
124
|
directory = _CACHE_DIRECTORY
|
|
67
125
|
if directory is None:
|
|
68
126
|
return func(*args, **kwargs)
|
|
69
|
-
key =
|
|
127
|
+
key = _dumps((func.__module__, func.__qualname__, args, sorted(kwargs.items())))
|
|
70
128
|
path = directory / (hashlib.sha256(key).hexdigest() + _SUFFIX)
|
|
71
129
|
try:
|
|
72
130
|
with open(path, 'rb') as f:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pickle_debug
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1
|
|
4
4
|
Summary: Store expensive objects from debugging
|
|
5
5
|
Author: Gerard
|
|
6
6
|
Author-email: gweatherby@uchc.edu
|
|
@@ -77,6 +77,19 @@ Decorator. Returns a wrapper (using `functools.wraps`) that:
|
|
|
77
77
|
Results are written to a temporary file and then renamed, so an interrupted run does not leave a partial
|
|
78
78
|
cache entry.
|
|
79
79
|
|
|
80
|
+
#### Unpicklable attributes
|
|
81
|
+
If an object (in the result or in the arguments) has attributes that cannot be pickled, such as locks,
|
|
82
|
+
open files, sockets or lambdas, those attributes are left out and the rest of the object is cached. This
|
|
83
|
+
applies to nested objects too, and covers both `__dict__` and `__slots__` attributes. Each dropped
|
|
84
|
+
attribute is logged at `DEBUG` level.
|
|
85
|
+
|
|
86
|
+
An object loaded from the cache does not have the dropped attributes, so accessing them raises
|
|
87
|
+
`AttributeError`. If the code you are debugging needs them, recreate them after the call.
|
|
88
|
+
|
|
89
|
+
Only attributes are filtered. An unpicklable value at the top level, or directly inside a list, tuple,
|
|
90
|
+
dict or set, still raises an exception. So does an object with a custom `__reduce__` or a `__getstate__`
|
|
91
|
+
that does not return a dict.
|
|
92
|
+
|
|
80
93
|
### `reset()`
|
|
81
94
|
Delete the whole cache directory. It refuses (raises `RuntimeError`) if the directory contains anything
|
|
82
95
|
other than `.pickle` and `.tmp` files, to avoid deleting unrelated data if `enable()` was pointed at the
|
|
@@ -84,8 +97,10 @@ wrong place. It does nothing if caching is not enabled or the directory does not
|
|
|
84
97
|
|
|
85
98
|
## Caveats
|
|
86
99
|
|
|
87
|
-
- **Arguments and results must be picklable
|
|
88
|
-
|
|
100
|
+
- **Arguments and results must be picklable**, apart from object attributes, which are left out (see
|
|
101
|
+
[Unpicklable attributes](#unpicklable-attributes)). The arguments are pickled to build the key, and the
|
|
102
|
+
result is pickled to store it. Because dropped attributes are not part of the key, two arguments that
|
|
103
|
+
differ only in unpicklable attributes share a cache entry.
|
|
89
104
|
- **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
|
|
90
105
|
different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
|
|
91
106
|
- **The key does not include the function's code or any files it reads.** If you change the function or
|
|
@@ -99,4 +114,4 @@ wrong place. It does nothing if caching is not enabled or the directory does not
|
|
|
99
114
|
## Logging
|
|
100
115
|
|
|
101
116
|
The module logs to the `pickle_debug` logger at `DEBUG` level when it enables caching, reads from the
|
|
102
|
-
cache,
|
|
117
|
+
cache, stores a result, or leaves an unpicklable attribute out.
|
|
@@ -8,9 +8,10 @@ def f(x, y=1):
|
|
|
8
8
|
time.sleep(1.5)
|
|
9
9
|
return x + y
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class Slow:
|
|
12
13
|
|
|
13
|
-
def invoke(self,x):
|
|
14
|
+
def invoke(self, x):
|
|
14
15
|
time.sleep(1.5)
|
|
15
16
|
return x * x
|
|
16
17
|
|
|
@@ -18,8 +19,8 @@ class Slow:
|
|
|
18
19
|
pickle_debug.enable("/tmp/mycache")
|
|
19
20
|
|
|
20
21
|
f = pickle_debug.cached(f)
|
|
21
|
-
print(f(5,6))
|
|
22
|
-
print(f(5,6))
|
|
22
|
+
print(f(5, 6))
|
|
23
|
+
print(f(5, 6))
|
|
23
24
|
|
|
24
25
|
slow = Slow()
|
|
25
26
|
slow.invoke = pickle_debug.cached(slow.invoke)
|
|
@@ -7,14 +7,14 @@ import pickle_debug
|
|
|
7
7
|
def f(x, y=1):
|
|
8
8
|
print(f"call {x} {y}")
|
|
9
9
|
time.sleep(1.5)
|
|
10
|
-
return x + y
|
|
10
|
+
return x + y
|
|
11
11
|
|
|
12
|
-
print(f(1,2))
|
|
13
|
-
print(f(1,2))
|
|
14
|
-
pickle_debug.enable("/tmp/mycache")
|
|
15
|
-
print(f(1,2))
|
|
16
|
-
print(f(1,2))
|
|
17
|
-
print(f(3,4))
|
|
18
|
-
print(f(1,2))
|
|
19
|
-
print(f(3,4))
|
|
20
12
|
|
|
13
|
+
print(f(1, 2))
|
|
14
|
+
print(f(1, 2))
|
|
15
|
+
pickle_debug.enable("/tmp/mycache")
|
|
16
|
+
print(f(1, 2))
|
|
17
|
+
print(f(1, 2))
|
|
18
|
+
print(f(3, 4))
|
|
19
|
+
print(f(1, 2))
|
|
20
|
+
print(f(3, 4))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import tempfile
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from pickle_debug import cached, enable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Holder:
|
|
9
|
+
def __init__(self, value):
|
|
10
|
+
self.value = value
|
|
11
|
+
self.lock = threading.Lock()
|
|
12
|
+
self.inner = Inner()
|
|
13
|
+
self.me = self
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Inner:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.ok = 'inner'
|
|
19
|
+
self.callback = lambda: None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Slotted:
|
|
23
|
+
__slots__ = ('a', 'lock')
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.a = 1
|
|
27
|
+
self.lock = threading.Lock()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
calls = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cached
|
|
34
|
+
def make(x):
|
|
35
|
+
calls.append(x)
|
|
36
|
+
return Holder(x), Slotted()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
enable(pathlib.Path(tempfile.mkdtemp()) / 'cache')
|
|
40
|
+
make(5)
|
|
41
|
+
h, s = make(5)
|
|
42
|
+
assert calls == [5], calls
|
|
43
|
+
assert h.value == 5 and not hasattr(h, 'lock') and h.me is h
|
|
44
|
+
assert h.inner.ok == 'inner' and not hasattr(h.inner, 'callback')
|
|
45
|
+
assert s.a == 1 and not hasattr(s, 'lock')
|
|
46
|
+
print('ok')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|