Repository: gorakhargosh/watchdog
Pull request #814 (2021-07-09 13:13:11)

Event dispatch map
Small change to allow to add custom events to event handler map to the dispatcher in custom event handlers inheriting from  `FileSystemEventHandler` 


Patch:
diff --git a/.gitignore b/.gitignore
index fbf09c5..2f0ff09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,10 +26,11 @@ eggs/
 parts/
 MANIFEST
 
-# Project files for idea, eclipse, and netbeans
+# Project files for VS Code, idea, eclipse, and netbeans
 nbproject/
 .idea/
 .settings/
+.vscode/
 
 # Generated by tests.
 .coverage
@@ -42,3 +43,4 @@ htmlcov/
 include/
 lib/
 /bootstrap.py
+venv/
diff --git a/README.rst b/README.rst
index 8cc3554..9593a3a 100755
--- a/README.rst
+++ b/README.rst
@@ -127,7 +127,7 @@ Install from PyPI using ``pip``:
     $ python -m pip install -U watchdog
 
     # or to install the watchmedo utility:
-    $ python -m pip install -U watchdog[watchmedo]
+    $ python -m pip install -U "watchdog[watchmedo]"
 
 Install from source:
 
diff --git a/changelog.rst b/changelog.rst
index 21b8e57..d79e0db 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -8,8 +8,11 @@ Changelog
 
 2021-xx-x • `full history <https://github.com/gorakhargosh/watchdog/compare/v2.1.3...master>`__
 
-- [watchmedo] Fix usage of ``os.setsid()`` and ``os.killpg()`` Unix-only functions.  (`#809 <https://github.com/gorakhargosh/watchdog/pull/809>`_)
-- Thanks to our beloved contributors: @replabrobin, @BoboTiG
+- [watchmedo] Fix usage of ``os.setsid()`` and ``os.killpg()`` Unix-only functions. (`#809 <https://github.com/gorakhargosh/watchdog/pull/809>`_)
+- [mac] Fix missing ``FileModifiedEvent`` on permission or ownership changes of a file. (`#809 <https://github.com/gorakhargosh/watchdog/pull/814>`_)
+- [mac] Convert absolute watch path in ``FSEeventsEmitter`` with ``os.path.realpath()``. (`#822 <https://github.com/gorakhargosh/watchdog/pull/822>`_)
+- Fix a possible ``AttributeError`` in ``SkipRepeatsQueue._put()``. (`#818 <https://github.com/gorakhargosh/watchdog/pull/818>`_)
+- Thanks to our beloved contributors: @replabrobin, @BoboTiG, @SamSchott, @AndreiB97, @NiklasRosenstein
 
 2.1.3
 ~~~~~
diff --git a/src/watchdog/events.py b/src/watchdog/events.py
index 8b9bca8..2682366 100755
--- a/src/watchdog/events.py
+++ b/src/watchdog/events.py
@@ -260,6 +260,16 @@ class FileSystemEventHandler:
     Base file system event handler that you can override methods from.
     """
 
+    def __init__(self):
+        # Allow overriding or adding custom event handlers to event dispatch map.
+        self.event_dispatch_map = {
+            EVENT_TYPE_CREATED: self.on_created,
+            EVENT_TYPE_DELETED: self.on_deleted,
+            EVENT_TYPE_MODIFIED: self.on_modified,
+            EVENT_TYPE_MOVED: self.on_moved,
+            EVENT_TYPE_CLOSED: self.on_closed,
+        }
+
     def dispatch(self, event):
         """Dispatches events to the appropriate methods.
 
@@ -269,13 +279,7 @@ def dispatch(self, event):
             :class:`FileSystemEvent`
         """
         self.on_any_event(event)
-        {
-            EVENT_TYPE_CREATED: self.on_created,
-            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)
+        self.event_dispatch_map[event.event_type](event)
 
     def on_any_event(self, event):
         """Catch-all event handler.
diff --git a/src/watchdog/observers/fsevents.py b/src/watchdog/observers/fsevents.py
index 151f622..f1020e8 100644
--- a/src/watchdog/observers/fsevents.py
+++ b/src/watchdog/observers/fsevents.py
@@ -84,7 +84,7 @@ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT, suppress
         self._start_time = 0.0
         self._starting_state = None
         self._lock = threading.Lock()
-        self._absolute_watch_path = os.path.abspath(os.path.expanduser(self.watch.path))
+        self._absolute_watch_path = os.path.realpath(os.path.abspath(os.path.expanduser(self.watch.path)))
 
     def on_thread_stop(self):
         _fsevents.remove_watch(self.watch)
@@ -156,6 +156,11 @@ def _is_historic_created_event(self, event):
 
         return in_history or before_start
 
+    @staticmethod
+    def _is_meta_mod(event):
+        """Returns True if the event indicates a change in metadata."""
+        return event.is_inode_meta_mod or event.is_xattr_mod or event.is_owner_change
+
     def queue_events(self, timeout, events):
 
         if logger.getEffectiveLevel() <= logging.DEBUG:
@@ -209,7 +214,7 @@ def queue_events(self, timeout, events):
 
                 self._fs_view.add(event.inode)
 
-                if event.is_modified or event.is_inode_meta_mod or event.is_xattr_mod:
+                if event.is_modified or self._is_meta_mod(event):
                     self._queue_modified_event(event, src_path, src_dirname)
 
                 self._queue_deleted_event(event, src_path, src_dirname)
@@ -222,7 +227,7 @@ def queue_events(self, timeout, events):
 
                 self._fs_view.add(event.inode)
 
-                if event.is_modified or event.is_inode_meta_mod or event.is_xattr_mod:
+                if event.is_modified or self._is_meta_mod(event):
                     self._queue_modified_event(event, src_path, src_dirname)
 
                 if event.is_renamed:
@@ -247,7 +252,7 @@ def queue_events(self, timeout, events):
 
                         events.remove(dst_event)
 
-                        if dst_event.is_modified or dst_event.is_inode_meta_mod or dst_event.is_xattr_mod:
+                        if dst_event.is_modified or self._is_meta_mod(dst_event):
                             self._queue_modified_event(dst_event, dst_path, dst_dirname)
 
                         if dst_event.is_removed:
diff --git a/src/watchdog/utils/bricks.py b/src/watchdog/utils/bricks.py
index 35426b7..0a7e1cd 100644
--- a/src/watchdog/utils/bricks.py
+++ b/src/watchdog/utils/bricks.py
@@ -87,7 +87,7 @@ def _init(self, maxsize):
         self._last_item = None
 
     def _put(self, item):
-        if item != self._last_item:
+        if self._last_item is None or item != self._last_item:
             super()._put(item)
             self._last_item = item
         else:


Test patch:
diff --git a/tests/test_emitter.py b/tests/test_emitter.py
index 708a037..d7e19b3 100644
--- a/tests/test_emitter.py
+++ b/tests/test_emitter.py
@@ -15,6 +15,7 @@
 # limitations under the License.
 
 import os
+import stat
 import time
 import pytest
 import logging
@@ -195,6 +196,21 @@ def test_modify():
         assert isinstance(event, FileClosedEvent)
 
 
+@pytest.mark.flaky(max_runs=5, min_passes=1, rerun_filter=rerun_filter)
+def test_chmod():
+    mkfile(p('a'))
+    start_watching()
+
+    # Note: We use S_IREAD here because chmod on Windows only
+    # allows setting the read-only flag.
+    os.chmod(p('a'), stat.S_IREAD)
+
+    expect_event(FileModifiedEvent(p('a')))
+
+    # Reset permissions to allow cleanup.
+    os.chmod(p('a'), stat.S_IWRITE)
+
+
 @pytest.mark.flaky(max_runs=5, min_passes=1, rerun_filter=rerun_filter)
 def test_move():
     mkdir(p('dir1'))
diff --git a/tests/test_skip_repeats_queue.py b/tests/test_skip_repeats_queue.py
index be03a80..9954a4a 100644
--- a/tests/test_skip_repeats_queue.py
+++ b/tests/test_skip_repeats_queue.py
@@ -16,6 +16,7 @@
 # limitations under the License.
 
 import pytest
+import watchdog.events as events
 from watchdog.utils.bricks import SkipRepeatsQueue
 
 from .markers import cpython_only
@@ -58,6 +59,18 @@ def test_allow_nonconsecutive():
     assert q.empty()
 
 
+def test_put_with_watchdog_events():
+    # FileSystemEvent.__ne__() uses the key property without
+    # doing any type checking. Since _last_item is set to
+    # None in __init__(), an AttributeError is raised when
+    # FileSystemEvent.__ne__() tries to use None.key
+    queue = SkipRepeatsQueue()
+    dummy_file = 'dummy.txt'
+    event = events.FileCreatedEvent(dummy_file)
+    queue.put(event)
+    assert queue.get() is event
+
+
 def test_prevent_consecutive():
     q = SkipRepeatsQueue()