pickle-debug 1.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.
- pickle_debug-1.0/LICENSE +21 -0
- pickle_debug-1.0/PKG-INFO +102 -0
- pickle_debug-1.0/README.md +90 -0
- pickle_debug-1.0/pyproject.toml +27 -0
- pickle_debug-1.0/setup.cfg +4 -0
- pickle_debug-1.0/src/pickle_debug/__init__.py +84 -0
- pickle_debug-1.0/src/pickle_debug.egg-info/PKG-INFO +102 -0
- pickle_debug-1.0/src/pickle_debug.egg-info/SOURCES.txt +11 -0
- pickle_debug-1.0/src/pickle_debug.egg-info/dependency_links.txt +1 -0
- pickle_debug-1.0/src/pickle_debug.egg-info/top_level.txt +2 -0
- pickle_debug-1.0/src/test/patch_test.py +27 -0
- pickle_debug-1.0/src/test/pd_test.py +43 -0
- pickle_debug-1.0/src/test/ttest.py +20 -0
pickle_debug-1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NMRhub
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pickle_debug
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Store expensive objects from debugging
|
|
5
|
+
Author: Gerard
|
|
6
|
+
Author-email: gweatherby@uchc.edu
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# pickle-debug
|
|
14
|
+
Pickle expensive operations for debugging operation
|
|
15
|
+
|
|
16
|
+
When debugging code that runs after a slow step (loading a large file, a long computation, a network call),
|
|
17
|
+
re-running the slow step on every iteration wastes time. `pickle_debug` caches the return value of
|
|
18
|
+
selected functions on disk, so the first call computes and stores the result and later calls with the
|
|
19
|
+
same arguments load it from the cache.
|
|
20
|
+
|
|
21
|
+
Requires Python 3.12 or later. No dependencies.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import pickle_debug
|
|
27
|
+
|
|
28
|
+
@pickle_debug.cached
|
|
29
|
+
def load_data(path, limit=None):
|
|
30
|
+
... # slow
|
|
31
|
+
|
|
32
|
+
pickle_debug.enable("/tmp/mycache")
|
|
33
|
+
|
|
34
|
+
load_data("big.csv") # computed, then stored
|
|
35
|
+
load_data("big.csv") # read from cache
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Caching is off until `enable()` is called. Until then, decorated functions simply call through.
|
|
39
|
+
This makes it safe to leave the `@cached` decorators in place and turn caching on only while debugging.
|
|
40
|
+
|
|
41
|
+
### Decorating a function from another module
|
|
42
|
+
|
|
43
|
+
A decorator is an ordinary function, so you can apply it after importing:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import other_module
|
|
47
|
+
other_module.slow = pickle_debug.cached(other_module.slow)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This affects callers that look up `other_module.slow` at call time. Modules that already did
|
|
51
|
+
`from other_module import slow` keep the undecorated function, so patch before they import it.
|
|
52
|
+
|
|
53
|
+
### Methods
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
slow = Slow()
|
|
57
|
+
slow.invoke = pickle_debug.cached(slow.invoke) # one instance
|
|
58
|
+
|
|
59
|
+
Slow.invoke = pickle_debug.cached(Slow.invoke) # all instances
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### `enable(directory)`
|
|
65
|
+
Turn caching on, storing results in `directory` (a `str` or `Path`). The directory is created if
|
|
66
|
+
needed and checked for writability. Raises `OSError` if it cannot be created or written.
|
|
67
|
+
|
|
68
|
+
### `cached(func)`
|
|
69
|
+
Decorator. Returns a wrapper (using `functools.wraps`) that:
|
|
70
|
+
|
|
71
|
+
1. calls `func` directly if caching is not enabled;
|
|
72
|
+
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;
|
|
74
|
+
3. returns the pickled result from `<directory>/<hash>.pickle` if present;
|
|
75
|
+
4. otherwise calls `func`, writes the result to the cache, and returns it.
|
|
76
|
+
|
|
77
|
+
Results are written to a temporary file and then renamed, so an interrupted run does not leave a partial
|
|
78
|
+
cache entry.
|
|
79
|
+
|
|
80
|
+
### `reset()`
|
|
81
|
+
Delete the whole cache directory. It refuses (raises `RuntimeError`) if the directory contains anything
|
|
82
|
+
other than `.pickle` and `.tmp` files, to avoid deleting unrelated data if `enable()` was pointed at the
|
|
83
|
+
wrong place. It does nothing if caching is not enabled or the directory does not exist.
|
|
84
|
+
|
|
85
|
+
## Caveats
|
|
86
|
+
|
|
87
|
+
- **Arguments and results must be picklable.** The arguments are pickled to build the key, and the result
|
|
88
|
+
is pickled to store it.
|
|
89
|
+
- **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
|
|
90
|
+
different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
|
|
91
|
+
- **The key does not include the function's code or any files it reads.** If you change the function or
|
|
92
|
+
its input data, call `reset()` or use a new cache directory, or you will get stale results.
|
|
93
|
+
- **Bound methods and `self`.** A wrapped bound method (`slow.invoke`) does not include `self` in the key,
|
|
94
|
+
so all instances share entries. A wrapped class function (`Slow.invoke`) includes `self`, so the
|
|
95
|
+
instance must be picklable and its state is part of the key.
|
|
96
|
+
- This is a debugging aid. Do not use it as a production cache. Cached files are loaded with `pickle`,
|
|
97
|
+
so only use a directory you trust.
|
|
98
|
+
|
|
99
|
+
## Logging
|
|
100
|
+
|
|
101
|
+
The module logs to the `pickle_debug` logger at `DEBUG` level when it enables caching, reads from the
|
|
102
|
+
cache, or stores a result.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# pickle-debug
|
|
2
|
+
Pickle expensive operations for debugging operation
|
|
3
|
+
|
|
4
|
+
When debugging code that runs after a slow step (loading a large file, a long computation, a network call),
|
|
5
|
+
re-running the slow step on every iteration wastes time. `pickle_debug` caches the return value of
|
|
6
|
+
selected functions on disk, so the first call computes and stores the result and later calls with the
|
|
7
|
+
same arguments load it from the cache.
|
|
8
|
+
|
|
9
|
+
Requires Python 3.12 or later. No dependencies.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import pickle_debug
|
|
15
|
+
|
|
16
|
+
@pickle_debug.cached
|
|
17
|
+
def load_data(path, limit=None):
|
|
18
|
+
... # slow
|
|
19
|
+
|
|
20
|
+
pickle_debug.enable("/tmp/mycache")
|
|
21
|
+
|
|
22
|
+
load_data("big.csv") # computed, then stored
|
|
23
|
+
load_data("big.csv") # read from cache
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Caching is off until `enable()` is called. Until then, decorated functions simply call through.
|
|
27
|
+
This makes it safe to leave the `@cached` decorators in place and turn caching on only while debugging.
|
|
28
|
+
|
|
29
|
+
### Decorating a function from another module
|
|
30
|
+
|
|
31
|
+
A decorator is an ordinary function, so you can apply it after importing:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import other_module
|
|
35
|
+
other_module.slow = pickle_debug.cached(other_module.slow)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This affects callers that look up `other_module.slow` at call time. Modules that already did
|
|
39
|
+
`from other_module import slow` keep the undecorated function, so patch before they import it.
|
|
40
|
+
|
|
41
|
+
### Methods
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
slow = Slow()
|
|
45
|
+
slow.invoke = pickle_debug.cached(slow.invoke) # one instance
|
|
46
|
+
|
|
47
|
+
Slow.invoke = pickle_debug.cached(Slow.invoke) # all instances
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `enable(directory)`
|
|
53
|
+
Turn caching on, storing results in `directory` (a `str` or `Path`). The directory is created if
|
|
54
|
+
needed and checked for writability. Raises `OSError` if it cannot be created or written.
|
|
55
|
+
|
|
56
|
+
### `cached(func)`
|
|
57
|
+
Decorator. Returns a wrapper (using `functools.wraps`) that:
|
|
58
|
+
|
|
59
|
+
1. calls `func` directly if caching is not enabled;
|
|
60
|
+
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;
|
|
62
|
+
3. returns the pickled result from `<directory>/<hash>.pickle` if present;
|
|
63
|
+
4. otherwise calls `func`, writes the result to the cache, and returns it.
|
|
64
|
+
|
|
65
|
+
Results are written to a temporary file and then renamed, so an interrupted run does not leave a partial
|
|
66
|
+
cache entry.
|
|
67
|
+
|
|
68
|
+
### `reset()`
|
|
69
|
+
Delete the whole cache directory. It refuses (raises `RuntimeError`) if the directory contains anything
|
|
70
|
+
other than `.pickle` and `.tmp` files, to avoid deleting unrelated data if `enable()` was pointed at the
|
|
71
|
+
wrong place. It does nothing if caching is not enabled or the directory does not exist.
|
|
72
|
+
|
|
73
|
+
## Caveats
|
|
74
|
+
|
|
75
|
+
- **Arguments and results must be picklable.** The arguments are pickled to build the key, and the result
|
|
76
|
+
is pickled to store it.
|
|
77
|
+
- **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
|
|
78
|
+
different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
|
|
79
|
+
- **The key does not include the function's code or any files it reads.** If you change the function or
|
|
80
|
+
its input data, call `reset()` or use a new cache directory, or you will get stale results.
|
|
81
|
+
- **Bound methods and `self`.** A wrapped bound method (`slow.invoke`) does not include `self` in the key,
|
|
82
|
+
so all instances share entries. A wrapped class function (`Slow.invoke`) includes `self`, so the
|
|
83
|
+
instance must be picklable and its state is part of the key.
|
|
84
|
+
- This is a debugging aid. Do not use it as a production cache. Cached files are loaded with `pickle`,
|
|
85
|
+
so only use a directory you trust.
|
|
86
|
+
|
|
87
|
+
## Logging
|
|
88
|
+
|
|
89
|
+
The module logs to the `pickle_debug` logger at `DEBUG` level when it enables caching, reads from the
|
|
90
|
+
cache, or stores a result.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pickle_debug"
|
|
3
|
+
description = "Store expensive objects from debugging"
|
|
4
|
+
license = {text = "MIT" }
|
|
5
|
+
version = "1.0"
|
|
6
|
+
dependencies = []
|
|
7
|
+
requires-python= ">= 3.12"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Gerard"},
|
|
11
|
+
{email = "gweatherby@uchc.edu"}
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools]
|
|
15
|
+
package-dir = {"" = "src" } # Tells setuptools that the root package is in the "src" directory
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"] # Specify the "src" directory to look for packages
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
# test = ['pytest']
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["setuptools"]
|
|
26
|
+
build-backend = "setuptools.build_meta"
|
|
27
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import hashlib
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import pickle
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
pickle_debug_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
__version__ = importlib.metadata.version('pickle-debug')
|
|
15
|
+
|
|
16
|
+
_CACHE_DIRECTORY: Path | None = None
|
|
17
|
+
_SUFFIX = '.pickle'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def enable(directory: Path | str) -> None:
|
|
21
|
+
"""Enable caching in specified directory. Raises OSError if directory can't be created or written"""
|
|
22
|
+
global _CACHE_DIRECTORY
|
|
23
|
+
directory = Path(directory)
|
|
24
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
# verify we can create and write files
|
|
26
|
+
with tempfile.TemporaryFile(dir=directory) as f:
|
|
27
|
+
f.write(b'test')
|
|
28
|
+
_CACHE_DIRECTORY = directory
|
|
29
|
+
pickle_debug_logger.debug(f"caching to {directory}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def reset() -> None:
|
|
33
|
+
"""Remove entire cache directory. Raises exception if it exists and can't be removed"""
|
|
34
|
+
if _CACHE_DIRECTORY is None or not _CACHE_DIRECTORY.exists():
|
|
35
|
+
return
|
|
36
|
+
unexpected = [p for p in _CACHE_DIRECTORY.iterdir()
|
|
37
|
+
if not (p.is_file() and not p.is_symlink() and p.suffix in (_SUFFIX, '.tmp'))]
|
|
38
|
+
if unexpected:
|
|
39
|
+
raise RuntimeError(f"Not removing {_CACHE_DIRECTORY}, unexpected contents: "
|
|
40
|
+
+ ', '.join(p.name for p in unexpected))
|
|
41
|
+
try:
|
|
42
|
+
shutil.rmtree(_CACHE_DIRECTORY)
|
|
43
|
+
except OSError as e:
|
|
44
|
+
raise RuntimeError(f"Unable to remove cache {_CACHE_DIRECTORY}") from e
|
|
45
|
+
if _CACHE_DIRECTORY.exists():
|
|
46
|
+
raise RuntimeError(f"Unable to remove cache {_CACHE_DIRECTORY}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _save(path: Path, result: Any) -> None:
|
|
50
|
+
"""Write via temporary file so an interrupted write doesn't leave partial result"""
|
|
51
|
+
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix='.tmp')
|
|
52
|
+
try:
|
|
53
|
+
with os.fdopen(fd, 'wb') as f:
|
|
54
|
+
pickle.dump(result, f)
|
|
55
|
+
os.replace(tmp, path)
|
|
56
|
+
except BaseException:
|
|
57
|
+
Path(tmp).unlink(missing_ok=True)
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cached(func: Callable) -> Callable:
|
|
62
|
+
"""Decorator: cache result of func in directory set by enable(), keyed by arguments"""
|
|
63
|
+
|
|
64
|
+
@functools.wraps(func)
|
|
65
|
+
def wrapper(*args, **kwargs):
|
|
66
|
+
directory = _CACHE_DIRECTORY
|
|
67
|
+
if directory is None:
|
|
68
|
+
return func(*args, **kwargs)
|
|
69
|
+
key = pickle.dumps((func.__module__, func.__qualname__, args, sorted(kwargs.items())))
|
|
70
|
+
path = directory / (hashlib.sha256(key).hexdigest() + _SUFFIX)
|
|
71
|
+
try:
|
|
72
|
+
with open(path, 'rb') as f:
|
|
73
|
+
result = pickle.load(f)
|
|
74
|
+
pickle_debug_logger.debug(f"{func.__qualname__} read from {path}")
|
|
75
|
+
return result
|
|
76
|
+
except FileNotFoundError:
|
|
77
|
+
pass
|
|
78
|
+
result = func(*args, **kwargs)
|
|
79
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
_save(path, result)
|
|
81
|
+
pickle_debug_logger.debug(f"{func.__qualname__} stored in {path}")
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
return wrapper
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pickle_debug
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Store expensive objects from debugging
|
|
5
|
+
Author: Gerard
|
|
6
|
+
Author-email: gweatherby@uchc.edu
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# pickle-debug
|
|
14
|
+
Pickle expensive operations for debugging operation
|
|
15
|
+
|
|
16
|
+
When debugging code that runs after a slow step (loading a large file, a long computation, a network call),
|
|
17
|
+
re-running the slow step on every iteration wastes time. `pickle_debug` caches the return value of
|
|
18
|
+
selected functions on disk, so the first call computes and stores the result and later calls with the
|
|
19
|
+
same arguments load it from the cache.
|
|
20
|
+
|
|
21
|
+
Requires Python 3.12 or later. No dependencies.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import pickle_debug
|
|
27
|
+
|
|
28
|
+
@pickle_debug.cached
|
|
29
|
+
def load_data(path, limit=None):
|
|
30
|
+
... # slow
|
|
31
|
+
|
|
32
|
+
pickle_debug.enable("/tmp/mycache")
|
|
33
|
+
|
|
34
|
+
load_data("big.csv") # computed, then stored
|
|
35
|
+
load_data("big.csv") # read from cache
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Caching is off until `enable()` is called. Until then, decorated functions simply call through.
|
|
39
|
+
This makes it safe to leave the `@cached` decorators in place and turn caching on only while debugging.
|
|
40
|
+
|
|
41
|
+
### Decorating a function from another module
|
|
42
|
+
|
|
43
|
+
A decorator is an ordinary function, so you can apply it after importing:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import other_module
|
|
47
|
+
other_module.slow = pickle_debug.cached(other_module.slow)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This affects callers that look up `other_module.slow` at call time. Modules that already did
|
|
51
|
+
`from other_module import slow` keep the undecorated function, so patch before they import it.
|
|
52
|
+
|
|
53
|
+
### Methods
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
slow = Slow()
|
|
57
|
+
slow.invoke = pickle_debug.cached(slow.invoke) # one instance
|
|
58
|
+
|
|
59
|
+
Slow.invoke = pickle_debug.cached(Slow.invoke) # all instances
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### `enable(directory)`
|
|
65
|
+
Turn caching on, storing results in `directory` (a `str` or `Path`). The directory is created if
|
|
66
|
+
needed and checked for writability. Raises `OSError` if it cannot be created or written.
|
|
67
|
+
|
|
68
|
+
### `cached(func)`
|
|
69
|
+
Decorator. Returns a wrapper (using `functools.wraps`) that:
|
|
70
|
+
|
|
71
|
+
1. calls `func` directly if caching is not enabled;
|
|
72
|
+
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;
|
|
74
|
+
3. returns the pickled result from `<directory>/<hash>.pickle` if present;
|
|
75
|
+
4. otherwise calls `func`, writes the result to the cache, and returns it.
|
|
76
|
+
|
|
77
|
+
Results are written to a temporary file and then renamed, so an interrupted run does not leave a partial
|
|
78
|
+
cache entry.
|
|
79
|
+
|
|
80
|
+
### `reset()`
|
|
81
|
+
Delete the whole cache directory. It refuses (raises `RuntimeError`) if the directory contains anything
|
|
82
|
+
other than `.pickle` and `.tmp` files, to avoid deleting unrelated data if `enable()` was pointed at the
|
|
83
|
+
wrong place. It does nothing if caching is not enabled or the directory does not exist.
|
|
84
|
+
|
|
85
|
+
## Caveats
|
|
86
|
+
|
|
87
|
+
- **Arguments and results must be picklable.** The arguments are pickled to build the key, and the result
|
|
88
|
+
is pickled to store it.
|
|
89
|
+
- **The key is the pickled arguments.** Equal arguments that pickle differently (for example sets with a
|
|
90
|
+
different iteration order) may miss the cache. Objects whose pickle changes between calls never hit it.
|
|
91
|
+
- **The key does not include the function's code or any files it reads.** If you change the function or
|
|
92
|
+
its input data, call `reset()` or use a new cache directory, or you will get stale results.
|
|
93
|
+
- **Bound methods and `self`.** A wrapped bound method (`slow.invoke`) does not include `self` in the key,
|
|
94
|
+
so all instances share entries. A wrapped class function (`Slow.invoke`) includes `self`, so the
|
|
95
|
+
instance must be picklable and its state is part of the key.
|
|
96
|
+
- This is a debugging aid. Do not use it as a production cache. Cached files are loaded with `pickle`,
|
|
97
|
+
so only use a directory you trust.
|
|
98
|
+
|
|
99
|
+
## Logging
|
|
100
|
+
|
|
101
|
+
The module logs to the `pickle_debug` logger at `DEBUG` level when it enables caching, reads from the
|
|
102
|
+
cache, or stores a result.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pickle_debug/__init__.py
|
|
5
|
+
src/pickle_debug.egg-info/PKG-INFO
|
|
6
|
+
src/pickle_debug.egg-info/SOURCES.txt
|
|
7
|
+
src/pickle_debug.egg-info/dependency_links.txt
|
|
8
|
+
src/pickle_debug.egg-info/top_level.txt
|
|
9
|
+
src/test/patch_test.py
|
|
10
|
+
src/test/pd_test.py
|
|
11
|
+
src/test/ttest.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pickle_debug
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def f(x, y=1):
|
|
7
|
+
print(f"call {x} {y}")
|
|
8
|
+
time.sleep(1.5)
|
|
9
|
+
return x + y
|
|
10
|
+
|
|
11
|
+
class Slow:
|
|
12
|
+
|
|
13
|
+
def invoke(self,x):
|
|
14
|
+
time.sleep(1.5)
|
|
15
|
+
return x * x
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
pickle_debug.enable("/tmp/mycache")
|
|
19
|
+
|
|
20
|
+
f = pickle_debug.cached(f)
|
|
21
|
+
print(f(5,6))
|
|
22
|
+
print(f(5,6))
|
|
23
|
+
|
|
24
|
+
slow = Slow()
|
|
25
|
+
slow.invoke = pickle_debug.cached(slow.invoke)
|
|
26
|
+
print(slow.invoke(7))
|
|
27
|
+
print(slow.invoke(7))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import tempfile
|
|
3
|
+
|
|
4
|
+
from pickle_debug import cached, enable, reset
|
|
5
|
+
|
|
6
|
+
calls = []
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@cached
|
|
10
|
+
def f(x, y=1):
|
|
11
|
+
calls.append((x, y))
|
|
12
|
+
return [x, y]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
assert f(1) == [1, 1] and f(1) == [1, 1] and len(calls) == 2 # disabled: passthrough
|
|
16
|
+
d = pathlib.Path(tempfile.mkdtemp()) / 'cache'
|
|
17
|
+
enable(d)
|
|
18
|
+
assert d.is_dir()
|
|
19
|
+
calls.clear()
|
|
20
|
+
assert f(2, y=3) == [2, 3]
|
|
21
|
+
assert f(2, y=3) == [2, 3]
|
|
22
|
+
assert f(2) == [2, 1]
|
|
23
|
+
assert len(calls) == 2, calls
|
|
24
|
+
assert len(list(d.glob('*.pickle'))) == 2
|
|
25
|
+
(d / 'keep.txt').write_text('x')
|
|
26
|
+
try:
|
|
27
|
+
reset()
|
|
28
|
+
raise SystemExit('reset should refuse')
|
|
29
|
+
except RuntimeError as e:
|
|
30
|
+
print('reset refusal ok:', e)
|
|
31
|
+
assert (d / 'keep.txt').exists() and len(list(d.glob('*.pickle'))) == 2
|
|
32
|
+
(d / 'keep.txt').unlink()
|
|
33
|
+
reset()
|
|
34
|
+
assert not d.exists()
|
|
35
|
+
f(2, y=3)
|
|
36
|
+
assert len(calls) == 3
|
|
37
|
+
assert len(list(d.glob('*.pickle'))) == 1
|
|
38
|
+
try:
|
|
39
|
+
enable('/proc/nope')
|
|
40
|
+
raise SystemExit('should fail')
|
|
41
|
+
except OSError as e:
|
|
42
|
+
print('enable failure ok:', type(e).__name__)
|
|
43
|
+
print('ok')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pickle_debug
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pickle_debug.cached
|
|
7
|
+
def f(x, y=1):
|
|
8
|
+
print(f"call {x} {y}")
|
|
9
|
+
time.sleep(1.5)
|
|
10
|
+
return x + y
|
|
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
|
+
|