From 2b09f64b8b486b94f909146385418800de8841f8 Mon Sep 17 00:00:00 2001 From: dstaple Date: Tue, 24 Jan 2023 09:55:51 -0800 Subject: [PATCH] [inotify] Add support for ``IN_OPEN`` events via ``FileOpenedEvent`` events (#941) * Add FileOpenedEvent As per https://github.com/gorakhargosh/watchdog/issues/901 , this adds support to detect file open events on supported OSes. * Add file_opened_event in test_file_system_event_handler_dispatch * Get tests passing on OS X Ventura * Update changelog.rst * flake8: Fix long line in test_emitter.py Co-authored-by: Douglas Staple --- changelog.rst | 4 ++-- src/watchdog/events.py | 21 +++++++++++++++++++++ src/watchdog/observers/inotify.py | 4 ++++ src/watchdog/observers/inotify_c.py | 5 +++++ tests/test_emitter.py | 25 ++++++++++++++++++++++++- tests/test_events.py | 15 +++++++++++++++ 6 files changed, 71 insertions(+), 3 deletions(-) diff --git a/changelog.rst b/changelog.rst index 63cd366d7..2e81f8bad 100644 --- a/changelog.rst +++ b/changelog.rst @@ -8,8 +8,8 @@ Changelog 2023-xx-xx • `full history `__ -- -- Thanks to our beloved contributors: @BoboTiG +- [inotify] Add support for ``IN_OPEN`` events: a ``FileOpenedEvent`` event will be fired. (`#941 `__) +- Thanks to our beloved contributors: @BoboTiG, @dstaple 2.2.1 ~~~~~ diff --git a/src/watchdog/events.py b/src/watchdog/events.py index 5f9e92ba3..06a788a0b 100755 --- a/src/watchdog/events.py +++ b/src/watchdog/events.py @@ -56,6 +56,10 @@ :members: :show-inheritance: +.. autoclass:: FileOpenedEvent + :members: + :show-inheritance: + .. autoclass:: DirCreatedEvent :members: :show-inheritance: @@ -100,6 +104,7 @@ EVENT_TYPE_CREATED = 'created' EVENT_TYPE_MODIFIED = 'modified' EVENT_TYPE_CLOSED = 'closed' +EVENT_TYPE_OPENED = 'opened' class FileSystemEvent: @@ -223,6 +228,12 @@ class FileClosedEvent(FileSystemEvent): event_type = EVENT_TYPE_CLOSED +class FileOpenedEvent(FileSystemEvent): + """File system event representing file close on the file system.""" + + event_type = EVENT_TYPE_OPENED + + # Directory events. @@ -275,6 +286,7 @@ def dispatch(self, event): EVENT_TYPE_MODIFIED: self.on_modified, EVENT_TYPE_MOVED: self.on_moved, EVENT_TYPE_CLOSED: self.on_closed, + EVENT_TYPE_OPENED: self.on_opened, }[event.event_type](event) def on_any_event(self, event): @@ -331,6 +343,15 @@ def on_closed(self, event): :class:`FileClosedEvent` """ + def on_opened(self, event): + """Called when a file is opened. + + :param event: + Event representing file opening. + :type event: + :class:`FileOpenedEvent` + """ + class PatternMatchingEventHandler(FileSystemEventHandler): """ diff --git a/src/watchdog/observers/inotify.py b/src/watchdog/observers/inotify.py index 2918464b8..f212230e6 100644 --- a/src/watchdog/observers/inotify.py +++ b/src/watchdog/observers/inotify.py @@ -87,6 +87,7 @@ FileMovedEvent, FileCreatedEvent, FileClosedEvent, + FileOpenedEvent, generate_sub_moved_events, generate_sub_created_events, ) @@ -176,6 +177,9 @@ def queue_events(self, timeout, full_events=False): cls = FileClosedEvent self.queue_event(cls(src_path)) self.queue_event(DirModifiedEvent(os.path.dirname(src_path))) + elif event.is_open and not event.is_directory: + cls = FileOpenedEvent + self.queue_event(cls(src_path)) # elif event.is_close_nowrite and not event.is_directory: # cls = FileClosedEvent # self.queue_event(cls(src_path)) diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py index d942d2a0d..2aec0bfc1 100644 --- a/src/watchdog/observers/inotify_c.py +++ b/src/watchdog/observers/inotify_c.py @@ -108,6 +108,7 @@ class InotifyConstants: InotifyConstants.IN_DELETE_SELF, InotifyConstants.IN_DONT_FOLLOW, InotifyConstants.IN_CLOSE_WRITE, + InotifyConstants.IN_OPEN, ]) @@ -486,6 +487,10 @@ def is_close_write(self): def is_close_nowrite(self): return self._mask & InotifyConstants.IN_CLOSE_NOWRITE > 0 + @property + def is_open(self): + return self._mask & InotifyConstants.IN_OPEN > 0 + @property def is_access(self): return self._mask & InotifyConstants.IN_ACCESS > 0 diff --git a/tests/test_emitter.py b/tests/test_emitter.py index 6b71e1a42..51d4966e6 100644 --- a/tests/test_emitter.py +++ b/tests/test_emitter.py @@ -34,6 +34,7 @@ DirCreatedEvent, DirMovedEvent, FileClosedEvent, + FileOpenedEvent, ) from watchdog.observers.api import ObservedWatch @@ -122,6 +123,9 @@ def test_create(): expect_event(DirModifiedEvent(p())) if platform.is_linux(): + event = event_queue.get(timeout=5)[0] + assert event.src_path == p('a') + assert isinstance(event, FileOpenedEvent) event = event_queue.get(timeout=5)[0] assert event.src_path == p('a') assert isinstance(event, FileClosedEvent) @@ -188,6 +192,11 @@ def test_modify(): touch(p('a')) + if platform.is_linux(): + event = event_queue.get(timeout=5)[0] + assert event.src_path == p('a') + assert isinstance(event, FileOpenedEvent) + expect_event(FileModifiedEvent(p('a'))) if platform.is_linux(): @@ -429,6 +438,11 @@ def test_recursive_on(): assert event.src_path == p('dir1', 'dir2', 'dir3') assert isinstance(event, DirModifiedEvent) + if platform.is_linux(): + event = event_queue.get(timeout=5)[0] + assert event.src_path == p('dir1', 'dir2', 'dir3', 'a') + assert isinstance(event, FileOpenedEvent) + if not platform.is_bsd(): event = event_queue.get(timeout=5)[0] assert event.src_path == p('dir1', 'dir2', 'dir3', 'a') @@ -450,6 +464,7 @@ def test_recursive_off(): expect_event(DirModifiedEvent(p())) if platform.is_linux(): + expect_event(FileOpenedEvent(p('b'))) expect_event(FileClosedEvent(p('b'))) # currently limiting these additional events to macOS only, see https://github.com/gorakhargosh/watchdog/pull/779 @@ -505,7 +520,8 @@ def test_renaming_top_level_directory(): if event_queue.empty(): break - assert all([isinstance(e, (FileCreatedEvent, FileMovedEvent, DirModifiedEvent, FileClosedEvent)) for e in events]) + assert all([isinstance(e, (FileCreatedEvent, FileMovedEvent, FileOpenedEvent, DirModifiedEvent, FileClosedEvent)) + for e in events]) for event in events: if isinstance(event, FileCreatedEvent): @@ -595,6 +611,11 @@ def test_move_nested_subdirectories(): touch(p('dir2/dir3', 'a')) + if platform.is_linux(): + event = event_queue.get(timeout=5)[0] + assert event.src_path == p('dir2/dir3', 'a') + assert isinstance(event, FileOpenedEvent) + event = event_queue.get(timeout=5)[0] assert event.src_path == p('dir2/dir3', 'a') assert isinstance(event, FileModifiedEvent) @@ -658,8 +679,10 @@ def test_file_lifecyle(): expect_event(DirModifiedEvent(p())) if platform.is_linux(): + expect_event(FileOpenedEvent(p('a'))) expect_event(FileClosedEvent(p('a'))) expect_event(DirModifiedEvent(p())) + expect_event(FileOpenedEvent(p('a'))) expect_event(FileModifiedEvent(p('a'))) diff --git a/tests/test_events.py b/tests/test_events.py index 476f6ef62..e5b610441 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -20,6 +20,7 @@ FileModifiedEvent, FileCreatedEvent, FileClosedEvent, + FileOpenedEvent, DirDeletedEvent, DirModifiedEvent, DirCreatedEvent, @@ -31,6 +32,7 @@ EVENT_TYPE_DELETED, EVENT_TYPE_MOVED, EVENT_TYPE_CLOSED, + EVENT_TYPE_OPENED, ) path_1 = '/path/xyz' @@ -92,6 +94,14 @@ def test_file_closed_event(): assert not event.is_synthetic +def test_file_opened_event(): + event = FileOpenedEvent(path_1) + assert path_1 == event.src_path + assert EVENT_TYPE_OPENED == event.event_type + assert not event.is_directory + assert not event.is_synthetic + + def test_dir_deleted_event(): event = DirDeletedEvent(path_1) assert path_1 == event.src_path @@ -122,6 +132,7 @@ def test_file_system_event_handler_dispatch(): dir_cre_event = DirCreatedEvent('/path/blah.py') file_cre_event = FileCreatedEvent('/path/blah.txt') file_cls_event = FileClosedEvent('/path/blah.txt') + file_opened_event = FileOpenedEvent('/path/blah.txt') dir_mod_event = DirModifiedEvent('/path/blah.py') file_mod_event = FileModifiedEvent('/path/blah.txt') dir_mov_event = DirMovedEvent('/path/blah.py', '/path/blah') @@ -137,6 +148,7 @@ def test_file_system_event_handler_dispatch(): file_cre_event, file_mov_event, file_cls_event, + file_opened_event, ] class TestableEventHandler(FileSystemEventHandler): @@ -159,6 +171,9 @@ def on_created(self, event): def on_closed(self, event): assert event.event_type == EVENT_TYPE_CLOSED + def on_opened(self, event): + assert event.event_type == EVENT_TYPE_OPENED + handler = TestableEventHandler() for event in all_events: