Skip to content

Commit

Permalink
[inotify] Add support for the IN_CLOSE_WRITE event (#747)
Browse files Browse the repository at this point in the history
* add: inotify file close events

* fix: NOWRITE_CLOSE doesn't modify dir

* update changelog & version

* add: test events

* Revert "update changelog & version"

This reverts commit 69d61d0.

* update changelog

* [gnu/linux] Fix ability to catch IN_CLOSE_WRITE, IN_CLOSE_NOWRITE events #690

* [gnu/linux] Test IN_CLOSE_WRITE, IN_CLOSE_NOWRITE events

* Fix typos

* [gnu/linux] Fix test due to IN_CLOSE_NOWRITE event

* [gnu/linux] Revert IN_CLOSE_NOWRITE support #747

* [gnu/linux] Extend test for IN_CLOSE_WRITE support #747

* fix typos

* Update changelog #747, #690

Co-authored-by: Lukas Šupienis <l.supienis@ncs.lt>
Co-authored-by: Lukas Šupienis <lukas.supienis@gmail.com>
  • Loading branch information
3 people committed Jan 15, 2021
1 parent 847a059 commit 2fab7c2
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 3 deletions.
3 changes: 2 additions & 1 deletion changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Changelog
202x-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v1.0.2...master>`__

- Avoid deprecated ``PyEval_InitThreads`` on Python 3.7+ (`#746 <https://github.com/gorakhargosh/watchdog/pull/746>`_)
- Thanks to our beloved contributors: @bstaletic
- [inotify] Add support for ``IN_CLOSE_WRITE`` events. A ``FileCloseEvent`` event will be fired. Note that ``IN_CLOSE_NOWRITE`` events are not handled to prevent much noise. (`#184 <https://github.com/gorakhargosh/watchdog/pull/184>`_, `#245 <https://github.com/gorakhargosh/watchdog/pull/245>`_, `#280 <https://github.com/gorakhargosh/watchdog/pull/280>`_, `#313 <https://github.com/gorakhargosh/watchdog/pull/313>`_, `#690 <https://github.com/gorakhargosh/watchdog/pull/690>`_)
- Thanks to our beloved contributors: @bstaletic, @lukassup, @ysard


1.0.2
Expand Down
21 changes: 21 additions & 0 deletions src/watchdog/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
:members:
:show-inheritance:
.. autoclass:: FileClosedEvent
:members:
:show-inheritance:
.. autoclass:: DirCreatedEvent
:members:
:show-inheritance:
Expand Down Expand Up @@ -95,6 +99,7 @@
EVENT_TYPE_DELETED = 'deleted'
EVENT_TYPE_CREATED = 'created'
EVENT_TYPE_MODIFIED = 'modified'
EVENT_TYPE_CLOSED = 'closed'


class FileSystemEvent:
Expand Down Expand Up @@ -212,6 +217,12 @@ class FileMovedEvent(FileSystemMovedEvent):
"""File system event representing file movement on the file system."""


class FileClosedEvent(FileSystemEvent):
"""File system event representing file close on the file system."""

event_type = EVENT_TYPE_CLOSED


# Directory events.


Expand Down Expand Up @@ -263,6 +274,7 @@ def dispatch(self, event):
EVENT_TYPE_DELETED: self.on_deleted,
EVENT_TYPE_MODIFIED: self.on_modified,
EVENT_TYPE_MOVED: self.on_moved,
EVENT_TYPE_CLOSED: self.on_closed,
}[event.event_type](event)

def on_any_event(self, event):
Expand Down Expand Up @@ -310,6 +322,15 @@ def on_modified(self, event):
:class:`DirModifiedEvent` or :class:`FileModifiedEvent`
"""

def on_closed(self, event):
"""Called when a file opened for writing is closed.
:param event:
Event representing file closing.
:type event:
:class:`FileClosedEvent`
"""


class PatternMatchingEventHandler(FileSystemEventHandler):
"""
Expand Down
8 changes: 8 additions & 0 deletions src/watchdog/observers/inotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
FileModifiedEvent,
FileMovedEvent,
FileCreatedEvent,
FileClosedEvent,
generate_sub_moved_events,
generate_sub_created_events,
)
Expand Down Expand Up @@ -170,6 +171,13 @@ def queue_events(self, timeout, full_events=False):
cls = DirCreatedEvent if event.is_directory else FileCreatedEvent
self.queue_event(cls(src_path))
self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
elif event.is_close_write and not event.is_directory:
cls = FileClosedEvent
self.queue_event(cls(src_path))
self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
# elif event.is_close_nowrite and not event.is_directory:
# cls = FileClosedEvent
# self.queue_event(cls(src_path))
elif event.is_delete_self and src_path == self.watch.path:
self.queue_event(DirDeletedEvent(src_path))
self.stop()
Expand Down
1 change: 1 addition & 0 deletions src/watchdog/observers/inotify_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ class InotifyConstants:
InotifyConstants.IN_DELETE,
InotifyConstants.IN_DELETE_SELF,
InotifyConstants.IN_DONT_FOLLOW,
InotifyConstants.IN_CLOSE_WRITE,
])


Expand Down
37 changes: 35 additions & 2 deletions tests/test_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
DirDeletedEvent,
DirModifiedEvent,
DirCreatedEvent,
DirMovedEvent
DirMovedEvent,
FileClosedEvent,
)
from watchdog.observers.api import ObservedWatch

Expand Down Expand Up @@ -107,6 +108,33 @@ def test_create():
assert os.path.normpath(event.src_path) == os.path.normpath(p(''))
assert isinstance(event, DirModifiedEvent)

if platform.is_linux():
event = event_queue.get(timeout=5)[0]
assert event.src_path == p('a')
assert isinstance(event, FileClosedEvent)


@pytest.mark.skipif(not platform.is_linux(), reason="FileCloseEvent only supported in GNU/Linux")
@pytest.mark.flaky(max_runs=5, min_passes=1, rerun_filter=rerun_filter)
def test_close():
f_d = open(p('a'), 'a')
start_watching()
f_d.close()

# After file creation/open in append mode
event = event_queue.get(timeout=5)[0]
assert event.src_path == p('a')
assert isinstance(event, FileClosedEvent)

event = event_queue.get(timeout=5)[0]
assert os.path.normpath(event.src_path) == os.path.normpath(p(''))
assert isinstance(event, DirModifiedEvent)

# After read-only, only IN_CLOSE_NOWRITE is emitted but not catched for now #747
open(p('a'), 'r').close()

assert event_queue.empty()


@pytest.mark.flaky(max_runs=5, min_passes=1, rerun_filter=rerun_filter)
@pytest.mark.skipif(
Expand Down Expand Up @@ -153,6 +181,11 @@ def test_modify():
assert event.src_path == p('a')
assert isinstance(event, FileModifiedEvent)

if platform.is_linux():
event = event_queue.get(timeout=5)[0]
assert event.src_path == p('a')
assert isinstance(event, FileClosedEvent)


@pytest.mark.flaky(max_runs=5, min_passes=1, rerun_filter=rerun_filter)
def test_move():
Expand Down Expand Up @@ -423,7 +456,7 @@ def test_renaming_top_level_directory():
if event_queue.empty():
break

assert all([isinstance(e, (FileCreatedEvent, FileMovedEvent, DirModifiedEvent)) for e in events])
assert all([isinstance(e, (FileCreatedEvent, FileMovedEvent, DirModifiedEvent, FileClosedEvent)) for e in events])

for event in events:
if isinstance(event, FileCreatedEvent):
Expand Down
15 changes: 15 additions & 0 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
FileDeletedEvent,
FileModifiedEvent,
FileCreatedEvent,
FileClosedEvent,
DirDeletedEvent,
DirModifiedEvent,
DirCreatedEvent,
Expand All @@ -29,6 +30,7 @@
EVENT_TYPE_CREATED,
EVENT_TYPE_DELETED,
EVENT_TYPE_MOVED,
EVENT_TYPE_CLOSED,
)

path_1 = '/path/xyz'
Expand Down Expand Up @@ -82,6 +84,14 @@ def test_file_moved_event():
assert not event.is_synthetic


def test_file_closed_event():
event = FileClosedEvent(path_1)
assert path_1 == event.src_path
assert EVENT_TYPE_CLOSED == 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
Expand Down Expand Up @@ -111,6 +121,7 @@ def test_file_system_event_handler_dispatch():
file_del_event = FileDeletedEvent('/path/blah.txt')
dir_cre_event = DirCreatedEvent('/path/blah.py')
file_cre_event = FileCreatedEvent('/path/blah.txt')
file_cls_event = FileClosedEvent('/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')
Expand All @@ -125,6 +136,7 @@ def test_file_system_event_handler_dispatch():
file_del_event,
file_cre_event,
file_mov_event,
file_cls_event,
]

class TestableEventHandler(FileSystemEventHandler):
Expand All @@ -144,6 +156,9 @@ def on_moved(self, event):
def on_created(self, event):
assert event.event_type == EVENT_TYPE_CREATED

def on_closed(self, event):
assert event.event_type == EVENT_TYPE_CLOSED

handler = TestableEventHandler()

for event in all_events:
Expand Down

0 comments on commit 2fab7c2

Please sign in to comment.