From be184a625943e098efd29bf6e1fe99baffc9cf7e Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 16 Sep 2026 18:24:45 +0200 Subject: [PATCH] fix(cache): distinguish callables in the shared EMS cache Include the function object in cache keys so methods of one interpolator cannot reuse a probability as a power value. Cover both call orders, keyword arguments, cache hits and separate closures with identical qualified names. --- src/akkudoktoreos/core/cache.py | 3 ++ tests/test_cache.py | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/akkudoktoreos/core/cache.py b/src/akkudoktoreos/core/cache.py index 4103e1b8..1ad3fc8e 100644 --- a/src/akkudoktoreos/core/cache.py +++ b/src/akkudoktoreos/core/cache.py @@ -228,6 +228,9 @@ def cache_energy_management( cached_wrapper = cachebox.cached( cache=CacheEnergyManagementStore().cache, + # This store is shared by all decorated callables, including methods + # on the same instance. Arguments alone cannot identify their results. + key_maker=lambda *args, **kwargs: cachebox.make_key(func, *args, **kwargs), callback=cache_energy_management_store_callback, )(wrapper) diff --git a/tests/test_cache.py b/tests/test_cache.py index 0680eaa8..1e91c0c6 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -88,6 +88,55 @@ class TestCacheUntilUpdateDecorators: assert CacheEnergyManagementStore.hit_count == 1 assert result1 == result2 + @pytest.mark.parametrize("reverse", [False, True]) + @pytest.mark.parametrize("use_kwargs", [False, True]) + def test_methods_with_identical_arguments_keep_separate_results( + self, cache_energy_management_store, reverse, use_kwargs + ): + calls = [] + + class Model: + @cache_energy_management + def fraction(self, value): + calls.append("fraction") + return value / 1000 + + @cache_energy_management + def energy(self, value): + calls.append("energy") + return value * 1000 + + model = Model() + cases = [(model.fraction, 0.005), (model.energy, 5000)] + if reverse: + cases.reverse() + for _ in range(2): + for method, expected in cases: + result = method(value=5) if use_kwargs else method(5) + assert result == expected + assert sorted(calls) == ["energy", "fraction"] + assert CacheEnergyManagementStore.miss_count == 2 + assert CacheEnergyManagementStore.hit_count == 2 + + def test_distinct_closures_with_same_name_do_not_share_results( + self, cache_energy_management_store + ): + def make_function(factor): + @cache_energy_management + def compute(value): + return value * factor + return compute + + double = make_function(2) + triple = make_function(3) + assert double.__qualname__ == triple.__qualname__ + assert double(4) == 8 + assert triple(4) == 12 + assert double(4) == 8 + assert triple(4) == 12 + assert CacheEnergyManagementStore.miss_count == 2 + assert CacheEnergyManagementStore.hit_count == 2 + def test_cache_energy_management(self, cache_energy_management_store): """Test that cache_energy_management caches function results."""