From 8fa303f73de1884c55d8937fab12f8309f8d0d69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 07:53:54 +0000 Subject: [PATCH 01/14] Initial plan From b46d5f8e6810b8cb3710482d764bb2251b7ec6b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:12:07 +0000 Subject: [PATCH 02/14] Fix: Handle variadic arguments (*args, **kwargs) in cache key generation - Modified _convert_args_kwargs() to properly handle VAR_POSITIONAL and VAR_KEYWORD parameters - Variadic positional args are now expanded as individual entries with __varargs_N__ keys - Variadic keyword args are stored with their original keys - Added comprehensive test suite for variadic arguments - All existing tests pass including custom hash function tests Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- src/cachier/core.py | 74 ++++++++++++--- tests/test_varargs.py | 205 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 tests/test_varargs.py diff --git a/src/cachier/core.py b/src/cachier/core.py index e999feaf..b3a19269 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -79,20 +79,74 @@ def _convert_args_kwargs( args = func.args + args kwds.update({k: v for k, v in func.keywords.items() if k not in kwds}) func = func.func - func_params = list(inspect.signature(func).parameters) - args_as_kw = dict( - zip(func_params[1:], args[1:]) - if _is_method - else zip(func_params, args) - ) - # init with default values + + sig = inspect.signature(func) + func_params = list(sig.parameters) + + # Separate regular parameters from VAR_POSITIONAL and VAR_KEYWORD + regular_params = [] + var_positional_name = None + var_keyword_name = None + + for param_name in func_params: + param = sig.parameters[param_name] + if param.kind == inspect.Parameter.VAR_POSITIONAL: + var_positional_name = param_name + elif param.kind == inspect.Parameter.VAR_KEYWORD: + var_keyword_name = param_name + else: + regular_params.append(param_name) + + # Map positional arguments to regular parameters + if _is_method: + # Skip 'self' for methods + args_to_map = args[1:] + params_to_use = regular_params[1:] + else: + args_to_map = args + params_to_use = regular_params + + # Map as many args as possible to regular parameters + num_regular = len(params_to_use) + args_as_kw = dict(zip(params_to_use, args_to_map[:num_regular])) + + # Handle variadic positional arguments + # Store them with indexed keys like __varargs_0__, __varargs_1__, etc. + if var_positional_name and len(args_to_map) > num_regular: + var_args = args_to_map[num_regular:] + for i, arg in enumerate(var_args): + args_as_kw[f"__varargs_{i}__"] = arg + + # Init with default values kwargs = { k: v.default - for k, v in inspect.signature(func).parameters.items() + for k, v in sig.parameters.items() if v.default is not inspect.Parameter.empty } - # merge args expanded as kwargs and the original kwds - kwargs.update(dict(**args_as_kw, **kwds)) + + # Merge args expanded as kwargs and the original kwds + kwargs.update(args_as_kw) + + # Handle variadic keyword arguments + if var_keyword_name: + # Separate kwds that match known parameters from those that don't + known_param_kwds = {} + extra_kwds = {} + for k, v in kwds.items(): + if k in sig.parameters: + param = sig.parameters[k] + if param.kind != inspect.Parameter.VAR_KEYWORD: + known_param_kwds[k] = v + else: + extra_kwds[k] = v + else: + extra_kwds[k] = v + kwargs.update(known_param_kwds) + # Store extra kwargs with their original keys + kwargs.update(extra_kwds) + else: + kwargs.update(kwds) + return OrderedDict(sorted(kwargs.items())) diff --git a/tests/test_varargs.py b/tests/test_varargs.py new file mode 100644 index 00000000..966e6ef1 --- /dev/null +++ b/tests/test_varargs.py @@ -0,0 +1,205 @@ +"""Test for variadic arguments (*args) handling in cachier.""" + +from datetime import timedelta + +import pytest + +from cachier import cachier + + +@pytest.mark.pickle +def test_varargs_different_cache_keys(): + """Test that functions with *args get unique cache keys for different arguments.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(*args): + """Test function that accepts variadic arguments.""" + nonlocal call_count + call_count += 1 + return f"Result for args: {args}, call #{call_count}" + + # Clear any existing cache + get_data.clear_cache() + call_count = 0 + + # Test 1: Call with different arguments should produce different cache entries + result1 = get_data("print", "domains") + assert call_count == 1 + assert "('print', 'domains')" in result1 + + result2 = get_data("print", "users", "allfields") + assert call_count == 2, "Function should be called again with different args" + assert "('print', 'users', 'allfields')" in result2 + assert result1 != result2, "Different args should produce different results" + + # Test 2: Calling with the same arguments should use cache + result3 = get_data("print", "domains") + assert call_count == 2, "Function should not be called again (cache hit)" + assert result3 == result1 + + result4 = get_data("print", "users", "allfields") + assert call_count == 2, "Function should not be called again (cache hit)" + assert result4 == result2 + + get_data.clear_cache() + + +@pytest.mark.pickle +def test_varargs_empty(): + """Test that functions with *args work with no arguments.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(*args): + """Test function that accepts variadic arguments.""" + nonlocal call_count + call_count += 1 + return f"Result for args: {args}, call #{call_count}" + + get_data.clear_cache() + call_count = 0 + + # Call with no arguments + result1 = get_data() + assert call_count == 1 + assert "()" in result1 + + # Second call should use cache + result2 = get_data() + assert call_count == 1 + assert result2 == result1 + + get_data.clear_cache() + + +@pytest.mark.pickle +def test_varargs_with_regular_args(): + """Test that functions with both regular and variadic arguments work correctly.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(command, *args): + """Test function with regular and variadic arguments.""" + nonlocal call_count + call_count += 1 + return f"Command: {command}, args: {args}, call #{call_count}" + + get_data.clear_cache() + call_count = 0 + + # Test different calls + result1 = get_data("print", "domains") + assert call_count == 1 + + result2 = get_data("print", "users", "allfields") + assert call_count == 2 + assert result1 != result2 + + result3 = get_data("list") + assert call_count == 3 + assert result3 != result1 + assert result3 != result2 + + # Test cache hits + result4 = get_data("print", "domains") + assert call_count == 3 + assert result4 == result1 + + get_data.clear_cache() + + +@pytest.mark.pickle +def test_varkwargs_different_cache_keys(): + """Test that functions with **kwargs get unique cache keys for different arguments.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(**kwargs): + """Test function that accepts keyword variadic arguments.""" + nonlocal call_count + call_count += 1 + return f"Result for kwargs: {kwargs}, call #{call_count}" + + get_data.clear_cache() + call_count = 0 + + # Test with different kwargs + result1 = get_data(type="domains", action="print") + assert call_count == 1 + + result2 = get_data(type="users", action="print", fields="allfields") + assert call_count == 2 + assert result1 != result2 + + # Test cache hits + result3 = get_data(type="domains", action="print") + assert call_count == 2 + assert result3 == result1 + + get_data.clear_cache() + + +@pytest.mark.pickle +def test_varargs_and_varkwargs(): + """Test that functions with both *args and **kwargs work correctly.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(*args, **kwargs): + """Test function with both variadic arguments.""" + nonlocal call_count + call_count += 1 + return f"args: {args}, kwargs: {kwargs}, call #{call_count}" + + get_data.clear_cache() + call_count = 0 + + # Test different combinations + result1 = get_data("print", "domains") + assert call_count == 1 + + result2 = get_data("print", "users", action="list") + assert call_count == 2 + assert result1 != result2 + + result3 = get_data(action="list", resource="domains") + assert call_count == 3 + assert result3 != result1 + assert result3 != result2 + + # Test cache hits + result4 = get_data("print", "domains") + assert call_count == 3 + assert result4 == result1 + + get_data.clear_cache() + + +@pytest.mark.memory +def test_varargs_memory_backend(): + """Test that variadic arguments work with memory backend.""" + call_count = 0 + + @cachier(backend="memory", stale_after=timedelta(seconds=500)) + def get_data(*args): + """Test function that accepts variadic arguments.""" + nonlocal call_count + call_count += 1 + return f"Result: {args}, call #{call_count}" + + get_data.clear_cache() + call_count = 0 + + result1 = get_data("a", "b") + assert call_count == 1 + + result2 = get_data("a", "b", "c") + assert call_count == 2 + assert result1 != result2 + + result3 = get_data("a", "b") + assert call_count == 2 + assert result3 == result1 + + get_data.clear_cache() From 08b9287780aac0556ce93125a6352767aa5f72b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:24:23 +0000 Subject: [PATCH 03/14] Address PR review feedback: keyword-only params, parametrize tests, simplify code - Fixed incorrect handling of keyword-only parameters (KEYWORD_ONLY should not be in regular_params) - Simplified VAR_KEYWORD handling by removing unreachable else branch - Parametrized first test to run on both pickle and memory backends - Added 3 new tests for keyword-only parameter edge cases - Fixed line length issues in docstrings and comments Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- src/cachier/core.py | 21 +++---- tests/test_varargs.py | 130 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/cachier/core.py b/src/cachier/core.py index b3a19269..ed15b62b 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -94,7 +94,10 @@ def _convert_args_kwargs( var_positional_name = param_name elif param.kind == inspect.Parameter.VAR_KEYWORD: var_keyword_name = param_name - else: + elif param.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ): regular_params.append(param_name) # Map positional arguments to regular parameters @@ -127,23 +130,15 @@ def _convert_args_kwargs( # Merge args expanded as kwargs and the original kwds kwargs.update(args_as_kw) - # Handle variadic keyword arguments + # Handle variadic keyword arguments and keyword-only parameters if var_keyword_name: # Separate kwds that match known parameters from those that don't - known_param_kwds = {} - extra_kwds = {} for k, v in kwds.items(): if k in sig.parameters: - param = sig.parameters[k] - if param.kind != inspect.Parameter.VAR_KEYWORD: - known_param_kwds[k] = v - else: - extra_kwds[k] = v + kwargs[k] = v else: - extra_kwds[k] = v - kwargs.update(known_param_kwds) - # Store extra kwargs with their original keys - kwargs.update(extra_kwds) + # Extra kwargs go directly into the result dict + kwargs[k] = v else: kwargs.update(kwds) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index 966e6ef1..dd705cd8 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -8,11 +8,12 @@ @pytest.mark.pickle -def test_varargs_different_cache_keys(): - """Test that functions with *args get unique cache keys for different arguments.""" +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +def test_varargs_different_cache_keys(backend): + """Test *args get unique cache keys for different arguments.""" call_count = 0 - @cachier(stale_after=timedelta(seconds=500)) + @cachier(backend=backend, stale_after=timedelta(seconds=500)) def get_data(*args): """Test function that accepts variadic arguments.""" nonlocal call_count @@ -23,15 +24,20 @@ def get_data(*args): get_data.clear_cache() call_count = 0 - # Test 1: Call with different arguments should produce different cache entries + # Test 1: Call with different arguments should produce different + # cache entries result1 = get_data("print", "domains") assert call_count == 1 assert "('print', 'domains')" in result1 result2 = get_data("print", "users", "allfields") - assert call_count == 2, "Function should be called again with different args" + assert call_count == 2, ( + "Function should be called again with different args" + ) assert "('print', 'users', 'allfields')" in result2 - assert result1 != result2, "Different args should produce different results" + assert result1 != result2, ( + "Different args should produce different results" + ) # Test 2: Calling with the same arguments should use cache result3 = get_data("print", "domains") @@ -75,7 +81,7 @@ def get_data(*args): @pytest.mark.pickle def test_varargs_with_regular_args(): - """Test that functions with both regular and variadic arguments work correctly.""" + """Test regular and variadic arguments work correctly.""" call_count = 0 @cachier(stale_after=timedelta(seconds=500)) @@ -111,7 +117,7 @@ def get_data(command, *args): @pytest.mark.pickle def test_varkwargs_different_cache_keys(): - """Test that functions with **kwargs get unique cache keys for different arguments.""" + """Test **kwargs get unique cache keys for different arguments.""" call_count = 0 @cachier(stale_after=timedelta(seconds=500)) @@ -203,3 +209,111 @@ def get_data(*args): assert result3 == result1 get_data.clear_cache() + + +@pytest.mark.pickle +def test_keyword_only_parameters(): + """Test that functions with keyword-only parameters work correctly.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(*args, kw_only): + """Test function with keyword-only parameter.""" + nonlocal call_count + call_count += 1 + return f"args={args}, kw_only={kw_only}, call #{call_count}" + + get_data.clear_cache() + call_count = 0 + + # Test with different keyword-only values + result1 = get_data("a", "b", kw_only="value1") + assert call_count == 1 + + result2 = get_data("a", "b", kw_only="value2") + assert call_count == 2 + assert result1 != result2 + + # Test cache hit + result3 = get_data("a", "b", kw_only="value1") + assert call_count == 2 + assert result3 == result1 + + get_data.clear_cache() + + +@pytest.mark.pickle +def test_keyword_only_with_default(): + """Test keyword-only parameters with defaults work correctly.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(*args, kw_only="default"): + """Test function with keyword-only parameter with default.""" + nonlocal call_count + call_count += 1 + return f"args={args}, kw_only={kw_only}, call #{call_count}" + + get_data.clear_cache() + call_count = 0 + + # Test with default value + result1 = get_data("a", "b") + assert call_count == 1 + assert "kw_only=default" in result1 + + # Test with explicit value + result2 = get_data("a", "b", kw_only="explicit") + assert call_count == 2 + assert result1 != result2 + + # Test cache hit with default + result3 = get_data("a", "b") + assert call_count == 2 + assert result3 == result1 + + # Test cache hit with explicit value + result4 = get_data("a", "b", kw_only="explicit") + assert call_count == 2 + assert result4 == result2 + + get_data.clear_cache() + + +@pytest.mark.pickle +def test_mixed_varargs_keyword_only(): + """Test *args and keyword-only parameters work correctly.""" + call_count = 0 + + @cachier(stale_after=timedelta(seconds=500)) + def get_data(regular, *args, kw_only, kw_with_default="default"): + """Test function with mixed parameter types.""" + nonlocal call_count + call_count += 1 + return ( + f"regular={regular}, args={args}, kw_only={kw_only}, " + f"kw_with_default={kw_with_default}, call #{call_count}" + ) + + get_data.clear_cache() + call_count = 0 + + # Test different combinations + result1 = get_data("r1", "a", "b", kw_only="k1") + assert call_count == 1 + + result2 = get_data("r1", "a", "b", kw_only="k2") + assert call_count == 2 + assert result1 != result2 + + result3 = get_data("r1", "a", "b", kw_only="k1", kw_with_default="custom") + assert call_count == 3 + assert result3 != result1 + + # Test cache hits + result4 = get_data("r1", "a", "b", kw_only="k1") + assert call_count == 3 + assert result4 == result1 + + get_data.clear_cache() + From f85478bf5eaf81c59b9f8f72849d42ac6046574c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:27:30 +0000 Subject: [PATCH 04/14] Improve test assertions to explicitly verify cache hits - Changed assertions to capture previous call_count before testing cache hits - Makes tests more explicit: verify count didn't increase rather than re-asserting same value - Applied to test_varargs_different_cache_keys, test_varargs_empty, test_varkwargs_different_cache_keys, and test_varargs_and_varkwargs Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- tests/test_varargs.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index dd705cd8..53fcd5ea 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -40,12 +40,17 @@ def get_data(*args): ) # Test 2: Calling with the same arguments should use cache + previous_call_count = call_count result3 = get_data("print", "domains") - assert call_count == 2, "Function should not be called again (cache hit)" + assert call_count == previous_call_count, ( + "Function should not be called again (cache hit)" + ) assert result3 == result1 result4 = get_data("print", "users", "allfields") - assert call_count == 2, "Function should not be called again (cache hit)" + assert call_count == previous_call_count, ( + "Function should not be called again (cache hit)" + ) assert result4 == result2 get_data.clear_cache() @@ -72,8 +77,9 @@ def get_data(*args): assert "()" in result1 # Second call should use cache + previous_call_count = call_count result2 = get_data() - assert call_count == 1 + assert call_count == previous_call_count assert result2 == result1 get_data.clear_cache() @@ -108,8 +114,9 @@ def get_data(command, *args): assert result3 != result2 # Test cache hits + previous_call_count = call_count result4 = get_data("print", "domains") - assert call_count == 3 + assert call_count == previous_call_count assert result4 == result1 get_data.clear_cache() @@ -139,8 +146,9 @@ def get_data(**kwargs): assert result1 != result2 # Test cache hits + previous_call_count = call_count result3 = get_data(type="domains", action="print") - assert call_count == 2 + assert call_count == previous_call_count assert result3 == result1 get_data.clear_cache() From 93cb73a53c89db67d94dcde330c2ad090702e1c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:31:39 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_varargs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index 53fcd5ea..2f2d746a 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -324,4 +324,3 @@ def get_data(regular, *args, kw_only, kw_with_default="default"): assert result4 == result1 get_data.clear_cache() - From 02da21dcdcdbd1a455789b023e76a041bae12427 Mon Sep 17 00:00:00 2001 From: jirka <6035284+Borda@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:32:48 +0100 Subject: [PATCH 06/14] Simplify parameter handling in `map_arguments` --- src/cachier/core.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/cachier/core.py b/src/cachier/core.py index b3a19269..1e679e70 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -94,7 +94,7 @@ def _convert_args_kwargs( var_positional_name = param_name elif param.kind == inspect.Parameter.VAR_KEYWORD: var_keyword_name = param_name - else: + elif param.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD): regular_params.append(param_name) # Map positional arguments to regular parameters @@ -134,11 +134,7 @@ def _convert_args_kwargs( extra_kwds = {} for k, v in kwds.items(): if k in sig.parameters: - param = sig.parameters[k] - if param.kind != inspect.Parameter.VAR_KEYWORD: - known_param_kwds[k] = v - else: - extra_kwds[k] = v + known_param_kwds[k] = v else: extra_kwds[k] = v kwargs.update(known_param_kwds) From d708f775c1127f2558ada80c7b67c8bd6326f3e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:32:56 +0000 Subject: [PATCH 07/14] Fix test assertion in test_varargs_memory_backend - Changed assertion to capture previous_call_count before cache hit test - Makes test explicit about verifying the count didn't increase - Consistent with other test improvements in previous commit Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- tests/test_varargs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index 2f2d746a..4e23b2e8 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -212,8 +212,9 @@ def get_data(*args): assert call_count == 2 assert result1 != result2 + previous_call_count = call_count result3 = get_data("a", "b") - assert call_count == 2 + assert call_count == previous_call_count assert result3 == result1 get_data.clear_cache() From 4b09f8b5eba7e007db9cf10292caf1b45e5fa742 Mon Sep 17 00:00:00 2001 From: jirka <6035284+Borda@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:40:06 +0100 Subject: [PATCH 08/14] Refactor and parametrize varargs/varkwargs test cases for enhanced readability and backend coverage --- tests/test_varargs.py | 649 ++++++++++++++++++++++++------------------ 1 file changed, 374 insertions(+), 275 deletions(-) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index 4e23b2e8..88f38685 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -9,319 +9,418 @@ @pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) -def test_varargs_different_cache_keys(backend): +class TestVarargsDifferentCacheKeys: """Test *args get unique cache keys for different arguments.""" - call_count = 0 - - @cachier(backend=backend, stale_after=timedelta(seconds=500)) - def get_data(*args): - """Test function that accepts variadic arguments.""" - nonlocal call_count - call_count += 1 - return f"Result for args: {args}, call #{call_count}" - - # Clear any existing cache - get_data.clear_cache() - call_count = 0 - - # Test 1: Call with different arguments should produce different - # cache entries - result1 = get_data("print", "domains") - assert call_count == 1 - assert "('print', 'domains')" in result1 - - result2 = get_data("print", "users", "allfields") - assert call_count == 2, ( - "Function should be called again with different args" - ) - assert "('print', 'users', 'allfields')" in result2 - assert result1 != result2, ( - "Different args should produce different results" - ) - - # Test 2: Calling with the same arguments should use cache - previous_call_count = call_count - result3 = get_data("print", "domains") - assert call_count == previous_call_count, ( - "Function should not be called again (cache hit)" - ) - assert result3 == result1 - - result4 = get_data("print", "users", "allfields") - assert call_count == previous_call_count, ( - "Function should not be called again (cache hit)" - ) - assert result4 == result2 - - get_data.clear_cache() + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + self.backend = backend + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(*args): + """Test function that accepts variadic arguments.""" + self.call_count += 1 + return f"Result for args: {args}, call #{self.call_count}" + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_different_args_produce_different_results(self): + """Test that different arguments produce different cache entries.""" + result1 = self.get_data("print", "domains") + assert self.call_count == 1 + assert "('print', 'domains')" in result1 + + result2 = self.get_data("print", "users", "allfields") + assert self.call_count == 2, ( + "Function should be called again with different args" + ) + assert "('print', 'users', 'allfields')" in result2 + assert result1 != result2, ( + "Different args should produce different results" + ) -@pytest.mark.pickle -def test_varargs_empty(): - """Test that functions with *args work with no arguments.""" - call_count = 0 - - @cachier(stale_after=timedelta(seconds=500)) - def get_data(*args): - """Test function that accepts variadic arguments.""" - nonlocal call_count - call_count += 1 - return f"Result for args: {args}, call #{call_count}" - - get_data.clear_cache() - call_count = 0 + def test_same_args_use_cache(self): + """Test that calling with same arguments uses cache.""" + # First call + result1 = self.get_data("print", "domains") + assert self.call_count == 1 + + # Second call with same args should use cache + previous_call_count = self.call_count + result3 = self.get_data("print", "domains") + assert self.call_count == previous_call_count, ( + "Function should not be called again (cache hit)" + ) + assert result3 == result1 + + def test_another_set_of_args_cache_hit(self): + """Test cache hit for another set of arguments.""" + # First call + result2 = self.get_data("print", "users", "allfields") + assert self.call_count == 1 + + # Second call with same args should use cache + previous_call_count = self.call_count + result4 = self.get_data("print", "users", "allfields") + assert self.call_count == previous_call_count, ( + "Function should not be called again (cache hit)" + ) + assert result4 == result2 - # Call with no arguments - result1 = get_data() - assert call_count == 1 - assert "()" in result1 - # Second call should use cache - previous_call_count = call_count - result2 = get_data() - assert call_count == previous_call_count - assert result2 == result1 +@pytest.mark.pickle +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestVarargsEmpty: + """Test that functions with *args work with no arguments.""" - get_data.clear_cache() + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(*args): + """Test function that accepts variadic arguments.""" + self.call_count += 1 + return f"Result for args: {args}, call #{self.call_count}" + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_call_with_no_arguments(self): + """Test calling with no arguments.""" + result1 = self.get_data() + assert self.call_count == 1 + assert "()" in result1 + + def test_no_args_cache_hit(self): + """Test cache hit when calling with no arguments again.""" + # First call + self.get_data() + assert self.call_count == 1 + + # Second call should use cache + previous_call_count = self.call_count + result2 = self.get_data() + assert self.call_count == previous_call_count @pytest.mark.pickle -def test_varargs_with_regular_args(): +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestVarargsWithRegularArgs: """Test regular and variadic arguments work correctly.""" - call_count = 0 - - @cachier(stale_after=timedelta(seconds=500)) - def get_data(command, *args): - """Test function with regular and variadic arguments.""" - nonlocal call_count - call_count += 1 - return f"Command: {command}, args: {args}, call #{call_count}" - - get_data.clear_cache() - call_count = 0 - # Test different calls - result1 = get_data("print", "domains") - assert call_count == 1 - - result2 = get_data("print", "users", "allfields") - assert call_count == 2 - assert result1 != result2 - - result3 = get_data("list") - assert call_count == 3 - assert result3 != result1 - assert result3 != result2 - - # Test cache hits - previous_call_count = call_count - result4 = get_data("print", "domains") - assert call_count == previous_call_count - assert result4 == result1 - - get_data.clear_cache() + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(command, *args): + """Test function with regular and variadic arguments.""" + self.call_count += 1 + return f"Command: {command}, args: {args}, call #{self.call_count}" + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_different_varargs_produce_different_results(self): + """Test that different varargs produce different results.""" + result1 = self.get_data("print", "domains") + assert self.call_count == 1 + + result2 = self.get_data("print", "users", "allfields") + assert self.call_count == 2 + assert result1 != result2 + + def test_different_regular_arg_produces_different_result(self): + """Test that different regular arg produces different result.""" + result1 = self.get_data("print", "domains") + result3 = self.get_data("list") + assert self.call_count == 2 + assert result3 != result1 + + def test_same_args_produce_cache_hit(self): + """Test that same arguments produce cache hit.""" + # First call + result1 = self.get_data("print", "domains") + assert self.call_count == 1 + + # Same args should use cache + previous_call_count = self.call_count + result4 = self.get_data("print", "domains") + assert self.call_count == previous_call_count + assert result4 == result1 @pytest.mark.pickle -def test_varkwargs_different_cache_keys(): +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestVarkwargsDifferentCacheKeys: """Test **kwargs get unique cache keys for different arguments.""" - call_count = 0 - - @cachier(stale_after=timedelta(seconds=500)) - def get_data(**kwargs): - """Test function that accepts keyword variadic arguments.""" - nonlocal call_count - call_count += 1 - return f"Result for kwargs: {kwargs}, call #{call_count}" - - get_data.clear_cache() - call_count = 0 - # Test with different kwargs - result1 = get_data(type="domains", action="print") - assert call_count == 1 - - result2 = get_data(type="users", action="print", fields="allfields") - assert call_count == 2 - assert result1 != result2 - - # Test cache hits - previous_call_count = call_count - result3 = get_data(type="domains", action="print") - assert call_count == previous_call_count - assert result3 == result1 - - get_data.clear_cache() + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(**kwargs): + """Test function that accepts keyword variadic arguments.""" + self.call_count += 1 + return f"Result for kwargs: {kwargs}, call #{self.call_count}" + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_different_kwargs_produce_different_results(self): + """Test that different kwargs produce different results.""" + result1 = self.get_data(type="domains", action="print") + assert self.call_count == 1 + + result2 = self.get_data(type="users", action="print", fields="allfields") + assert self.call_count == 2 + assert result1 != result2 + + def test_same_kwargs_produce_cache_hit(self): + """Test that same kwargs produce cache hit.""" + # First call + result1 = self.get_data(type="domains", action="print") + assert self.call_count == 1 + + # Same kwargs should use cache + previous_call_count = self.call_count + result3 = self.get_data(type="domains", action="print") + assert self.call_count == previous_call_count + assert result3 == result1 @pytest.mark.pickle -def test_varargs_and_varkwargs(): +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestVarargsAndVarkwargs: """Test that functions with both *args and **kwargs work correctly.""" - call_count = 0 - - @cachier(stale_after=timedelta(seconds=500)) - def get_data(*args, **kwargs): - """Test function with both variadic arguments.""" - nonlocal call_count - call_count += 1 - return f"args: {args}, kwargs: {kwargs}, call #{call_count}" - - get_data.clear_cache() - call_count = 0 - - # Test different combinations - result1 = get_data("print", "domains") - assert call_count == 1 - result2 = get_data("print", "users", action="list") - assert call_count == 2 - assert result1 != result2 - - result3 = get_data(action="list", resource="domains") - assert call_count == 3 - assert result3 != result1 - assert result3 != result2 - - # Test cache hits - result4 = get_data("print", "domains") - assert call_count == 3 - assert result4 == result1 - - get_data.clear_cache() - - -@pytest.mark.memory -def test_varargs_memory_backend(): - """Test that variadic arguments work with memory backend.""" - call_count = 0 - - @cachier(backend="memory", stale_after=timedelta(seconds=500)) - def get_data(*args): - """Test function that accepts variadic arguments.""" - nonlocal call_count - call_count += 1 - return f"Result: {args}, call #{call_count}" - - get_data.clear_cache() - call_count = 0 - - result1 = get_data("a", "b") - assert call_count == 1 - - result2 = get_data("a", "b", "c") - assert call_count == 2 - assert result1 != result2 - - previous_call_count = call_count - result3 = get_data("a", "b") - assert call_count == previous_call_count - assert result3 == result1 - - get_data.clear_cache() + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(*args, **kwargs): + """Test function with both variadic arguments.""" + self.call_count += 1 + return f"args: {args}, kwargs: {kwargs}, call #{self.call_count}" + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_args_only_call(self): + """Test call with only args.""" + result1 = self.get_data("print", "domains") + assert self.call_count == 1 + + # Cache hit + result4 = self.get_data("print", "domains") + assert self.call_count == 1 + assert result4 == result1 + + def test_args_and_kwargs_call(self): + """Test call with both args and kwargs.""" + result2 = self.get_data("print", "users", action="list") + assert self.call_count == 1 + + # Cache hit + result_check = self.get_data("print", "users", action="list") + assert self.call_count == 1 + assert result_check == result2 + + def test_kwargs_only_call(self): + """Test call with only kwargs.""" + result3 = self.get_data(action="list", resource="domains") + assert self.call_count == 1 + + # Cache hit + result_check = self.get_data(action="list", resource="domains") + assert self.call_count == 1 + assert result_check == result3 + + def test_different_combinations_produce_different_results(self): + """Test that different combinations produce different results.""" + result1 = self.get_data("print", "domains") + result2 = self.get_data("print", "users", action="list") + result3 = self.get_data(action="list", resource="domains") + + assert result3 != result1 + assert result3 != result2 + assert result1 != result2 @pytest.mark.pickle -def test_keyword_only_parameters(): +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestKeywordOnlyParameters: """Test that functions with keyword-only parameters work correctly.""" - call_count = 0 - @cachier(stale_after=timedelta(seconds=500)) - def get_data(*args, kw_only): - """Test function with keyword-only parameter.""" - nonlocal call_count - call_count += 1 - return f"args={args}, kw_only={kw_only}, call #{call_count}" + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(*args, kw_only): + """Test function with keyword-only parameter.""" + self.call_count += 1 + return f"args={args}, kw_only={kw_only}, call #{self.call_count}" - get_data.clear_cache() - call_count = 0 + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() - # Test with different keyword-only values - result1 = get_data("a", "b", kw_only="value1") - assert call_count == 1 + def test_different_kw_only_values_produce_different_results(self): + """Test that different keyword-only values produce different results.""" + result1 = self.get_data("a", "b", kw_only="value1") + assert self.call_count == 1 - result2 = get_data("a", "b", kw_only="value2") - assert call_count == 2 - assert result1 != result2 + result2 = self.get_data("a", "b", kw_only="value2") + assert self.call_count == 2 + assert result1 != result2 - # Test cache hit - result3 = get_data("a", "b", kw_only="value1") - assert call_count == 2 - assert result3 == result1 + def test_same_kw_only_value_produces_cache_hit(self): + """Test that same keyword-only value produces cache hit.""" + # First call + result1 = self.get_data("a", "b", kw_only="value1") + assert self.call_count == 1 - get_data.clear_cache() + # Same kw_only should use cache + result3 = self.get_data("a", "b", kw_only="value1") + assert self.call_count == 1 + assert result3 == result1 @pytest.mark.pickle -def test_keyword_only_with_default(): +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestKeywordOnlyWithDefault: """Test keyword-only parameters with defaults work correctly.""" - call_count = 0 - @cachier(stale_after=timedelta(seconds=500)) - def get_data(*args, kw_only="default"): - """Test function with keyword-only parameter with default.""" - nonlocal call_count - call_count += 1 - return f"args={args}, kw_only={kw_only}, call #{call_count}" - - get_data.clear_cache() - call_count = 0 - - # Test with default value - result1 = get_data("a", "b") - assert call_count == 1 - assert "kw_only=default" in result1 - - # Test with explicit value - result2 = get_data("a", "b", kw_only="explicit") - assert call_count == 2 - assert result1 != result2 - - # Test cache hit with default - result3 = get_data("a", "b") - assert call_count == 2 - assert result3 == result1 - - # Test cache hit with explicit value - result4 = get_data("a", "b", kw_only="explicit") - assert call_count == 2 - assert result4 == result2 - - get_data.clear_cache() + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(*args, kw_only="default"): + """Test function with keyword-only parameter with default.""" + self.call_count += 1 + return f"args={args}, kw_only={kw_only}, call #{self.call_count}" + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_with_default_value(self): + """Test calling with default value.""" + result1 = self.get_data("a", "b") + assert self.call_count == 1 + assert "kw_only=default" in result1 + + def test_with_explicit_value(self): + """Test calling with explicit value.""" + result1 = self.get_data("a", "b") + result2 = self.get_data("a", "b", kw_only="explicit") + assert self.call_count == 2 + assert result1 != result2 + + def test_cache_hit_with_default(self): + """Test cache hit when using default value.""" + # First call with default + result1 = self.get_data("a", "b") + assert self.call_count == 1 + + # Second call with default should use cache + result3 = self.get_data("a", "b") + assert self.call_count == 1 + assert result3 == result1 + + def test_cache_hit_with_explicit(self): + """Test cache hit when using explicit value.""" + # First call with explicit + result2 = self.get_data("a", "b", kw_only="explicit") + assert self.call_count == 1 + + # Second call with explicit should use cache + result4 = self.get_data("a", "b", kw_only="explicit") + assert self.call_count == 1 + assert result4 == result2 @pytest.mark.pickle -def test_mixed_varargs_keyword_only(): +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestMixedVarargsKeywordOnly: """Test *args and keyword-only parameters work correctly.""" - call_count = 0 - - @cachier(stale_after=timedelta(seconds=500)) - def get_data(regular, *args, kw_only, kw_with_default="default"): - """Test function with mixed parameter types.""" - nonlocal call_count - call_count += 1 - return ( - f"regular={regular}, args={args}, kw_only={kw_only}, " - f"kw_with_default={kw_with_default}, call #{call_count}" - ) - - get_data.clear_cache() - call_count = 0 - - # Test different combinations - result1 = get_data("r1", "a", "b", kw_only="k1") - assert call_count == 1 - - result2 = get_data("r1", "a", "b", kw_only="k2") - assert call_count == 2 - assert result1 != result2 - - result3 = get_data("r1", "a", "b", kw_only="k1", kw_with_default="custom") - assert call_count == 3 - assert result3 != result1 - - # Test cache hits - result4 = get_data("r1", "a", "b", kw_only="k1") - assert call_count == 3 - assert result4 == result1 - get_data.clear_cache() + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def get_data(regular, *args, kw_only, kw_with_default="default"): + """Test function with mixed parameter types.""" + self.call_count += 1 + return ( + f"regular={regular}, args={args}, kw_only={kw_only}, " + f"kw_with_default={kw_with_default}, call #{self.call_count}" + ) + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_different_kw_only_produces_different_results(self): + """Test that different kw_only values produce different results.""" + result1 = self.get_data("r1", "a", "b", kw_only="k1") + assert self.call_count == 1 + + result2 = self.get_data("r1", "a", "b", kw_only="k2") + assert self.call_count == 2 + assert result1 != result2 + + def test_different_kw_with_default_produces_different_results(self): + """Test that different kw_with_default values produce different results.""" + result1 = self.get_data("r1", "a", "b", kw_only="k1") + result3 = self.get_data("r1", "a", "b", kw_only="k1", kw_with_default="custom") + assert self.call_count == 2 + assert result3 != result1 + + def test_same_args_produce_cache_hit(self): + """Test that same arguments produce cache hit.""" + # First call + result1 = self.get_data("r1", "a", "b", kw_only="k1") + assert self.call_count == 1 + + # Same args should use cache + result4 = self.get_data("r1", "a", "b", kw_only="k1") + assert self.call_count == 1 + assert result4 == result1 From ce825f4292efbee3022716f39122218c600b2638 Mon Sep 17 00:00:00 2001 From: jirka <6035284+Borda@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:44:56 +0100 Subject: [PATCH 09/14] Refactor and parametrize varargs/varkwargs test cases for enhanced readability and backend coverage --- tests/test_varargs.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index 88f38685..e2b41917 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -110,7 +110,7 @@ def test_no_args_cache_hit(self): # Second call should use cache previous_call_count = self.call_count - result2 = self.get_data() + _ = self.get_data() assert self.call_count == previous_call_count @@ -192,7 +192,9 @@ def test_different_kwargs_produce_different_results(self): result1 = self.get_data(type="domains", action="print") assert self.call_count == 1 - result2 = self.get_data(type="users", action="print", fields="allfields") + result2 = self.get_data( + type="users", action="print", fields="allfields" + ) assert self.call_count == 2 assert result1 != result2 @@ -295,7 +297,7 @@ def get_data(*args, kw_only): get_data.clear_cache() def test_different_kw_only_values_produce_different_results(self): - """Test that different keyword-only values produce different results.""" + """Test that different keyword-only values produce dif results.""" result1 = self.get_data("a", "b", kw_only="value1") assert self.call_count == 1 @@ -408,9 +410,11 @@ def test_different_kw_only_produces_different_results(self): assert result1 != result2 def test_different_kw_with_default_produces_different_results(self): - """Test that different kw_with_default values produce different results.""" + """Test that different kw_with_default values produce dif results.""" result1 = self.get_data("r1", "a", "b", kw_only="k1") - result3 = self.get_data("r1", "a", "b", kw_only="k1", kw_with_default="custom") + result3 = self.get_data( + "r1", "a", "b", kw_only="k1", kw_with_default="custom" + ) assert self.call_count == 2 assert result3 != result1 From 00808aadd2ff2e51f9a92e1c8a1a0be8a5cb85ac Mon Sep 17 00:00:00 2001 From: jirka <6035284+Borda@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:49:20 +0100 Subject: [PATCH 10/14] Remove redundant `@pytest.mark.pickle` annotations from varargs/varkwargs test cases --- tests/test_varargs.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index e2b41917..c73fafbf 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -7,7 +7,6 @@ from cachier import cachier -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestVarargsDifferentCacheKeys: """Test *args get unique cache keys for different arguments.""" @@ -74,7 +73,6 @@ def test_another_set_of_args_cache_hit(self): assert result4 == result2 -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestVarargsEmpty: """Test that functions with *args work with no arguments.""" @@ -114,7 +112,6 @@ def test_no_args_cache_hit(self): assert self.call_count == previous_call_count -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestVarargsWithRegularArgs: """Test regular and variadic arguments work correctly.""" @@ -165,7 +162,6 @@ def test_same_args_produce_cache_hit(self): assert result4 == result1 -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestVarkwargsDifferentCacheKeys: """Test **kwargs get unique cache keys for different arguments.""" @@ -211,7 +207,6 @@ def test_same_kwargs_produce_cache_hit(self): assert result3 == result1 -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestVarargsAndVarkwargs: """Test that functions with both *args and **kwargs work correctly.""" @@ -274,7 +269,6 @@ def test_different_combinations_produce_different_results(self): assert result1 != result2 -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestKeywordOnlyParameters: """Test that functions with keyword-only parameters work correctly.""" @@ -317,7 +311,6 @@ def test_same_kw_only_value_produces_cache_hit(self): assert result3 == result1 -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestKeywordOnlyWithDefault: """Test keyword-only parameters with defaults work correctly.""" @@ -375,7 +368,6 @@ def test_cache_hit_with_explicit(self): assert result4 == result2 -@pytest.mark.pickle @pytest.mark.parametrize("backend", ["pickle", "memory"]) class TestMixedVarargsKeywordOnly: """Test *args and keyword-only parameters work correctly.""" From 6ffc7ec701e4323c752d19bc4b100ab6a11af8a3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:29:25 +0000 Subject: [PATCH 11/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/cachier/core.py | 6 +----- tests/test_varargs.py | 24 ++++++------------------ 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/cachier/core.py b/src/cachier/core.py index a83777dd..030527df 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -117,11 +117,7 @@ def _convert_args_kwargs(func, _is_method: bool, args: tuple, kwds: dict) -> dic args_as_kw[f"__varargs_{i}__"] = arg # Init with default values - kwargs = { - k: v.default - for k, v in sig.parameters.items() - if v.default is not inspect.Parameter.empty - } + kwargs = {k: v.default for k, v in sig.parameters.items() if v.default is not inspect.Parameter.empty} # Merge args expanded as kwargs and the original kwds kwargs.update(args_as_kw) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index c73fafbf..d49dbc04 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -36,13 +36,9 @@ def test_different_args_produce_different_results(self): assert "('print', 'domains')" in result1 result2 = self.get_data("print", "users", "allfields") - assert self.call_count == 2, ( - "Function should be called again with different args" - ) + assert self.call_count == 2, "Function should be called again with different args" assert "('print', 'users', 'allfields')" in result2 - assert result1 != result2, ( - "Different args should produce different results" - ) + assert result1 != result2, "Different args should produce different results" def test_same_args_use_cache(self): """Test that calling with same arguments uses cache.""" @@ -53,9 +49,7 @@ def test_same_args_use_cache(self): # Second call with same args should use cache previous_call_count = self.call_count result3 = self.get_data("print", "domains") - assert self.call_count == previous_call_count, ( - "Function should not be called again (cache hit)" - ) + assert self.call_count == previous_call_count, "Function should not be called again (cache hit)" assert result3 == result1 def test_another_set_of_args_cache_hit(self): @@ -67,9 +61,7 @@ def test_another_set_of_args_cache_hit(self): # Second call with same args should use cache previous_call_count = self.call_count result4 = self.get_data("print", "users", "allfields") - assert self.call_count == previous_call_count, ( - "Function should not be called again (cache hit)" - ) + assert self.call_count == previous_call_count, "Function should not be called again (cache hit)" assert result4 == result2 @@ -188,9 +180,7 @@ def test_different_kwargs_produce_different_results(self): result1 = self.get_data(type="domains", action="print") assert self.call_count == 1 - result2 = self.get_data( - type="users", action="print", fields="allfields" - ) + result2 = self.get_data(type="users", action="print", fields="allfields") assert self.call_count == 2 assert result1 != result2 @@ -404,9 +394,7 @@ def test_different_kw_only_produces_different_results(self): def test_different_kw_with_default_produces_different_results(self): """Test that different kw_with_default values produce dif results.""" result1 = self.get_data("r1", "a", "b", kw_only="k1") - result3 = self.get_data( - "r1", "a", "b", kw_only="k1", kw_with_default="custom" - ) + result3 = self.get_data("r1", "a", "b", kw_only="k1", kw_with_default="custom") assert self.call_count == 2 assert result3 != result1 From 79b47fd488a6a379dc2fd8baa53e403561a18996 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:21:06 +0000 Subject: [PATCH 12/14] Add comprehensive edge case tests for variadic arguments - Added TestFunctoolsPartial: tests for functools.partial wrapped functions - Added TestMethodWithVarargs: tests for functions simulating method behavior - Added TestPositionalOnlyParams: tests for positional-only parameters (/) with varargs - Added TestComplexParameterMix: tests for functions with all parameter types combined - Added TestEdgeCasesEmptyAndNone: tests for None, empty strings, and zero values in varargs - Total test count increased from 42 to 68 tests (26 new tests) - All tests parametrized across pickle and memory backends for comprehensive coverage Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- tests/test_varargs.py | 255 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index d49dbc04..e05a4978 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -408,3 +408,258 @@ def test_same_args_produce_cache_hit(self): result4 = self.get_data("r1", "a", "b", kw_only="k1") assert self.call_count == 1 assert result4 == result1 + + +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestFunctoolsPartial: + """Test functools.partial wrapped functions with variadic arguments.""" + + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + import functools + + self.call_count = 0 + self.backend = backend + + def base_function(prefix, *args, suffix="end"): + """Base function to be wrapped with partial.""" + self.call_count += 1 + return f"{prefix}-{args}-{suffix}-{self.call_count}" + + # Create partial with prefix bound + partial_func = functools.partial(base_function, "PREFIX") + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def wrapped_partial(*args, **kwargs): + return partial_func(*args, **kwargs) + + self.get_data = wrapped_partial + wrapped_partial.clear_cache() + self.call_count = 0 + yield + wrapped_partial.clear_cache() + + def test_partial_with_different_varargs(self): + """Test that partial functions with different *args get unique keys.""" + result1 = self.get_data("a", "b") + assert self.call_count == 1 + + result2 = self.get_data("a", "b", "c") + assert self.call_count == 2 + assert result1 != result2 + + def test_partial_with_same_varargs_uses_cache(self): + """Test that partial functions with same *args use cache.""" + result1 = self.get_data("a", "b") + assert self.call_count == 1 + + result2 = self.get_data("a", "b") + assert self.call_count == 1 + assert result1 == result2 + + +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestMethodWithVarargs: + """Test instance methods with variadic arguments.""" + + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test class for each test.""" + self.backend = backend + self.call_count = 0 + + # Create a simple test class with a cached method + # Note: Methods cache based on ALL arguments including self, + # so we test that different instances don't interfere + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def standalone_func(*args): + """Standalone function that simulates method behavior.""" + self.call_count += 1 + return f"Result: {args}, call #{self.call_count}" + + self.cached_func = standalone_func + standalone_func.clear_cache() + yield + standalone_func.clear_cache() + + def test_method_with_different_varargs(self): + """Test that functions with different *args get unique keys.""" + result1 = self.cached_func("a", "b") + assert self.call_count == 1 + + result2 = self.cached_func("x", "y", "z") + assert self.call_count == 2 + assert result1 != result2 + + def test_method_with_same_varargs_uses_cache(self): + """Test that functions with same *args use cache.""" + result1 = self.cached_func("a", "b") + assert self.call_count == 1 + + result2 = self.cached_func("a", "b") + assert self.call_count == 1 + assert result1 == result2 + + +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestPositionalOnlyParams: + """Test functions with positional-only parameters (/) and varargs.""" + + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + self.backend = backend + + # Note: POSITIONAL_ONLY requires Python 3.8+ + # Using exec to define function with / syntax + exec_globals = {"cachier": cachier, "timedelta": timedelta, "backend": backend} + exec( # noqa: S102 + """ +@cachier(backend=backend, stale_after=timedelta(seconds=500)) +def get_data(pos_only, /, *args, kw_only=None): + global call_count + call_count += 1 + return f"pos_only={pos_only}, args={args}, kw_only={kw_only}, call #{call_count}" +""", + exec_globals, + ) + + self.get_data = exec_globals["get_data"] + exec_globals["call_count"] = 0 + self.exec_globals = exec_globals + self.get_data.clear_cache() + yield + self.get_data.clear_cache() + + def test_positional_only_with_varargs(self): + """Test positional-only params with varargs produce unique keys.""" + self.exec_globals["call_count"] = 0 + + result1 = self.get_data("pos1", "a", "b", kw_only="k1") + assert self.exec_globals["call_count"] == 1 + + result2 = self.get_data("pos2", "a", "b", kw_only="k1") + assert self.exec_globals["call_count"] == 2 + assert result1 != result2 + + def test_positional_only_different_varargs(self): + """Test different varargs with positional-only produce unique keys.""" + self.exec_globals["call_count"] = 0 + + result1 = self.get_data("pos1", "a", "b") + assert self.exec_globals["call_count"] == 1 + + result2 = self.get_data("pos1", "x", "y", "z") + assert self.exec_globals["call_count"] == 2 + assert result1 != result2 + + +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestComplexParameterMix: + """Test functions with all parameter types combined.""" + + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + self.backend = backend + + @cachier(backend=backend, stale_after=timedelta(seconds=500)) + def complex_func(regular1, regular2="default2", *args, kw_only, kw_default="kw_def", **kwargs): + """Function with all parameter types.""" + self.call_count += 1 + return ( + f"r1={regular1}, r2={regular2}, args={args}, kw_only={kw_only}, " + f"kw_default={kw_default}, kwargs={sorted(kwargs.items())}, call #{self.call_count}" + ) + + self.get_data = complex_func + complex_func.clear_cache() + self.call_count = 0 + yield + complex_func.clear_cache() + + def test_all_params_different_combinations(self): + """Test various combinations of all parameter types.""" + # Combination 1: minimal required params + result1 = self.get_data("r1", kw_only="ko1") + assert self.call_count == 1 + + # Combination 2: with varargs + result2 = self.get_data("r1", "r2val", "extra1", "extra2", kw_only="ko1") + assert self.call_count == 2 + assert result1 != result2 + + # Combination 3: with varkwargs + result3 = self.get_data("r1", kw_only="ko1", extra_kw="value") + assert self.call_count == 3 + assert result3 != result1 + + # Combination 4: full complexity + result4 = self.get_data("r1", "r2val", "e1", "e2", kw_only="ko1", kw_default="custom", x="a", y="b") + assert self.call_count == 4 + assert result4 != result1 + assert result4 != result2 + assert result4 != result3 + + def test_cache_hit_with_complex_params(self): + """Test cache hit with complex parameter mix.""" + result1 = self.get_data("r1", "r2val", "e1", kw_only="ko1", extra="val") + assert self.call_count == 1 + + result2 = self.get_data("r1", "r2val", "e1", kw_only="ko1", extra="val") + assert self.call_count == 1 + assert result1 == result2 + + +@pytest.mark.parametrize("backend", ["pickle", "memory"]) +class TestEdgeCasesEmptyAndNone: + """Test edge cases with empty values and None.""" + + @pytest.fixture(autouse=True) + def setup(self, backend): + """Set up the test function for each test.""" + self.call_count = 0 + self.backend = backend + + @cachier(backend=backend, stale_after=timedelta(seconds=500), allow_none=True) + def get_data(*args, **kwargs): + """Function that might return None.""" + self.call_count += 1 + return (args, kwargs, self.call_count) if args or kwargs else None + + self.get_data = get_data + get_data.clear_cache() + self.call_count = 0 + yield + get_data.clear_cache() + + def test_none_in_varargs(self): + """Test that None values in varargs are handled correctly.""" + result1 = self.get_data(None, "value") + assert self.call_count == 1 + + result2 = self.get_data("value", None) + assert self.call_count == 2 + assert result1 != result2 + + def test_empty_string_in_varargs(self): + """Test that empty strings in varargs are distinguished.""" + result1 = self.get_data("", "value") + assert self.call_count == 1 + + result2 = self.get_data("value", "") + assert self.call_count == 2 + assert result1 != result2 + + def test_zero_in_varargs(self): + """Test that zero values in varargs are handled correctly.""" + result1 = self.get_data(0, 1) + assert self.call_count == 1 + + result2 = self.get_data(1, 0) + assert self.call_count == 2 + assert result1 != result2 + From ab76ca6c912d2ed87fb85042f45a8e50dbe19411 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:25:10 +0000 Subject: [PATCH 13/14] Simplify redundant conditional in _convert_args_kwargs - Removed redundant if-else where both branches performed the same action - Simplified kwds handling to always use kwargs.update(kwds) - Removed unused var_keyword_name variable - All 68 tests still pass Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- src/cachier/core.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/cachier/core.py b/src/cachier/core.py index 030527df..fa21176e 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -79,17 +79,14 @@ def _convert_args_kwargs(func, _is_method: bool, args: tuple, kwds: dict) -> dic sig = inspect.signature(func) func_params = list(sig.parameters) - # Separate regular parameters from VAR_POSITIONAL and VAR_KEYWORD + # Separate regular parameters from VAR_POSITIONAL regular_params = [] var_positional_name = None - var_keyword_name = None for param_name in func_params: param = sig.parameters[param_name] if param.kind == inspect.Parameter.VAR_POSITIONAL: var_positional_name = param_name - elif param.kind == inspect.Parameter.VAR_KEYWORD: - var_keyword_name = param_name elif param.kind in ( inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD, @@ -122,17 +119,8 @@ def _convert_args_kwargs(func, _is_method: bool, args: tuple, kwds: dict) -> dic # Merge args expanded as kwargs and the original kwds kwargs.update(args_as_kw) - # Handle variadic keyword arguments and keyword-only parameters - if var_keyword_name: - # Separate kwds that match known parameters from those that don't - for k, v in kwds.items(): - if k in sig.parameters: - kwargs[k] = v - else: - # Extra kwargs go directly into the result dict - kwargs[k] = v - else: - kwargs.update(kwds) + # Handle keyword arguments (including variadic keyword arguments) + kwargs.update(kwds) return OrderedDict(sorted(kwargs.items())) From 14860b4aa27d09b3d2897341c8003f3d8e2801fc Mon Sep 17 00:00:00 2001 From: jirka <6035284+Borda@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:30:33 +0100 Subject: [PATCH 14/14] linting --- tests/test_varargs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_varargs.py b/tests/test_varargs.py index e05a4978..17e4dc50 100644 --- a/tests/test_varargs.py +++ b/tests/test_varargs.py @@ -662,4 +662,3 @@ def test_zero_in_varargs(self): result2 = self.get_data(1, 0) assert self.call_count == 2 assert result1 != result2 -