dependency-cache 0.1.0__tar.gz → 0.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.
Files changed (27) hide show
  1. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/.github/workflows/Test.yml +2 -0
  2. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/PKG-INFO +12 -4
  3. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/README.md +10 -2
  4. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/pyproject.toml +2 -2
  5. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/dependency_cache/dependency_cache.pyi +4 -0
  6. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/test_python.py +47 -10
  7. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/test_python_automagic.py +9 -9
  8. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/src/decorator.rs +1 -0
  9. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/src/decorator_factory.rs +37 -2
  10. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/src/dependency_cache_base.rs +55 -3
  11. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/uv.lock +1 -1
  12. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/.github/workflows/CI.yml +0 -0
  13. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/.gitignore +0 -0
  14. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/Cargo.lock +0 -0
  15. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/Cargo.toml +0 -0
  16. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/LICENSE +0 -0
  17. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/dependency_cache/__init__.py +0 -0
  18. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/dependency_cache/plot_dependency_graph.py +0 -0
  19. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/examples/automagic_dependency_example.py +0 -0
  20. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/examples/example.py +0 -0
  21. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/examples/function_calls_example.py +0 -0
  22. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/examples/inheritance_example.py +0 -0
  23. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/examples/manual_dependency_example.py +0 -0
  24. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/python/examples/use_cache_example.py +0 -0
  25. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/src/dependency_graph.rs +0 -0
  26. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/src/lib.rs +0 -0
  27. {dependency_cache-0.1.0 → dependency_cache-0.1.1}/src/normalise_method_args.rs +0 -0
@@ -5,6 +5,7 @@ on:
5
5
  branches: [ "master" ]
6
6
  pull_request:
7
7
  branches: [ "master" ]
8
+ types: [ opened, synchronize, reopened, ready_for_review ]
8
9
 
9
10
  env:
10
11
  CARGO_TERM_COLOR: always
@@ -12,6 +13,7 @@ env:
12
13
  jobs:
13
14
  build:
14
15
  runs-on: ubuntu-latest
16
+ if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
15
17
 
16
18
  steps:
17
19
  - uses: actions/checkout@v4
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dependency_cache
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
7
7
  Requires-Dist: matplotlib>=3.7.5
8
8
  Requires-Dist: networkx>=3.1
9
9
  License-File: LICENSE
10
- Summary: A caching system with a dependency graph for invalidation
10
+ Summary: A caching system for object methods that utilises a dependency graph to track relationships and trigger invalidation
11
11
  Keywords: caching,dependency-graph,invalidation
12
12
  License: MIT
13
13
  Requires-Python: >=3.8
@@ -24,13 +24,21 @@ A library for managing the caching of an object's methods with cache invalidatio
24
24
  Instead of recomputing expensive, deeply nested calculations on every call, Dependency Cache stores the result and only invalidates (and recalculates) it when one of its dependencies actually changes.
25
25
  This project is written in rust, with [pyo3](https://github.com/PyO3/pyo3), and compiled using [maturin](https://github.com/PyO3/maturin).
26
26
 
27
+ ### Installation
28
+
29
+ Install the latest stable version from [PyPI](https://pypi.org/project/dependency-cache/):
30
+
31
+ ```bash
32
+ pip install dependency-cache
33
+ ```
34
+
27
35
  ### API Overview
28
36
 
29
37
  | Export | What it's for |
30
38
  |---|---|
31
39
  | `DependencyCacheBase` | Base class to inherit from. Gives your instance a cache and a dependency graph. |
32
- | `dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=False)` | A footgun-enabled :gun: decorator for a method where you **explicitly declare** direct dependencies. |
33
- | `automagically_dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=True)` | A decorator which **automagically infers** the direct dependencies with the option to override manually using `dependencies=`. |
40
+ | `dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=False)` | A footgun-enabled decorator for a method where you **explicitly declare** all direct dependencies. |
41
+ | `automagically_dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=True)` | A decorator which **automagically infers** the direct dependencies. Optionally override the defaults and inference using the `dependencies` parameter. |
34
42
  | `plot_dependency_graph(obj, **kwargs)` | Visualizes an instance's dependency graph, for inspection/debugging. |
35
43
 
36
44
  ### Setup
@@ -6,13 +6,21 @@ A library for managing the caching of an object's methods with cache invalidatio
6
6
  Instead of recomputing expensive, deeply nested calculations on every call, Dependency Cache stores the result and only invalidates (and recalculates) it when one of its dependencies actually changes.
7
7
  This project is written in rust, with [pyo3](https://github.com/PyO3/pyo3), and compiled using [maturin](https://github.com/PyO3/maturin).
8
8
 
9
+ ### Installation
10
+
11
+ Install the latest stable version from [PyPI](https://pypi.org/project/dependency-cache/):
12
+
13
+ ```bash
14
+ pip install dependency-cache
15
+ ```
16
+
9
17
  ### API Overview
10
18
 
11
19
  | Export | What it's for |
12
20
  |---|---|
13
21
  | `DependencyCacheBase` | Base class to inherit from. Gives your instance a cache and a dependency graph. |
14
- | `dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=False)` | A footgun-enabled :gun: decorator for a method where you **explicitly declare** direct dependencies. |
15
- | `automagically_dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=True)` | A decorator which **automagically infers** the direct dependencies with the option to override manually using `dependencies=`. |
22
+ | `dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=False)` | A footgun-enabled decorator for a method where you **explicitly declare** all direct dependencies. |
23
+ | `automagically_dependency_cached(use_cache=True, dependencies=[...], track_runtime_dependencies=True)` | A decorator which **automagically infers** the direct dependencies. Optionally override the defaults and inference using the `dependencies` parameter. |
16
24
  | `plot_dependency_graph(obj, **kwargs)` | Visualizes an instance's dependency graph, for inspection/debugging. |
17
25
 
18
26
  ### Setup
@@ -11,8 +11,8 @@ classifiers = [
11
11
  "Programming Language :: Python :: Implementation :: PyPy",
12
12
  ]
13
13
  keywords = ["caching", "dependency-graph", "invalidation"]
14
- version = "0.1.0"
15
- description = "A caching system with a dependency graph for invalidation"
14
+ version = "0.1.1"
15
+ description = "A caching system for object methods that utilises a dependency graph to track relationships and trigger invalidation"
16
16
  readme = "README.md"
17
17
  license = { text = "MIT" }
18
18
  dependencies = [
@@ -3,11 +3,13 @@ from typing import Any, final
3
3
  class DependencyCacheBase:
4
4
  def __new__(cls, /, *_args, **_kwargs) -> DependencyCacheBase: ...
5
5
  def clear_cache(self, /) -> None: ...
6
+ def dump_cache(self, /) -> dict: ...
6
7
  def get_cached_value(self, /, method_name: str, **kwargs) -> Any | None: ...
7
8
  def get_cached_value_by_hash(self, /, hash: int) -> Any | None: ...
8
9
  def get_cached_values(self, /) -> dict: ...
9
10
  def get_dependency_graph(self, /) -> dict: ...
10
11
  def get_validation_state(self, /) -> dict: ...
12
+ def load_cache(self, /, data: dict) -> None: ...
11
13
  def update_cached_value(
12
14
  self, /, method_name: str, value: Any, **kwargs
13
15
  ) -> None: ...
@@ -21,6 +23,7 @@ class automagically_dependency_cached:
21
23
  use_cache: bool = True,
22
24
  dependencies: Any | None = None,
23
25
  track_runtime_dependencies: bool = True,
26
+ serialisable: bool = False,
24
27
  ) -> automagically_dependency_cached: ...
25
28
 
26
29
  @final
@@ -32,4 +35,5 @@ class dependency_cached:
32
35
  use_cache: bool = True,
33
36
  dependencies: Any | None = None,
34
37
  track_runtime_dependencies: bool = False,
38
+ serialisable: bool = False,
35
39
  ) -> dependency_cached: ...
@@ -11,15 +11,6 @@ class IncorrectInheritance:
11
11
  return 1
12
12
 
13
13
 
14
- class IncorrectMethod(DependencyCacheBase):
15
- def __init__(self):
16
- super().__init__()
17
-
18
- @dependency_cached()
19
- def A():
20
- return 1
21
-
22
-
23
14
  class SimpleTestObj(DependencyCacheBase):
24
15
  def __init__(self, x, y):
25
16
  super().__init__()
@@ -130,14 +121,41 @@ class FunctionCallObj(DependencyCacheBase):
130
121
  return total
131
122
 
132
123
 
124
+ class SerialisableObj(DependencyCacheBase):
125
+ @dependency_cached(serialisable=True)
126
+ def A(self):
127
+ return None
128
+
129
+ @dependency_cached(serialisable=True)
130
+ def B(self):
131
+ return None
132
+
133
+ @dependency_cached(dependencies=["A", "B"])
134
+ def C(self):
135
+ a = self.A()
136
+ b = self.B()
137
+ if a and b:
138
+ return a + b
139
+ return None
140
+
141
+
133
142
  def test_raises_typeerror():
134
- c = IncorrectInheritance()
135
143
  with pytest.raises(TypeError):
144
+ c = IncorrectInheritance()
136
145
  c.A()
137
146
 
138
147
 
139
148
  def test_incorrect_method_raises_valueerror():
140
149
  with pytest.raises(TypeError):
150
+
151
+ class IncorrectMethod(DependencyCacheBase):
152
+ def __init__(self):
153
+ super().__init__()
154
+
155
+ @dependency_cached()
156
+ def A():
157
+ return 1
158
+
141
159
  c = IncorrectMethod()
142
160
  c.A()
143
161
 
@@ -308,3 +326,22 @@ def test_incorrect_decorator_format():
308
326
  @dependency_cached()
309
327
  def B(self):
310
328
  return 2
329
+
330
+
331
+ @pytest.mark.parametrize(
332
+ ("data", "result"),
333
+ [
334
+ ({"A": 1, "B": 2}, 3),
335
+ ({"A": 1.45, "B": 2.55}, 4.0),
336
+ ({"A": "1", "B": "2"}, "12"),
337
+ ({"A": [1], "B": [2]}, [1, 2]),
338
+ ],
339
+ )
340
+ def test_serialising_data(data, result):
341
+ obj = SerialisableObj()
342
+
343
+ obj.load_cache(data)
344
+
345
+ assert obj.C() is not None
346
+
347
+ assert obj.C() == result
@@ -11,15 +11,6 @@ class IncorrectInheritance:
11
11
  return 1
12
12
 
13
13
 
14
- class IncorrectMethod(DependencyCacheBase):
15
- def __init__(self):
16
- super().__init__()
17
-
18
- @automagically_dependency_cached()
19
- def A():
20
- return 1
21
-
22
-
23
14
  class SimpleTestObj(DependencyCacheBase):
24
15
  def __init__(self, x, y):
25
16
  super().__init__()
@@ -138,6 +129,15 @@ def test_raises_typeerror():
138
129
 
139
130
  def test_incorrect_method_raises_valueerror():
140
131
  with pytest.raises(TypeError):
132
+
133
+ class IncorrectMethod(DependencyCacheBase):
134
+ def __init__(self):
135
+ super().__init__()
136
+
137
+ @automagically_dependency_cached()
138
+ def A():
139
+ return 1
140
+
141
141
  c = IncorrectMethod()
142
142
  c.A()
143
143
 
@@ -12,6 +12,7 @@ pub struct DependencyCacheDecorator {
12
12
  pub dependencies: Vec<isize>,
13
13
  pub method_name: String,
14
14
  pub track_runtime_dependencies: bool,
15
+ pub serialisable: bool,
15
16
  }
16
17
 
17
18
  #[pymethods]
@@ -6,6 +6,26 @@ use crate::decorator::DependencyCacheDecorator;
6
6
  use crate::normalise_method_args::normalise_and_hash_method;
7
7
 
8
8
 
9
+ pub fn validate_self_only_method(py: Python<'_>, func: &Bound<'_, PyAny>) -> PyResult<()> {
10
+ let inspect = py.import("inspect")?;
11
+ let builtins = py.import("builtins")?;
12
+ let signature = inspect.call_method1("signature", (func,))?;
13
+ // really ugly, but the inspect.Signature type is not extractable
14
+ let parameters = signature.getattr("parameters")?;
15
+ let list_obj = builtins.call_method1("list", (parameters,))?;
16
+ let param_names: Vec<String> = list_obj.extract()?;
17
+
18
+ if param_names.len() != 1 || param_names[0] != "self" {
19
+ let msg = format!(
20
+ "Serialisable method must have exactly one parameter named 'self', got: {:?}",
21
+ param_names
22
+ );
23
+ return Err(PyValueError::new_err(msg));
24
+ }
25
+ Ok(())
26
+ }
27
+
28
+
9
29
  fn parse_dependencies(py: Python<'_>, dep_list: &Bound<'_, PyAny>) -> PyResult<Vec<(String, Py<PyDict>)>> {
10
30
  let list = dep_list.cast::<PyList>()?;
11
31
  let mut parsed = Vec::with_capacity(list.len());
@@ -40,17 +60,20 @@ pub struct ManualDependencyCacheDecoratorFactory {
40
60
  use_cache: bool,
41
61
  dependencies: Vec<(String, Py<PyDict>)>,
42
62
  track_runtime_dependencies: bool,
63
+ serialisable: bool,
43
64
  }
44
65
 
45
66
  #[pymethods]
46
67
  impl ManualDependencyCacheDecoratorFactory {
68
+
47
69
  #[new]
48
- #[pyo3(signature = (use_cache=true, dependencies=None, track_runtime_dependencies=false))]
70
+ #[pyo3(signature = (use_cache=true, dependencies=None, track_runtime_dependencies=false, serialisable=false))]
49
71
  fn new(
50
72
  py: Python<'_>,
51
73
  use_cache: bool,
52
74
  dependencies: Option<&Bound<'_, PyAny>>,
53
75
  track_runtime_dependencies: bool,
76
+ serialisable: bool,
54
77
  ) -> PyResult<Self> {
55
78
  let parsed_deps = if let Some(list) = dependencies {
56
79
  parse_dependencies(py, list)?
@@ -61,6 +84,7 @@ impl ManualDependencyCacheDecoratorFactory {
61
84
  use_cache,
62
85
  dependencies: parsed_deps,
63
86
  track_runtime_dependencies,
87
+ serialisable,
64
88
  });
65
89
  }
66
90
 
@@ -81,12 +105,16 @@ impl ManualDependencyCacheDecoratorFactory {
81
105
  Ok(hash)
82
106
  })
83
107
  .collect::<PyResult<Vec<_>>>()?;
108
+ if self.serialisable {
109
+ validate_self_only_method(py, func.bind(py))?;
110
+ }
84
111
  return Ok(DependencyCacheDecorator {
85
112
  func,
86
113
  use_cache: self.use_cache,
87
114
  dependencies: hashed_dependencies,
88
115
  method_name,
89
116
  track_runtime_dependencies: self.track_runtime_dependencies,
117
+ serialisable: self.serialisable,
90
118
  });
91
119
  }
92
120
  }
@@ -96,17 +124,19 @@ pub struct AutomagicDependencyCacheDecoratorFactory {
96
124
  use_cache: bool,
97
125
  dependencies: Vec<(String, Py<PyDict>)>,
98
126
  track_runtime_dependencies: bool,
127
+ serialisable: bool,
99
128
  }
100
129
 
101
130
  #[pymethods]
102
131
  impl AutomagicDependencyCacheDecoratorFactory {
103
132
  #[new]
104
- #[pyo3(signature = (use_cache=true, dependencies=None, track_runtime_dependencies=true))]
133
+ #[pyo3(signature = (use_cache=true, dependencies=None, track_runtime_dependencies=true, serialisable=false))]
105
134
  fn new(
106
135
  py: Python<'_>,
107
136
  use_cache: bool,
108
137
  dependencies: Option<&Bound<'_, PyAny>>,
109
138
  track_runtime_dependencies: bool,
139
+ serialisable: bool,
110
140
  ) -> PyResult<Self> {
111
141
  let parsed_deps = if let Some(list) = dependencies {
112
142
  parse_dependencies(py, list)?
@@ -117,6 +147,7 @@ impl AutomagicDependencyCacheDecoratorFactory {
117
147
  use_cache,
118
148
  dependencies: parsed_deps,
119
149
  track_runtime_dependencies,
150
+ serialisable,
120
151
  });
121
152
  }
122
153
 
@@ -137,12 +168,16 @@ impl AutomagicDependencyCacheDecoratorFactory {
137
168
  Ok(hash)
138
169
  })
139
170
  .collect::<PyResult<Vec<_>>>()?;
171
+ if self.serialisable {
172
+ validate_self_only_method(py, func.bind(py))?;
173
+ }
140
174
  return Ok(DependencyCacheDecorator {
141
175
  func,
142
176
  use_cache: self.use_cache,
143
177
  dependencies: hashed_dependencies,
144
178
  method_name,
145
179
  track_runtime_dependencies: self.track_runtime_dependencies,
180
+ serialisable: self.serialisable,
146
181
  });
147
182
  }
148
183
  }
@@ -1,11 +1,12 @@
1
1
  use pyo3::prelude::*;
2
- use pyo3::types::{PyDict, PyTuple, PyString, PySet};
2
+ use pyo3::types::{PyDict, PyTuple, PyString, PySet, PyType};
3
+ use pyo3::exceptions::PyValueError;
3
4
 
4
- use std::collections::{HashMap};
5
+ use std::collections::{HashMap, HashSet};
5
6
 
6
7
  use crate::dependency_graph::{MethodDependencyGraph, ValidationState};
7
8
  use crate::normalise_method_args::normalise_and_hash_method;
8
-
9
+ use crate::decorator::DependencyCacheDecorator;
9
10
 
10
11
  #[pyclass(subclass)]
11
12
  pub struct DependencyCacheBase {
@@ -269,4 +270,55 @@ impl DependencyCacheBase {
269
270
  }
270
271
  Ok(dict)
271
272
  }
273
+
274
+ pub fn dump_cache<'py>(slf: &Bound<'py, Self>, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
275
+ let cls = slf.get_type();
276
+
277
+ let mut visited = HashSet::new();
278
+ let mut serialisable_methods = Vec::new();
279
+
280
+ for mro_class in cls.mro().iter() {
281
+ let mro_class: Bound<'_, PyType> = mro_class.extract()?;
282
+ let namespace = mro_class.getattr("__dict__")?;
283
+ for item in namespace.call_method0("items")?.try_iter()? {
284
+ let (name, value): (String, Bound<'_, PyAny>) = item?.extract()?;
285
+ if name.starts_with("__") || !visited.insert(name.clone()) {
286
+ continue;
287
+ }
288
+ if let Ok(decorator) = value.cast::<DependencyCacheDecorator>() {
289
+ if decorator.borrow().serialisable {
290
+ serialisable_methods.push(name);
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ let result = PyDict::new(py);
297
+ for method_name in serialisable_methods {
298
+ let value = slf.call_method(&method_name, (), None)?;
299
+ result.set_item(method_name, value)?;
300
+ }
301
+
302
+ Ok(result)
303
+ }
304
+
305
+ pub fn load_cache<'py>(
306
+ slf: &Bound<'_, Self>,
307
+ py: Python<'py>,
308
+ data: Bound<'py, PyDict>,
309
+ ) -> PyResult<()> {
310
+ for (key, loaded_value) in data.iter() {
311
+ let method_name: String = key.extract()?;
312
+
313
+ let _ = slf.call_method(&method_name, (), None)?;
314
+
315
+ let empty_args = PyTuple::empty(py);
316
+ let (hash, _) = normalise_and_hash_method(py, &method_name, None, &empty_args, None)
317
+ .map_err(|e| PyValueError::new_err(format!("Failed to hash '{}': {}", method_name, e)))?;
318
+
319
+ slf.borrow_mut().cache.insert(hash, loaded_value.unbind());
320
+ }
321
+
322
+ Ok(())
323
+ }
272
324
  }
@@ -329,7 +329,7 @@ wheels = [
329
329
 
330
330
  [[package]]
331
331
  name = "dependency-cache"
332
- version = "0.1.0"
332
+ version = "0.1.1"
333
333
  source = { editable = "." }
334
334
  dependencies = [
335
335
  { name = "matplotlib", version = "3.7.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },