From 12e2830e63163347406aa28f665f5f26a8e5321a Mon Sep 17 00:00:00 2001 From: Marzio Monticelli Date: Tue, 3 Nov 2026 11:08:54 +0000 Subject: [PATCH 1/2] fix(pickle): match cache-file events by name so waiters wake under watchdog 7 watchdog 7 matches PatternMatchingEventHandler patterns against the whole path (PurePath.full_match), so the "*" + filename pattern never matched an absolute event path. A caller waiting on an entry being calculated by another thread or process never saw the change and hung forever. --- src/cachier/cores/pickle.py | 27 ++++++++++++++-------- tests/pickle_tests/test_pickle_core.py | 31 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/cachier/cores/pickle.py b/src/cachier/cores/pickle.py index fda3b308..aad64550 100644 --- a/src/cachier/cores/pickle.py +++ b/src/cachier/cores/pickle.py @@ -17,7 +17,7 @@ from typing import IO, TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, cast import portalocker # to lock on pickle cache IO -from watchdog.events import PatternMatchingEventHandler +from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer from .._types import HashFunc @@ -35,22 +35,31 @@ class _PickleCore(_BaseCore): _SHARED_LOCK_SUFFIX = ".lock" - class CacheChangeHandler(PatternMatchingEventHandler): + class CacheChangeHandler(FileSystemEventHandler): """Handles cache-file modification events.""" def __init__(self, filename, core, key): - PatternMatchingEventHandler.__init__( - self, - patterns=["*" + filename], - ignore_patterns=None, - ignore_directories=True, - case_sensitive=False, - ) + FileSystemEventHandler.__init__(self) + self.filename = filename self.core = core self.key = key self.observer = None self.value = None + def dispatch(self, event: FileSystemEvent) -> None: + """Dispatch only the events of this cache file. + + Matched by file name rather than a glob pattern: watchdog 7 matches + patterns against the whole path (``PurePath.full_match``), so the + former ``"*" + filename`` pattern never matched and waiters hung. + + """ + if event.is_directory: + return + paths = [event.src_path, getattr(event, "dest_path", "")] + if any(path and os.path.basename(os.fsdecode(path)) == self.filename for path in paths): + super().dispatch(event) + def inject_observer(self, observer) -> None: """Inject the observer running this handler.""" self.observer = observer diff --git a/tests/pickle_tests/test_pickle_core.py b/tests/pickle_tests/test_pickle_core.py index 451363da..8a300aa0 100644 --- a/tests/pickle_tests/test_pickle_core.py +++ b/tests/pickle_tests/test_pickle_core.py @@ -22,6 +22,12 @@ from unittest.mock import Mock, patch import pytest +from watchdog.events import ( + DirModifiedEvent, + FileCreatedEvent, + FileModifiedEvent, + FileMovedEvent, +) try: import queue @@ -265,6 +271,31 @@ def test_pickle_being_calculated(separate_files): assert res1 == res2 +@pytest.mark.pickle +@pytest.mark.parametrize( + ("event", "dispatched"), + [ + (FileModifiedEvent("/cache/dir/.mod.func"), True), + (FileCreatedEvent("/cache/dir/.mod.func"), True), + (FileMovedEvent("/cache/dir/.mod.func.tmp", "/cache/dir/.mod.func"), True), + (FileModifiedEvent("/cache/dir/.mod.func.lock"), False), + (FileModifiedEvent("/cache/dir/.mod.other_func"), False), + (DirModifiedEvent("/cache/dir/.mod.func"), False), + ], +) +def test_pickle_cache_change_handler_matches_the_cache_file_by_name(event, dispatched): + """Test the waiter's handler reacts to its cache file's events on a full path. + + watchdog 7 matches glob patterns against the whole path, which broke a + ``"*" + filename`` pattern and left concurrent waiters hanging. + + """ + handler = _PickleCore.CacheChangeHandler(filename=".mod.func", core=Mock(), key="k") + with patch.object(handler, "_check_calculation") as check_calculation: + handler.dispatch(event) + assert check_calculation.called is dispatched + + def _being_calc_next_time(arg_1, arg_2): """Some function.""" sleep(1) From 39a7733e39718df4036c3da35d82c6b9fec16981 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:10:10 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/pickle_tests/test_pickle_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pickle_tests/test_pickle_core.py b/tests/pickle_tests/test_pickle_core.py index 8a300aa0..5e56389a 100644 --- a/tests/pickle_tests/test_pickle_core.py +++ b/tests/pickle_tests/test_pickle_core.py @@ -286,8 +286,8 @@ def test_pickle_being_calculated(separate_files): def test_pickle_cache_change_handler_matches_the_cache_file_by_name(event, dispatched): """Test the waiter's handler reacts to its cache file's events on a full path. - watchdog 7 matches glob patterns against the whole path, which broke a - ``"*" + filename`` pattern and left concurrent waiters hanging. + watchdog 7 matches glob patterns against the whole path, which broke a ``"*" + filename`` pattern and left + concurrent waiters hanging. """ handler = _PickleCore.CacheChangeHandler(filename=".mod.func", core=Mock(), key="k")