persistent-function-cache 0.2.1__tar.gz → 0.3.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.
Files changed (33) hide show
  1. {persistent_function_cache-0.2.1/src/persistent_function_cache.egg-info → persistent_function_cache-0.3.0}/PKG-INFO +10 -9
  2. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/README.md +1 -1
  3. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/pyproject.toml +14 -7
  4. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/cli/clear_cache.py +2 -2
  5. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/main/cache_slot.py +15 -3
  6. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/main/decorator.py +5 -1
  7. persistent_function_cache-0.3.0/src/persistent_cache/main/hashing.py +54 -0
  8. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/models/path.py +4 -4
  9. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/reducers/speedup_deep_learning.py +0 -4
  10. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0/src/persistent_function_cache.egg-info}/PKG-INFO +10 -9
  11. persistent_function_cache-0.3.0/src/persistent_function_cache.egg-info/requires.txt +9 -0
  12. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/tests/test_cache.py +11 -0
  13. persistent_function_cache-0.2.1/src/persistent_cache/main/hashing.py +0 -80
  14. persistent_function_cache-0.2.1/src/persistent_function_cache.egg-info/requires.txt +0 -9
  15. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/LICENSE +0 -0
  16. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/setup.cfg +0 -0
  17. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/__init__.py +0 -0
  18. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/caches/__init__.py +0 -0
  19. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/caches/deep_learning.py +0 -0
  20. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/caches/speedup_deep_learning.py +0 -0
  21. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/cli/__init__.py +0 -0
  22. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/main/__init__.py +0 -0
  23. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/models/__init__.py +0 -0
  24. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/py.typed +0 -0
  25. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/reducers/__init__.py +0 -0
  26. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/reducers/base.py +0 -0
  27. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_cache/reducers/deep_learning.py +0 -0
  28. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_function_cache.egg-info/SOURCES.txt +0 -0
  29. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_function_cache.egg-info/dependency_links.txt +0 -0
  30. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_function_cache.egg-info/entry_points.txt +0 -0
  31. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/src/persistent_function_cache.egg-info/top_level.txt +0 -0
  32. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/tests/test_clear_cache.py +0 -0
  33. {persistent_function_cache-0.2.1 → persistent_function_cache-0.3.0}/tests/test_deep_learning_cache.py +0 -0
@@ -1,26 +1,27 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: persistent-function-cache
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Persistent cache for expensive functions
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Source Code, https://github.com/quintenroets/persistent-cache
8
- Requires-Python: <3.13,>=3.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: package-utils<1,>=0.6.7
12
- Requires-Dist: powercli<1,>=0.3.2
13
- Requires-Dist: superpathlib<3,>=2.0.9
11
+ Requires-Dist: package-utils<1,>=0.8.5
12
+ Requires-Dist: powercli<1,>=0.4.0
13
+ Requires-Dist: superpathlib<3,>=2.0.13
14
14
  Provides-Extra: dev
15
- Requires-Dist: package-dev-tools<1,>=0.5.11; extra == "dev"
15
+ Requires-Dist: package-dev-tools<1,>=0.8.1; extra == "dev"
16
16
  Requires-Dist: package-dev-utils<1,>=0.1.6; extra == "dev"
17
17
  Requires-Dist: numpy<3,>=1.26.0; extra == "dev"
18
18
  Requires-Dist: torch<3,>=1.26.0; extra == "dev"
19
+ Dynamic: license-file
19
20
 
20
21
  # Cache
21
22
  [![PyPI version](https://badge.fury.io/py/persistent-function-cache.svg)](https://badge.fury.io/py/persistent-function-cache)
22
23
  ![PyPI downloads](https://img.shields.io/pypi/dm/persistent-function-cache)
23
- ![Python version](https://img.shields.io/badge/python-3.10--3.12-brightgreen)
24
+ ![Python version](https://img.shields.io/badge/python-3.11+-brightgreen)
24
25
  ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS%20%7c%20windows-brightgreen)
25
26
  ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
26
27
 
@@ -1,7 +1,7 @@
1
1
  # Cache
2
2
  [![PyPI version](https://badge.fury.io/py/persistent-function-cache.svg)](https://badge.fury.io/py/persistent-function-cache)
3
3
  ![PyPI downloads](https://img.shields.io/pypi/dm/persistent-function-cache)
4
- ![Python version](https://img.shields.io/badge/python-3.10--3.12-brightgreen)
4
+ ![Python version](https://img.shields.io/badge/python-3.11+-brightgreen)
5
5
  ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS%20%7c%20windows-brightgreen)
6
6
  ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
7
7
 
@@ -1,20 +1,20 @@
1
1
  [project]
2
2
  name = "persistent-function-cache"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Persistent cache for expensive functions"
5
5
  authors = [{name = "Quinten Roets", email = "qdr2104@columbia.edu"}]
6
- license = {text = "MIT"}
6
+ license = "MIT"
7
7
  readme = "README.md"
8
- requires-python = ">=3.10, <3.13"
8
+ requires-python = ">=3.11"
9
9
  dependencies = [
10
- "package-utils >=0.6.7, <1",
11
- "powercli >=0.3.2, <1",
12
- "superpathlib >=2.0.9, <3",
10
+ "package-utils >=0.8.5, <1",
11
+ "powercli >=0.4.0, <1",
12
+ "superpathlib >=2.0.13, <3",
13
13
  ]
14
14
 
15
15
  [project.optional-dependencies]
16
16
  dev = [
17
- "package-dev-tools >=0.5.11, <1",
17
+ "package-dev-tools >=0.8.1, <1",
18
18
  "package-dev-utils >=0.1.6, <1",
19
19
  "numpy >=1.26.0, <3",
20
20
  "torch >=1.26.0, <3",
@@ -53,7 +53,14 @@ fix = true
53
53
  select = ["ALL"]
54
54
  ignore = [
55
55
  "ANN401", # annotated with Any
56
+ "CPY001", # missing copyright notice
56
57
  "D", # docstrings
58
+ "D1", # missing docstrings
59
+ "D200", # one-line docstring
60
+ "D203", # conflicts with D211
61
+ "D205", # blank line between summary and description
62
+ "D212", # conflicts with D213
63
+ "D401", # imperative first line
57
64
  "G004", # logging f-string
58
65
  ]
59
66
 
@@ -1,6 +1,6 @@
1
1
  import time
2
2
  from dataclasses import dataclass
3
- from datetime import datetime, timezone
3
+ from datetime import UTC, datetime
4
4
  from functools import cached_property
5
5
  from typing import Annotated
6
6
 
@@ -43,7 +43,7 @@ def main(options: Options) -> None:
43
43
  if options.verbose:
44
44
  relative_path = path.relative_to(Options.cache_path)
45
45
  timestamp = datetime.fromtimestamp(path.mtime).astimezone(
46
- tz=timezone.utc,
46
+ tz=UTC,
47
47
  )
48
48
  message = f"{relative_path} ({timestamp})"
49
49
  cli.console.print(message)
@@ -3,6 +3,7 @@ import pickle
3
3
  from collections.abc import Callable, Iterable, Iterator
4
4
  from dataclasses import dataclass
5
5
  from functools import cached_property
6
+ from inspect import BoundArguments
6
7
  from typing import Any
7
8
 
8
9
  from persistent_cache.models import Path
@@ -18,6 +19,7 @@ class CacheSlot:
18
19
  kwargs: dict[str, Any]
19
20
  directory: Path
20
21
  key_arguments: Iterable[str] | str | None
22
+ argument_reducers: dict[str, Callable[[Any], Any]] | None
21
23
  extra_keys: Any
22
24
  key_reducer: type[Reducer] | None
23
25
  deep_learning: bool
@@ -60,28 +62,38 @@ class CacheSlot:
60
62
 
61
63
  @property
62
64
  def argument_values(self) -> Iterator[Any]:
63
- if self.key_arguments is None:
65
+ if self.key_arguments is None and self.argument_reducers is None:
64
66
  yield from self.args
65
67
  yield from self.kwargs.values()
66
68
  else:
67
69
  arguments = inspect.signature(self.function).bind(*self.args, **self.kwargs)
68
70
  arguments.apply_defaults()
71
+ yield from self.extract_argument_values(arguments)
72
+
73
+ def extract_argument_values(self, arguments: BoundArguments) -> Iterator[Any]:
74
+ if self.key_arguments is not None:
69
75
  if isinstance(self.key_arguments, str):
70
76
  yield arguments.arguments.get(self.key_arguments)
71
77
  else:
72
78
  for name in self.key_arguments:
73
79
  yield arguments.arguments.get(name)
80
+ if self.argument_reducers is not None:
81
+ for argument_name, reducer in self.argument_reducers.items():
82
+ argument = arguments.arguments.get(argument_name)
83
+ yield reducer(argument)
74
84
 
75
85
  @property
76
86
  def reducer(self) -> type[Reducer]:
77
87
  if self.key_reducer is not None:
78
88
  return self.key_reducer
79
89
  if self.deep_learning:
80
- from persistent_cache.reducers.deep_learning import Reducer as Reducer_
90
+ from persistent_cache.reducers.deep_learning import ( # noqa: PLC0415
91
+ Reducer as Reducer_,
92
+ )
81
93
 
82
94
  return Reducer_
83
95
  if self.speedup_deep_learning:
84
- from persistent_cache.reducers.speedup_deep_learning import (
96
+ from persistent_cache.reducers.speedup_deep_learning import ( # noqa: PLC0415
85
97
  Reducer as Reducer_,
86
98
  )
87
99
 
@@ -16,6 +16,7 @@ def cache(
16
16
  *,
17
17
  cache_directory: Path = Path.cache,
18
18
  cache_key_arguments: Iterable[str] | str | None = None,
19
+ argument_reducers: dict[str, Callable[[Any], Any]] | None = None,
19
20
  extra_cache_keys: Iterable[Any] | None = None,
20
21
  key_reducer: type[Reducer] = Reducer,
21
22
  deep_learning: bool = False,
@@ -29,6 +30,7 @@ def cache(
29
30
  *,
30
31
  cache_directory: Path = Path.cache,
31
32
  cache_key_arguments: Iterable[str] | str | None = None,
33
+ argument_reducers: dict[str, Callable[[Any], Any]] | None = None,
32
34
  extra_cache_keys: Any = None,
33
35
  key_reducer: type[Reducer] = Reducer,
34
36
  deep_learning: bool = False,
@@ -41,6 +43,7 @@ def cache( # noqa: PLR0913
41
43
  *,
42
44
  cache_directory: Path = Path.cache,
43
45
  cache_key_arguments: Iterable[str] | str | None = None,
46
+ argument_reducers: dict[str, Callable[[Any], Any]] | None = None,
44
47
  extra_cache_keys: Any = None,
45
48
  key_reducer: type[Reducer] | None = None,
46
49
  deep_learning: bool = False,
@@ -68,6 +71,7 @@ def cache( # noqa: PLR0913
68
71
  kwargs,
69
72
  cache_directory,
70
73
  cache_key_arguments,
74
+ argument_reducers,
71
75
  extra_cache_keys,
72
76
  key_reducer,
73
77
  deep_learning,
@@ -80,7 +84,7 @@ def cache( # noqa: PLR0913
80
84
  cache_slot.value = result
81
85
  return result
82
86
 
83
- return cast(F, wrapped_function)
87
+ return cast("F", wrapped_function)
84
88
 
85
89
  if function is not None:
86
90
  cache_decorator = cache_decorator(function)
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import inspect
5
+ import io
6
+ import pickle
7
+ from functools import cache
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from package_utils.annotations import first_parameter_types
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable, Iterator # pragma: nocover
14
+ from typing import BinaryIO # pragma: nocover
15
+
16
+ from persistent_cache.reducers.base import Reducer # pragma: nocover
17
+
18
+
19
+ def compute_hash(key_reducer: type[Reducer], items: Iterator[Any]) -> str:
20
+ with io.BytesIO() as fp:
21
+ HashPickler(fp, key_reducer).dump(tuple(items))
22
+ data = fp.getvalue()
23
+ # use fast hash function because it is not used for security
24
+ return hashlib.new("sha1", data=data, usedforsecurity=False).hexdigest()
25
+
26
+
27
+ class HashPickler(pickle.Pickler):
28
+ def __init__(self, file_pointer: BinaryIO, reducer: type[Reducer]) -> None:
29
+ super().__init__(file_pointer)
30
+ self.reducers = load_reducers(reducer) # type: ignore[arg-type]
31
+
32
+ def reducer_override(self, obj: Any) -> Any:
33
+ """The goal of this pickler is to create hashes of complex objects, not to
34
+ reconstruct complex objects.
35
+
36
+ So mapping does not need to be reversible.
37
+ """
38
+ reducer = next(self.determine_reducer(obj), None)
39
+ return NotImplemented if reducer is None else (tuple, (reducer(obj),))
40
+
41
+ def determine_reducer(self, obj: Any) -> Iterator[Callable[[Any], Any]]:
42
+ if obj is not tuple:
43
+ for obj_type, reducer in self.reducers.items():
44
+ if isinstance(obj, obj_type):
45
+ yield reducer
46
+
47
+
48
+ @cache
49
+ def load_reducers(reducer: type[Reducer]) -> dict[type, Callable[[Any], Any]]:
50
+ return {
51
+ parameter_type: method
52
+ for _, method in inspect.getmembers(reducer, predicate=inspect.ismethod)
53
+ for parameter_type in first_parameter_types(method)
54
+ }
@@ -1,4 +1,4 @@
1
- from typing import TypeVar, cast
1
+ from typing import Self, TypeVar, cast
2
2
 
3
3
  import superpathlib
4
4
  from simple_classproperty import classproperty
@@ -9,11 +9,11 @@ T = TypeVar("T", bound="Path")
9
9
  class Path(superpathlib.Path):
10
10
  @classmethod
11
11
  @classproperty
12
- def source_root(cls: type[T]) -> T:
12
+ def source_root(cls) -> Self:
13
13
  return cls(__file__).parent.parent
14
14
 
15
15
  @classmethod
16
16
  @classproperty
17
- def cache(cls: type[T]) -> T:
17
+ def cache(cls) -> Self:
18
18
  path = cls.script_assets / cls.source_root.name
19
- return cast(T, path)
19
+ return cast("Self", path)
@@ -57,7 +57,3 @@ class Reducer(deep_learning.Reducer):
57
57
  label = None
58
58
 
59
59
  return length, data, label
60
-
61
- @classmethod
62
- def reduce_tensor(cls, tensor: torch.Tensor) -> NDArray[Any]:
63
- return tensor.detach().cpu().numpy()
@@ -1,26 +1,27 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: persistent-function-cache
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Persistent cache for expensive functions
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Source Code, https://github.com/quintenroets/persistent-cache
8
- Requires-Python: <3.13,>=3.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: package-utils<1,>=0.6.7
12
- Requires-Dist: powercli<1,>=0.3.2
13
- Requires-Dist: superpathlib<3,>=2.0.9
11
+ Requires-Dist: package-utils<1,>=0.8.5
12
+ Requires-Dist: powercli<1,>=0.4.0
13
+ Requires-Dist: superpathlib<3,>=2.0.13
14
14
  Provides-Extra: dev
15
- Requires-Dist: package-dev-tools<1,>=0.5.11; extra == "dev"
15
+ Requires-Dist: package-dev-tools<1,>=0.8.1; extra == "dev"
16
16
  Requires-Dist: package-dev-utils<1,>=0.1.6; extra == "dev"
17
17
  Requires-Dist: numpy<3,>=1.26.0; extra == "dev"
18
18
  Requires-Dist: torch<3,>=1.26.0; extra == "dev"
19
+ Dynamic: license-file
19
20
 
20
21
  # Cache
21
22
  [![PyPI version](https://badge.fury.io/py/persistent-function-cache.svg)](https://badge.fury.io/py/persistent-function-cache)
22
23
  ![PyPI downloads](https://img.shields.io/pypi/dm/persistent-function-cache)
23
- ![Python version](https://img.shields.io/badge/python-3.10--3.12-brightgreen)
24
+ ![Python version](https://img.shields.io/badge/python-3.11+-brightgreen)
24
25
  ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS%20%7c%20windows-brightgreen)
25
26
  ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
26
27
 
@@ -0,0 +1,9 @@
1
+ package-utils<1,>=0.8.5
2
+ powercli<1,>=0.4.0
3
+ superpathlib<3,>=2.0.13
4
+
5
+ [dev]
6
+ package-dev-tools<1,>=0.8.1
7
+ package-dev-utils<1,>=0.1.6
8
+ numpy<3,>=1.26.0
9
+ torch<3,>=1.26.0
@@ -84,3 +84,14 @@ def test_cache_with_reducer(
84
84
  key_reducer=Reducer,
85
85
  )
86
86
  cached_function("test")
87
+
88
+
89
+ @pytest.mark.parametrize("cache_decorator", caches)
90
+ def test_cache_with_argument_reducer(
91
+ cache_decorator: Callable[..., Any],
92
+ ) -> None:
93
+ cached_function = cache_decorator(
94
+ calculate_with_name,
95
+ argument_reducers={"value": lambda x: x.strip()},
96
+ )
97
+ cached_function("test")
@@ -1,80 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
- import inspect
5
- import io
6
- import pickle
7
- from types import UnionType
8
- from typing import TYPE_CHECKING, Any, get_args, get_origin, get_type_hints
9
-
10
- from persistent_cache.reducers.base import Reducer
11
-
12
- if TYPE_CHECKING:
13
- from collections.abc import Callable, Iterator # pragma: nocover
14
- from typing import BinaryIO # pragma: nocover
15
-
16
-
17
- def extract_types(method: Callable[[Any], Any]) -> Iterator[type]:
18
- type_hints = get_type_hints(method).values()
19
- if type_hints:
20
- argument_type = next(iter(type_hints))
21
- origin = get_origin(argument_type)
22
- arguments = get_args(argument_type)
23
- if origin is UnionType:
24
- yield from arguments
25
- elif origin is not None:
26
- yield origin
27
- else:
28
- yield argument_type
29
-
30
-
31
- class HashPickler(pickle.Pickler):
32
- def __init__(
33
- self,
34
- file_pointer: BinaryIO,
35
- reducer: type[Reducer] = Reducer,
36
- ) -> None:
37
- super().__init__(file_pointer)
38
- self.reducer = reducer
39
- self.reducers = {}
40
- for _, method in inspect.getmembers(reducer, predicate=inspect.ismethod):
41
- argument_types = extract_types(method)
42
- for argument_type in argument_types:
43
- self.reducers[argument_type] = method
44
-
45
- def reducer_override(self, obj: Any) -> Any:
46
- """The goal of this pickler is to create hashes of complex objects, not to
47
- reconstruct complex objects.
48
-
49
- So mapping does not need to be reversible.
50
- """
51
- reducer = next(self.determine_reducer(obj), None)
52
- reduction: Any
53
- if reducer is None:
54
- reduction = NotImplemented
55
- else:
56
- mapping = reducer(obj)
57
- str_mapping = str(item_to_bytes(self.reducer, mapping))
58
- reduction = str, (str_mapping,)
59
- return reduction
60
-
61
- def determine_reducer(self, obj: Any) -> Iterator[Callable[[Any], Any]]:
62
- if obj is not str:
63
- for obj_type, reducer in self.reducers.items():
64
- if isinstance(obj, obj_type):
65
- yield reducer
66
-
67
-
68
- def compute_hash(key_reducer: type[Reducer], items: Iterator[Any]) -> str:
69
- data = item_to_bytes(key_reducer, tuple(items))
70
- # use fast hash function because it is not used for security
71
- return hashlib.new("sha1", data=data, usedforsecurity=False).hexdigest()
72
-
73
-
74
- def item_to_bytes(key_reducer: type[Reducer], item: Any) -> bytes:
75
- with io.BytesIO() as fp:
76
- # Use custom pickler to generate bytes from complex structures
77
- pickler = HashPickler(fp, key_reducer)
78
- pickler.dump(item)
79
- fp.seek(0)
80
- return fp.read()
@@ -1,9 +0,0 @@
1
- package-utils<1,>=0.6.7
2
- powercli<1,>=0.3.2
3
- superpathlib<3,>=2.0.9
4
-
5
- [dev]
6
- package-dev-tools<1,>=0.5.11
7
- package-dev-utils<1,>=0.1.6
8
- numpy<3,>=1.26.0
9
- torch<3,>=1.26.0