Repository: gorakhargosh/watchdog

Issue:
Modifed event triggered twice
if I copy and paste a file on OSX lion
it triggered created event once and modified event twice
should it be just trigger once modified event?

`
INFO:root:Created file: /Users/ouyangjichao/BoxSync/rename/testwatchdog.py
INFO:root:Modified file: /Users/ouyangjichao/BoxSync/rename/testwatchdog.py
INFO:root:Modified file: /Users/ouyangjichao/BoxSync/rename/testwatchdog.py
`


Comments:

Patch:
diff --git a/changelog.rst b/changelog.rst
index 167f5cb..52943e5 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -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
diff --git a/src/watchdog/events.py b/src/watchdog/events.py
index 73e82e8..8b9bca8 100755
--- a/src/watchdog/events.py
+++ b/src/watchdog/events.py
@@ -52,6 +52,10 @@ Event Classes
    :members:
    :show-inheritance:
 
+.. autoclass:: FileClosedEvent
+   :members:
+   :show-inheritance:
+
 .. autoclass:: DirCreatedEvent
    :members:
    :show-inheritance:
@@ -95,6 +99,7 @@ EVENT_TYPE_MOVED = 'moved'
 EVENT_TYPE_DELETED = 'deleted'
 EVENT_TYPE_CREATED = 'created'
 EVENT_TYPE_MODIFIED = 'modified'
+EVENT_TYPE_CLOSED = 'closed'
 
 
 class FileSystemEvent:
@@ -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.
 
 
@@ -263,6 +274,7 @@ class FileSystemEventHandler:
             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):
@@ -310,6 +322,15 @@ class FileSystemEventHandler:
             :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):
     """
diff --git a/src/watchdog/observers/inotify.py b/src/watchdog/observers/inotify.py
index 5cb8c5d..7c5d9c6 100644
--- a/src/watchdog/observers/inotify.py
+++ b/src/watchdog/observers/inotify.py
@@ -86,6 +86,7 @@ from watchdog.events import (
     FileModifiedEvent,
     FileMovedEvent,
     FileCreatedEvent,
+    FileClosedEvent,
     generate_sub_moved_events,
     generate_sub_created_events,
 )
@@ -170,6 +171,13 @@ class InotifyEmitter(EventEmitter):
                 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()
diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py
index 61452dd..8925147 100644
--- a/src/watchdog/observers/inotify_c.py
+++ b/src/watchdog/observers/inotify_c.py
@@ -139,6 +139,7 @@ WATCHDOG_ALL_EVENTS = reduce(
         InotifyConstants.IN_DELETE,
         InotifyConstants.IN_DELETE_SELF,
         InotifyConstants.IN_DONT_FOLLOW,
+        InotifyConstants.IN_CLOSE_WRITE,
     ])
 
 


Test patch:
diff --git a/tests/test_emitter.py b/tests/test_emitter.py
index 6df98ae..4fd0506 100644
--- a/tests/test_emitter.py
+++ b/tests/test_emitter.py
@@ -31,7 +31,8 @@ from watchdog.events import (
     DirDeletedEvent,
     DirModifiedEvent,
     DirCreatedEvent,
-    DirMovedEvent
+    DirMovedEvent,
+    FileClosedEvent,
 )
 from watchdog.observers.api import ObservedWatch
 
@@ -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(
@@ -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():
@@ -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):
diff --git a/tests/test_events.py b/tests/test_events.py
index f41a3f8..476f6ef 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -19,6 +19,7 @@ from watchdog.events import (
     FileDeletedEvent,
     FileModifiedEvent,
     FileCreatedEvent,
+    FileClosedEvent,
     DirDeletedEvent,
     DirModifiedEvent,
     DirCreatedEvent,
@@ -29,6 +30,7 @@ from watchdog.events import (
     EVENT_TYPE_CREATED,
     EVENT_TYPE_DELETED,
     EVENT_TYPE_MOVED,
+    EVENT_TYPE_CLOSED,
 )
 
 path_1 = '/path/xyz'
@@ -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
@@ -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')
@@ -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):
@@ -144,6 +156,9 @@ def test_file_system_event_handler_dispatch():
         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: