diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b14b9..96ed0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Changed + +- Remove the ``frozendict`` dependency, replacing it with a stdlib-only hashable ``dict`` subclass used to build ``lru_cache`` keys. This drops the last LGPL-3.0 dependency from einx without changing caching behaviour (https://github.com/fferflo/einx/pull/35). + ## [0.4.3] ### Fixed diff --git a/einx/_src/util/lru_cache.py b/einx/_src/util/lru_cache.py index 6a168d2..c38d7b0 100644 --- a/einx/_src/util/lru_cache.py +++ b/einx/_src/util/lru_cache.py @@ -5,7 +5,6 @@ from collections import defaultdict import numpy as np from functools import partial -import frozendict import types _thread_local = threading.local() @@ -14,13 +13,22 @@ max_cache_size = int(os.environ.get("EINX_CACHE_SIZE", -1)) +class _FrozenDict(dict): + # A hashable dict used to build lru_cache keys. It still behaves like a + # mapping, since the frozen value is read back via key access downstream, + # but is hashable. Hashing is order-independent so that two equal dicts + # always produce the same cache key. + def __hash__(self): + return hash(frozenset(self.items())) + + def _freeze_value(x): if isinstance(x, np.ndarray): return _freeze_value(x.tolist()) elif isinstance(x, list | tuple): return tuple(_freeze_value(x) for x in x) elif isinstance(x, dict): - return frozendict.frozendict({k: _freeze_value(v) for k, v in x.items()}) + return _FrozenDict((k, _freeze_value(v)) for k, v in x.items()) elif isinstance(x, types.SimpleNamespace): return _freeze_value(vars(x)) elif isinstance(x, inspect.Parameter): diff --git a/pyproject.toml b/pyproject.toml index 6a98206..523321d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ readme = "README.md" dependencies = [ "numpy", "sympy", - "frozendict", ] [project.optional-dependencies]