diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py index f302e126..d86a89a7 100644 --- a/ovos_utils/file_utils.py +++ b/ovos_utils/file_utils.py @@ -3,6 +3,7 @@ import os import re import tempfile +from threading import RLock from typing import Optional, List import time @@ -319,7 +320,10 @@ def __init__(self, files, callback, recursive=False, ignore_creation=False): self.observer = Observer() self.handlers = [] for file_path in files: - watch_dir = dirname(file_path) + if os.path.isfile(file_path): + watch_dir = dirname(file_path) + else: + watch_dir = file_path self.observer.schedule(FileEventHandler(file_path, callback, ignore_creation), watch_dir, recursive=recursive) self.observer.start() @@ -330,22 +334,38 @@ def shutdown(self): class FileEventHandler(FileSystemEventHandler): - def __init__(self, file_path, callback, ignore_creation=False): + def __init__(self, file_path: str, callback: callable, + ignore_creation: bool = False): + """ + Create a handler for file change events + @param file_path: file_path being watched Unused(?) + @param callback: function or method to call on file change + @param ignore_creation: if True, only track file modification events + """ super().__init__() self._callback = callback self._file_path = file_path - self._debounce = 1 - self._last_update = 0 if ignore_creation: self._events = ('modified') else: self._events = ('created', 'modified') + self._changed_files = [] + self._lock = RLock() def on_any_event(self, event): if event.is_directory: return - elif event.event_type in self._events: - if event.src_path == self._file_path: - if time.time() - self._last_update >= self._debounce: - self._callback(event.src_path) - self._last_update = time.time() + with self._lock: + if event.event_type == "closed": + if event.src_path in self._changed_files: + self._changed_files.remove(event.src_path) + # fire event, it is now safe + try: + self._callback(event.src_path) + except: + LOG.exception("An error occurred handling file " + "change event callback") + + elif event.event_type in self._events: + if event.src_path not in self._changed_files: + self._changed_files.append(event.src_path) diff --git a/test/unittests/test_file_utils.py b/test/unittests/test_file_utils.py index 787ec25f..65bd8c24 100644 --- a/test/unittests/test_file_utils.py +++ b/test/unittests/test_file_utils.py @@ -1,5 +1,6 @@ import unittest -from os.path import isdir, isfile +from os.path import isdir, join, dirname +from unittest.mock import Mock class TestFileUtils(unittest.TestCase): @@ -50,8 +51,48 @@ def test_read_translated_file(self): def test_filewatcher(self): from ovos_utils.file_utils import FileWatcher + test_file = join(dirname(__file__), "test.watch") # TODO def test_file_event_handler(self): from ovos_utils.file_utils import FileEventHandler - # TODO + from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileClosedEvent + test_file = join(dirname(__file__), "test.watch") + callback = Mock() + + # Test ignore creation callbacks + handler = FileEventHandler(test_file, callback, True) + handler.on_any_event(FileCreatedEvent(test_file)) + callback.assert_not_called() + + # Closed before modification (i.e. listener started while file open) + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_not_called() + + # Modified + handler.on_any_event(FileModifiedEvent(test_file)) + handler.on_any_event(FileModifiedEvent(test_file)) + callback.assert_not_called() + # Closed triggers callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() + # Second close won't trigger callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() + + # Test include creation callbacks + callback.reset_mock() + handler = FileEventHandler(test_file, callback, False) + handler.on_any_event(FileCreatedEvent(test_file)) + callback.assert_not_called() + + # Modified + handler.on_any_event(FileModifiedEvent(test_file)) + handler.on_any_event(FileModifiedEvent(test_file)) + callback.assert_not_called() + # Closed triggers callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() + # Second close won't trigger callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() \ No newline at end of file