Repository: pallets/werkzeug

Issue:
WatchdogReloaderLoop reloading on every http request due to change in watchdog
(Note, I'm not sure if this is an issue in werkzeug or watchdog as I think this is an intentional change in behavior in watchdog)

The watchdog event handler `PatternMatchingEventHandler.on_any_event()` as of 2.3.0 now includes file opened events. This is causing the Werkzeug `WatchdogReloaderLoop` to reload whenever a file is opened. Flask, in debug mode, uses this reloader and as a result, the reloader has become a challenge to use as files are opened on each http request (generally).
(https://github.com/gorakhargosh/watchdog/commit/2b09f64b8b486b94f909146385418800de8841f8)

Note that in 2.3.1, watchdog started to ignore file open events, but only for `Trick` event handlers (https://github.com/gorakhargosh/watchdog/commit/25a2d1fec55fc32f97055ba827c8ce7b959acb34)

<!--
Describe how to replicate the bug.
-->
1. Launch a simple flask server in debug mode (FLASK_DEBUG=1)
2. Make http request
3. Server restarts

<!--
Describe the expected behavior that should have happened but didn't.
-->
Server should not restart on every http request when a watchdog reloader is being used.

Environment:

- Python version: 3.11.2
- Werkzeug version: 2.2.3
- Watchdog version: 2.3.1 and 2.3.0 (both)



Discussion:
Happy to review a PR that ignores these open events.

Resolving patch:
diff --git a/src/werkzeug/_reloader.py b/src/werkzeug/_reloader.py
--- a/src/werkzeug/_reloader.py
+++ b/src/werkzeug/_reloader.py
@@ -310,12 +310,17 @@ class WatchdogReloaderLoop(ReloaderLoop):
     def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
         from watchdog.observers import Observer
         from watchdog.events import PatternMatchingEventHandler
+        from watchdog.events import EVENT_TYPE_OPENED
+        from watchdog.events import FileModifiedEvent
 
         super().__init__(*args, **kwargs)
         trigger_reload = self.trigger_reload
 
         class EventHandler(PatternMatchingEventHandler):
-            def on_any_event(self, event):  # type: ignore
+            def on_any_event(self, event: FileModifiedEvent):  # type: ignore
+                if event.event_type == EVENT_TYPE_OPENED:
+                    return
+
                 trigger_reload(event.src_path)
 
         reloader_name = Observer.__name__.lower()


Test patch:
diff --git a/requirements/tests.txt b/requirements/tests.txt
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -40,5 +40,5 @@ pytest-xprocess==0.22.2
     # via -r requirements/tests.in
 tomli==2.0.1
     # via pytest
-watchdog==2.2.1
+watchdog==2.3.1
     # via -r requirements/tests.in
diff --git a/tests/test_serving.py b/tests/test_serving.py
--- a/tests/test_serving.py
+++ b/tests/test_serving.py
@@ -7,13 +7,18 @@
 import sys
 from io import BytesIO
 from pathlib import Path
+from unittest.mock import patch
 
 import pytest
+from watchdog.events import EVENT_TYPE_MODIFIED
+from watchdog.events import EVENT_TYPE_OPENED
+from watchdog.events import FileModifiedEvent
 
 from werkzeug import run_simple
 from werkzeug._reloader import _find_stat_paths
 from werkzeug._reloader import _find_watchdog_paths
 from werkzeug._reloader import _get_args_for_reloading
+from werkzeug._reloader import WatchdogReloaderLoop
 from werkzeug.datastructures import FileStorage
 from werkzeug.serving import make_ssl_devcert
 from werkzeug.test import stream_encode_multipart
@@ -115,6 +120,22 @@ def test_reloader_sys_path(tmp_path, dev_server, reloader_type):
     assert client.request().status == 200
 
 
+@patch.object(WatchdogReloaderLoop, "trigger_reload")
+def test_watchdog_reloader_ignores_opened(mock_trigger_reload):
+    reloader = WatchdogReloaderLoop()
+    modified_event = FileModifiedEvent("")
+    modified_event.event_type = EVENT_TYPE_MODIFIED
+    reloader.event_handler.on_any_event(modified_event)
+    mock_trigger_reload.assert_called_once()
+
+    reloader.trigger_reload.reset_mock()
+
+    opened_event = FileModifiedEvent("")
+    opened_event.event_type = EVENT_TYPE_OPENED
+    reloader.event_handler.on_any_event(opened_event)
+    reloader.trigger_reload.assert_not_called()
+
+
 @pytest.mark.skipif(sys.version_info >= (3, 10), reason="not needed on >= 3.10")
 def test_windows_get_args_for_reloading(monkeypatch, tmp_path):
     argv = [str(tmp_path / "test.exe"), "run"]