From 666f933ac4e8ff5a0e86701c6be7e1c80a1854f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 19:09:26 +0000 Subject: [PATCH 1/4] Fix PermissionError in _clear_all_cache_files on Windows by adding retry loop Co-authored-by: shaypal5 <917954+shaypal5@users.noreply.github.com> --- src/cachier/cores/pickle.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/cachier/cores/pickle.py b/src/cachier/cores/pickle.py index 37f29281..22c83a46 100644 --- a/src/cachier/cores/pickle.py +++ b/src/cachier/cores/pickle.py @@ -145,7 +145,19 @@ def _clear_all_cache_files(self) -> None: path, name = os.path.split(self.cache_fpath) for subpath in os.listdir(path): if subpath.startswith(f"{name}_"): - os.remove(os.path.join(path, subpath)) + fpath = os.path.join(path, subpath) + # Retry loop to handle Windows mandatory file-locking (WinError 32): + # portalocker holds an exclusive lock while a thread is computing, + # so os.remove() may fail transiently until the lock is released. + for attempt in range(3): + try: + os.remove(fpath) + break + except PermissionError: + if attempt < 2: + time.sleep(0.1 * (attempt + 1)) + else: + raise def _clear_being_calculated_all_cache_files(self) -> None: path, name = os.path.split(self.cache_fpath) From 715756677ffe2cc3a4284f7674e003d5f71ddc79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 19:42:37 +0000 Subject: [PATCH 2/4] Add tests for _clear_all_cache_files retry logic to reach 100% patch coverage Co-authored-by: shaypal5 <917954+shaypal5@users.noreply.github.com> --- tests/pickle_tests/test_pickle_core.py | 72 ++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/pickle_tests/test_pickle_core.py b/tests/pickle_tests/test_pickle_core.py index 7b8aab22..3643d12d 100644 --- a/tests/pickle_tests/test_pickle_core.py +++ b/tests/pickle_tests/test_pickle_core.py @@ -1212,6 +1212,78 @@ def mock_func(): core.delete_stale_entries(timedelta(hours=1)) +@pytest.mark.pickle +def test_clear_all_cache_files_retries_on_permission_error(tmp_path): + """Test _clear_all_cache_files retries on PermissionError then succeeds.""" + core = _PickleCore( + hash_func=None, + cache_dir=tmp_path, + pickle_reload=False, + wait_for_calc_timeout=10, + separate_files=True, + ) + + def mock_func(): + pass + + core.set_func(mock_func) + + # Create a cache file that matches the name pattern + cache_fpath = core.cache_fpath + dummy_file = cache_fpath + "_dummykey" + with open(dummy_file, "wb") as f: + f.write(b"") + + # os.remove fails twice then succeeds on the third call + real_remove = os.remove + call_count = 0 + + def flaky_remove(path): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise PermissionError("locked") + real_remove(path) + + with patch("cachier.cores.pickle.os.remove", side_effect=flaky_remove), patch( + "cachier.cores.pickle.time.sleep" + ) as mock_sleep: + core._clear_all_cache_files() + assert mock_sleep.call_count == 2 + mock_sleep.assert_any_call(0.1) + mock_sleep.assert_any_call(0.2) + + assert not os.path.exists(dummy_file) + + +@pytest.mark.pickle +def test_clear_all_cache_files_raises_on_persistent_permission_error(tmp_path): + """Test _clear_all_cache_files re-raises PermissionError after all retries.""" + core = _PickleCore( + hash_func=None, + cache_dir=tmp_path, + pickle_reload=False, + wait_for_calc_timeout=10, + separate_files=True, + ) + + def mock_func(): + pass + + core.set_func(mock_func) + + # Create a cache file that matches the name pattern + cache_fpath = core.cache_fpath + dummy_file = cache_fpath + "_dummykey" + with open(dummy_file, "wb") as f: + f.write(b"") + + with patch("cachier.cores.pickle.os.remove", side_effect=PermissionError("locked")), patch( + "cachier.cores.pickle.time.sleep" + ), pytest.raises(PermissionError): + core._clear_all_cache_files() + + # Redis core static method tests @pytest.mark.parametrize( ("test_input", "expected"), From cd1a9a9eef629ea43553a10eae4e59d8eac3af01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 19:55:52 +0000 Subject: [PATCH 3/4] Fix ruff-format pre-commit.ci failure in test_pickle_core.py Co-authored-by: shaypal5 <917954+shaypal5@users.noreply.github.com> --- tests/pickle_tests/test_pickle_core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/pickle_tests/test_pickle_core.py b/tests/pickle_tests/test_pickle_core.py index 3643d12d..66e27c6b 100644 --- a/tests/pickle_tests/test_pickle_core.py +++ b/tests/pickle_tests/test_pickle_core.py @@ -1245,9 +1245,10 @@ def flaky_remove(path): raise PermissionError("locked") real_remove(path) - with patch("cachier.cores.pickle.os.remove", side_effect=flaky_remove), patch( - "cachier.cores.pickle.time.sleep" - ) as mock_sleep: + with ( + patch("cachier.cores.pickle.os.remove", side_effect=flaky_remove), + patch("cachier.cores.pickle.time.sleep") as mock_sleep, + ): core._clear_all_cache_files() assert mock_sleep.call_count == 2 mock_sleep.assert_any_call(0.1) @@ -1278,9 +1279,11 @@ def mock_func(): with open(dummy_file, "wb") as f: f.write(b"") - with patch("cachier.cores.pickle.os.remove", side_effect=PermissionError("locked")), patch( - "cachier.cores.pickle.time.sleep" - ), pytest.raises(PermissionError): + with ( + patch("cachier.cores.pickle.os.remove", side_effect=PermissionError("locked")), + patch("cachier.cores.pickle.time.sleep"), + pytest.raises(PermissionError), + ): core._clear_all_cache_files() From 7c818b1d6003cec4027fc6fc2a7c0fa37bd614d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:28:18 +0000 Subject: [PATCH 4/4] Add pragma no branch to inner for loop to reach 100% patch coverage Co-authored-by: shaypal5 <917954+shaypal5@users.noreply.github.com> --- src/cachier/cores/pickle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cachier/cores/pickle.py b/src/cachier/cores/pickle.py index 22c83a46..e9395822 100644 --- a/src/cachier/cores/pickle.py +++ b/src/cachier/cores/pickle.py @@ -149,7 +149,7 @@ def _clear_all_cache_files(self) -> None: # Retry loop to handle Windows mandatory file-locking (WinError 32): # portalocker holds an exclusive lock while a thread is computing, # so os.remove() may fail transiently until the lock is released. - for attempt in range(3): + for attempt in range(3): # pragma: no branch try: os.remove(fpath) break