pickle-debug 1.1__tar.gz → 2.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pickle_debug
3
- Version: 1.1
3
+ Version: 2.0
4
4
  Summary: Store expensive objects from debugging
5
5
  Author: Gerard
6
6
  Author-email: gweatherby@uchc.edu
@@ -59,6 +59,35 @@ slow.invoke = pickle_debug.cached(slow.invoke) # one instance
59
59
  Slow.invoke = pickle_debug.cached(Slow.invoke) # all instances
60
60
  ```
61
61
 
62
+ The decorator also works inside a class body. For a classmethod, put `@cached` below `@classmethod`:
63
+
64
+ ```python
65
+ class Slow:
66
+ @pickle_debug.cached
67
+ def invoke(self, x):
68
+ ...
69
+
70
+ @classmethod
71
+ @pickle_debug.cached
72
+ def build(cls, x):
73
+ ...
74
+ ```
75
+
76
+ A wrapper may be needed to cache some functions, e.g. a method returning a generator:
77
+
78
+ ```python
79
+ def something(x,y,z)->Generator:
80
+
81
+ @pickle_debug.cached
82
+ def cache_something(x,y,z)->list:
83
+ return list(something(x,y,z))
84
+ ```
85
+
86
+ When a cached function is called as a method, `self` (or `cls`) is keyed by its class's name
87
+ (`module.QualifiedName`), not its current value. All instances of a class share cache entries, and the
88
+ instance does not need to be picklable. Subclasses get their own entries. Static methods are keyed like
89
+ plain functions.
90
+
62
91
  ## API
63
92
 
64
93
  ### `enable(directory)`
@@ -70,7 +99,8 @@ Decorator. Returns a wrapper (using `functools.wraps`) that:
70
99
 
71
100
  1. calls `func` directly if caching is not enabled;
72
101
  2. otherwise builds a key from `func.__module__`, `func.__qualname__`, the positional arguments and
73
- the sorted keyword arguments, and hashes it with SHA-256;
102
+ the sorted keyword arguments, and hashes it with SHA-256. If `func` is called as a method, the
103
+ first argument (`self` or `cls`) is replaced by its class's name (see [Methods](#methods));
74
104
  3. returns the pickled result from `<directory>/<hash>.pickle` if present;
75
105
  4. otherwise calls `func`, writes the result to the cache, and returns it.
76
106
 
@@ -101,13 +131,15 @@ wrong place. It does nothing if caching is not enabled or the directory does not
101
131
  [Unpicklable attributes](#unpicklable-attributes)). The arguments are pickled to build the key, and the
102
132
  result is pickled to store it. Because dropped attributes are not part of the key, two arguments that
103
133
  differ only in unpicklable attributes share a cache entry.
134
+ - **Wrappers may be required** for methods that return generators.
104
135
  - **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
105
136
  different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
106
137
  - **The key does not include the function's code or any files it reads.** If you change the function or
107
138
  its input data, call `reset()` or use a new cache directory, or you will get stale results.
108
- - **Bound methods and `self`.** A wrapped bound method (`slow.invoke`) does not include `self` in the key,
109
- so all instances share entries. A wrapped class function (`Slow.invoke`) includes `self`, so the
110
- instance must be picklable and its state is part of the key.
139
+ - **Instance state is not part of the key.** Methods are keyed by the class of `self` or `cls`, not by its
140
+ value, so all instances of a class share entries. If a method's result depends on instance attributes,
141
+ instances with different state will get each other's cached results. Pass that state as an argument
142
+ instead, or call `reset()` between runs.
111
143
  - This is a debugging aid. Do not use it as a production cache. Cached files are loaded with `pickle`,
112
144
  so only use a directory you trust.
113
145
 
@@ -47,6 +47,35 @@ slow.invoke = pickle_debug.cached(slow.invoke) # one instance
47
47
  Slow.invoke = pickle_debug.cached(Slow.invoke) # all instances
48
48
  ```
49
49
 
50
+ The decorator also works inside a class body. For a classmethod, put `@cached` below `@classmethod`:
51
+
52
+ ```python
53
+ class Slow:
54
+ @pickle_debug.cached
55
+ def invoke(self, x):
56
+ ...
57
+
58
+ @classmethod
59
+ @pickle_debug.cached
60
+ def build(cls, x):
61
+ ...
62
+ ```
63
+
64
+ A wrapper may be needed to cache some functions, e.g. a method returning a generator:
65
+
66
+ ```python
67
+ def something(x,y,z)->Generator:
68
+
69
+ @pickle_debug.cached
70
+ def cache_something(x,y,z)->list:
71
+ return list(something(x,y,z))
72
+ ```
73
+
74
+ When a cached function is called as a method, `self` (or `cls`) is keyed by its class's name
75
+ (`module.QualifiedName`), not its current value. All instances of a class share cache entries, and the
76
+ instance does not need to be picklable. Subclasses get their own entries. Static methods are keyed like
77
+ plain functions.
78
+
50
79
  ## API
51
80
 
52
81
  ### `enable(directory)`
@@ -58,7 +87,8 @@ Decorator. Returns a wrapper (using `functools.wraps`) that:
58
87
 
59
88
  1. calls `func` directly if caching is not enabled;
60
89
  2. otherwise builds a key from `func.__module__`, `func.__qualname__`, the positional arguments and
61
- the sorted keyword arguments, and hashes it with SHA-256;
90
+ the sorted keyword arguments, and hashes it with SHA-256. If `func` is called as a method, the
91
+ first argument (`self` or `cls`) is replaced by its class's name (see [Methods](#methods));
62
92
  3. returns the pickled result from `<directory>/<hash>.pickle` if present;
63
93
  4. otherwise calls `func`, writes the result to the cache, and returns it.
64
94
 
@@ -89,13 +119,15 @@ wrong place. It does nothing if caching is not enabled or the directory does not
89
119
  [Unpicklable attributes](#unpicklable-attributes)). The arguments are pickled to build the key, and the
90
120
  result is pickled to store it. Because dropped attributes are not part of the key, two arguments that
91
121
  differ only in unpicklable attributes share a cache entry.
122
+ - **Wrappers may be required** for methods that return generators.
92
123
  - **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
93
124
  different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
94
125
  - **The key does not include the function's code or any files it reads.** If you change the function or
95
126
  its input data, call `reset()` or use a new cache directory, or you will get stale results.
96
- - **Bound methods and `self`.** A wrapped bound method (`slow.invoke`) does not include `self` in the key,
97
- so all instances share entries. A wrapped class function (`Slow.invoke`) includes `self`, so the
98
- instance must be picklable and its state is part of the key.
127
+ - **Instance state is not part of the key.** Methods are keyed by the class of `self` or `cls`, not by its
128
+ value, so all instances of a class share entries. If a method's result depends on instance attributes,
129
+ instances with different state will get each other's cached results. Pass that state as an argument
130
+ instead, or call `reset()` between runs.
99
131
  - This is a debugging aid. Do not use it as a production cache. Cached files are loaded with `pickle`,
100
132
  so only use a directory you trust.
101
133
 
@@ -2,7 +2,7 @@
2
2
  name = "pickle_debug"
3
3
  description = "Store expensive objects from debugging"
4
4
  license = {text = "MIT" }
5
- version = "1.1"
5
+ version = "2.0"
6
6
  dependencies = []
7
7
  requires-python= ">= 3.12"
8
8
  readme = "README.md"
@@ -116,15 +116,32 @@ def _save(path: Path, result: Any) -> None:
116
116
  raise
117
117
 
118
118
 
119
+ def _is_method_receiver(obj: Any, name: str, wrapper: Callable) -> bool:
120
+ """True if obj is the self or cls that wrapper was called through as a method"""
121
+ cls = obj if isinstance(obj, type) else type(obj)
122
+ for klass in cls.__mro__:
123
+ attr = vars(klass).get(name)
124
+ if isinstance(attr, classmethod):
125
+ attr = attr.__func__
126
+ if attr is wrapper:
127
+ return True
128
+ return False
129
+
130
+
119
131
  def cached(func: Callable) -> Callable:
120
- """Decorator: cache result of func in directory set by enable(), keyed by arguments"""
132
+ """Decorator: cache result of func in directory set by enable(), keyed by arguments.
133
+ When used on a method, self (or cls) is keyed by its class name rather than its current value"""
121
134
 
122
135
  @functools.wraps(func)
123
136
  def wrapper(*args, **kwargs):
124
137
  directory = _CACHE_DIRECTORY
125
138
  if directory is None:
126
139
  return func(*args, **kwargs)
127
- key = _dumps((func.__module__, func.__qualname__, args, sorted(kwargs.items())))
140
+ key_args = args
141
+ if args and _is_method_receiver(args[0], func.__name__, wrapper):
142
+ cls = args[0] if isinstance(args[0], type) else type(args[0])
143
+ key_args = (f"{cls.__module__}.{cls.__qualname__}",) + args[1:]
144
+ key = _dumps((func.__module__, func.__qualname__, key_args, sorted(kwargs.items())))
128
145
  path = directory / (hashlib.sha256(key).hexdigest() + _SUFFIX)
129
146
  try:
130
147
  with open(path, 'rb') as f:
@@ -140,3 +157,18 @@ def cached(func: Callable) -> Callable:
140
157
  return result
141
158
 
142
159
  return wrapper
160
+
161
+ def record(name:str,fn,*args,**kwargs):
162
+ p = None
163
+ if _CACHE_DIRECTORY is not None:
164
+ p = _CACHE_DIRECTORY / f"{name}.pickle"
165
+ if p.is_file():
166
+ with open(p,'rb') as f:
167
+ result = pickle.load(f)
168
+ return result
169
+ result = fn(*args,**kwargs)
170
+ if p is not None:
171
+ with open(p,'wb') as f:
172
+ pickle.dump(result,f)
173
+ return result
174
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pickle_debug
3
- Version: 1.1
3
+ Version: 2.0
4
4
  Summary: Store expensive objects from debugging
5
5
  Author: Gerard
6
6
  Author-email: gweatherby@uchc.edu
@@ -59,6 +59,35 @@ slow.invoke = pickle_debug.cached(slow.invoke) # one instance
59
59
  Slow.invoke = pickle_debug.cached(Slow.invoke) # all instances
60
60
  ```
61
61
 
62
+ The decorator also works inside a class body. For a classmethod, put `@cached` below `@classmethod`:
63
+
64
+ ```python
65
+ class Slow:
66
+ @pickle_debug.cached
67
+ def invoke(self, x):
68
+ ...
69
+
70
+ @classmethod
71
+ @pickle_debug.cached
72
+ def build(cls, x):
73
+ ...
74
+ ```
75
+
76
+ A wrapper may be needed to cache some functions, e.g. a method returning a generator:
77
+
78
+ ```python
79
+ def something(x,y,z)->Generator:
80
+
81
+ @pickle_debug.cached
82
+ def cache_something(x,y,z)->list:
83
+ return list(something(x,y,z))
84
+ ```
85
+
86
+ When a cached function is called as a method, `self` (or `cls`) is keyed by its class's name
87
+ (`module.QualifiedName`), not its current value. All instances of a class share cache entries, and the
88
+ instance does not need to be picklable. Subclasses get their own entries. Static methods are keyed like
89
+ plain functions.
90
+
62
91
  ## API
63
92
 
64
93
  ### `enable(directory)`
@@ -70,7 +99,8 @@ Decorator. Returns a wrapper (using `functools.wraps`) that:
70
99
 
71
100
  1. calls `func` directly if caching is not enabled;
72
101
  2. otherwise builds a key from `func.__module__`, `func.__qualname__`, the positional arguments and
73
- the sorted keyword arguments, and hashes it with SHA-256;
102
+ the sorted keyword arguments, and hashes it with SHA-256. If `func` is called as a method, the
103
+ first argument (`self` or `cls`) is replaced by its class's name (see [Methods](#methods));
74
104
  3. returns the pickled result from `<directory>/<hash>.pickle` if present;
75
105
  4. otherwise calls `func`, writes the result to the cache, and returns it.
76
106
 
@@ -101,13 +131,15 @@ wrong place. It does nothing if caching is not enabled or the directory does not
101
131
  [Unpicklable attributes](#unpicklable-attributes)). The arguments are pickled to build the key, and the
102
132
  result is pickled to store it. Because dropped attributes are not part of the key, two arguments that
103
133
  differ only in unpicklable attributes share a cache entry.
134
+ - **Wrappers may be required** for methods that return generators.
104
135
  - **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
105
136
  different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
106
137
  - **The key does not include the function's code or any files it reads.** If you change the function or
107
138
  its input data, call `reset()` or use a new cache directory, or you will get stale results.
108
- - **Bound methods and `self`.** A wrapped bound method (`slow.invoke`) does not include `self` in the key,
109
- so all instances share entries. A wrapped class function (`Slow.invoke`) includes `self`, so the
110
- instance must be picklable and its state is part of the key.
139
+ - **Instance state is not part of the key.** Methods are keyed by the class of `self` or `cls`, not by its
140
+ value, so all instances of a class share entries. If a method's result depends on instance attributes,
141
+ instances with different state will get each other's cached results. Pass that state as an argument
142
+ instead, or call `reset()` between runs.
111
143
  - This is a debugging aid. Do not use it as a production cache. Cached files are loaded with `pickle`,
112
144
  so only use a directory you trust.
113
145
 
@@ -8,5 +8,4 @@ src/pickle_debug.egg-info/dependency_links.txt
8
8
  src/pickle_debug.egg-info/top_level.txt
9
9
  src/test/patch_test.py
10
10
  src/test/pd_test.py
11
- src/test/ttest.py
12
- src/test/unpicklable_test.py
11
+ src/test/ttest.py
@@ -1,46 +0,0 @@
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