From cec7ac8a4771f54865bb87655a9011f876a4932f Mon Sep 17 00:00:00 2001 From: "T.Rzepka" Date: Fri, 6 Mar 2026 20:12:00 +0100 Subject: [PATCH 1/3] Wait for timeout or return already received events immediately. Implementation prevents an endless loop, if events happens periodically while waiting for the timeout. Reduced timeout to 1 second for testcases. --- src/watchdog/observers/winapi.py | 2 ++ tests/utils.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/watchdog/observers/winapi.py b/src/watchdog/observers/winapi.py index 6bdd5a11..6e315b99 100644 --- a/src/watchdog/observers/winapi.py +++ b/src/watchdog/observers/winapi.py @@ -465,4 +465,6 @@ def get_events(self, timeout: float) -> list[WinAPINativeEvent]: except queue.Empty: break events.extend(_parse_event_buffer(buf)) + if self._buf_queue.empty(): + break return [WinAPINativeEvent(action, src_path) for action, src_path in events] diff --git a/tests/utils.py b/tests/utils.py index 948562f7..176bfbf3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -252,7 +252,7 @@ def _check_events_with_order( self, found_events: list[FileSystemEvent]) -> None for event, expected_event in zip(found_events, self.expected_events): assert self._match_event(event, expected_event), "did not find expected event" - def check_events(self, timeout: float = 2) -> None: + def check_events(self, timeout: float = 1.0) -> None: """Read events from the events queue (waiting for up to `timeout` for new events to appear). Confirm that expected events, as specified by calling add(), appear in the sequence of events receieved. From 30f1e5fb1c2ede258ac20637ffc57c6bd3eb0b50 Mon Sep 17 00:00:00 2001 From: "T.Rzepka" Date: Sun, 28 Jul 2024 20:28:12 +0200 Subject: [PATCH 2/3] Fix a silent fail if path is a file #1034 (Windows only). --- src/watchdog/observers/read_directory_changes.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/watchdog/observers/read_directory_changes.py b/src/watchdog/observers/read_directory_changes.py index 5d00980b..5d50324f 100644 --- a/src/watchdog/observers/read_directory_changes.py +++ b/src/watchdog/observers/read_directory_changes.py @@ -41,11 +41,16 @@ def __init__( super().__init__(event_queue, watch, timeout=timeout, event_filter=event_filter) self._lock = threading.RLock() self._reader: DirectoryChangeReader | None = None + self._watched_files: dict[str, str] = {} def on_thread_start(self) -> None: + watch_path = self.watch.path + if os.path.isfile(watch_path): + watch_path, basename = os.path.split(watch_path) + self._watched_files[self.watch.path] = basename with self._lock: assert self._reader is None - self._reader = DirectoryChangeReader(self.watch.path, recursive=self.watch.is_recursive) + self._reader = DirectoryChangeReader(watch_path, recursive=self.watch.is_recursive) self._reader.start() if platform.python_implementation() == "PyPy": @@ -71,7 +76,13 @@ def queue_events(self, timeout: float) -> None: last_renamed_src_path = "" should_stop = False for winapi_event in reader.get_events(timeout): - src_path = os.path.join(self.watch.path, winapi_event.src_path) + try: + basename = self._watched_files[self.watch.path] # Is a file? + if basename != winapi_event.src_path: + continue + src_path = self.watch.path + except KeyError: + src_path = os.path.join(self.watch.path, winapi_event.src_path) if winapi_event.is_renamed_old: last_renamed_src_path = src_path From 693bbb82d1c1a9b175505bdf85786a6f8b8cefac Mon Sep 17 00:00:00 2001 From: "T.Rzepka" Date: Sun, 28 Jul 2024 20:28:12 +0200 Subject: [PATCH 3/3] Testcases for subscribing a single file. --- tests/test_single_file_watched.py | 172 ++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 tests/test_single_file_watched.py diff --git a/tests/test_single_file_watched.py b/tests/test_single_file_watched.py new file mode 100644 index 00000000..f8d0778e --- /dev/null +++ b/tests/test_single_file_watched.py @@ -0,0 +1,172 @@ +"""Test cases when a single file is subscribed.""" + +import time +from os import mkdir + +from .utils import P, StartWatching, TestEventQueue + +SLEEP = 0.1 + + +def test_selftest(p: P, start_watching: StartWatching, event_queue: TestEventQueue) -> None: + """Check if all events are catched in SLEEP time.""" + + emitter = start_watching(path=p()) # Pretty sure this should work + + with open(p("file1.bak"), "w") as fp: + fp.write("test1") + + with open(p("file2.bak"), "w") as fp: + fp.write("test2") + + time.sleep(SLEEP) + + found_files = set() + try: + while event_queue.qsize(): + event, _ = event_queue.get() + if event.is_directory: # Not catched on Windows + assert event.src_path == p() + else: + found_files.add(event.src_path) + finally: + emitter.stop() + + assert len(found_files) == 2, "Number of expected files differ. Increase sleep time." + + +def test_file_access(p: P, start_watching: StartWatching, event_queue: TestEventQueue) -> None: + """Check if file fires events.""" + + file1 = "file1.bak" + tmpfile = p(file1) + + with open(tmpfile, "w") as fp: + fp.write("init1") + + emitter = start_watching(path=tmpfile) + + # This is what we want to see + with open(tmpfile, "w") as fp: + fp.write("test1") + + time.sleep(SLEEP) + + try: + while event_queue.qsize(): + event, _ = event_queue.get() + if event.is_directory: + assert event.src_path == p() + continue + assert event.src_path.endswith(file1) + break + else: + raise AssertionError # No event catched + finally: + emitter.stop() + + +def test_file_access_multiple(p: P, start_watching: StartWatching, event_queue: TestEventQueue) -> None: + """Check if file fires events multiple times.""" + + file1 = "file1.bak" + tmpfile = p(file1) + + with open(tmpfile, "w") as fp: + fp.write("init1") + + emitter = start_watching(path=tmpfile) + + try: + for _i in range(5): + # This is what we want to see multiple times + with open(tmpfile, "w") as fp: + fp.write("test1") + + time.sleep(SLEEP) + + while event_queue.qsize(): + event, _ = event_queue.get() + if event.is_directory: + assert event.src_path == p() + continue + assert event.src_path.endswith(file1) + break + else: + raise AssertionError # No event catched + + finally: + emitter.stop() + + +def test_file_access_other_file(p: P, start_watching: StartWatching, event_queue: TestEventQueue) -> None: + """Check if other files doesn't fires events.""" + + file1 = "file1.bak" + tmpfile = p(file1) + + with open(tmpfile, "w") as fp: + fp.write("init1") + + emitter = start_watching(path=tmpfile) + + # Don't wanted + with open(p("file2.bak"), "w") as fp: + fp.write("test2") + + # but this + with open(tmpfile, "w") as fp: + fp.write("test1") + + time.sleep(SLEEP) + + found_files = set() + try: + while event_queue.qsize(): + event, _ = event_queue.get() + if event.is_directory: + assert event.src_path == p() + else: + found_files.add(event.src_path) + assert event.src_path.endswith(file1) + finally: + emitter.stop() + + assert len(found_files) == 1, "Number of expected files differ. Wrong file catched." + + +def test_create_folder(p: P, start_watching: StartWatching, event_queue: TestEventQueue) -> None: + """Check if creation of a directory and inside files doesn't fires events.""" + + file1 = "file1.bak" + tmpfile = p(file1) + + with open(tmpfile, "w") as fp: + fp.write("init1") + + emitter = start_watching(path=tmpfile) + + # Don't wanted + mkdir(p("myfolder")) + with open(p("myfolder/file2.bak"), "w") as fp: + fp.write("test2") + + # but this + with open(tmpfile, "w") as fp: + fp.write("test1") + + time.sleep(SLEEP) + + found_files = set() + try: + while event_queue.qsize(): + event, _ = event_queue.get() + if event.is_directory: + assert event.src_path == p() + else: + found_files.add(event.src_path) + assert event.src_path.endswith(file1) + finally: + emitter.stop() + + assert len(found_files) == 1, "Number of expected files differ. Wrong file catched."