Repository: gorakhargosh/watchdog (python)

Issue:
watchmedo auto-restart behavior change with 2.1.9
We were using `watchmedo auto-restart` to call mypy and some flake8 tooling upon file changes. With the changes introduced by https://github.com/gorakhargosh/watchdog/pull/896, these processes re-run continually every time they exit whereas before they were re-ran upon a watched file/directory change.

The name `auto-restart` makes me believe that this is actually desirable behavior and perhaps we were misusing that sub-command, however there doesn't appear to be an alternative to get the behavior we once had. `watchmedo shell-command` only runs the command on change with no option to run it once _and also_ upon watched file change.

Is there a way to add a flag to `auto-restart` which only restarts on an exit code, or maybe a way to use `shell-command` to call the command initially and then repeat on a file change?

Pull request description:
[watchmedo] Add option to not auto-restart the command after it exits.
This adds a new `restart_on_command_exit` argument to `AutoRestartTrick`, and a new `--no-restart-on-command-exit` flag to `watchmedo aut-restart`.

Both the CLI and the trick still default to restarting the command if it exits.

Closes #922.

Patch:
diff --git a/changelog.rst b/changelog.rst
index f182e85..e7b5a01 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -11,6 +11,7 @@ Changelog
 - [inotify] Add support for ``IN_OPEN`` events: a ``FileOpenedEvent`` event will be fired. (`#941 <https://github.com/gorakhargosh/watchdog/pull/941>`__)
 - [watchmedo] Add optional event debouncing for ``auto-restart``, only restarting once if many events happen in quick succession (`#940 <https://github.com/gorakhargosh/watchdog/pull/940>`__)
 - [watchmedo] Exit gracefully on ``KeyboardInterrupt`` exception (Ctrl+C) (`#945 <https://github.com/gorakhargosh/watchdog/pull/945>`__)
+- [watchmedo] Add option to not auto-restart the command after it exits. (`#946 <https://github.com/gorakhargosh/watchdog/pull/946>`__)
 - Thanks to our beloved contributors: @BoboTiG, @dstaple, @taleinat
 
 2.2.1
diff --git a/src/watchdog/tricks/__init__.py b/src/watchdog/tricks/__init__.py
index e2ceace..51e8eb2 100644
--- a/src/watchdog/tricks/__init__.py
+++ b/src/watchdog/tricks/__init__.py
@@ -179,7 +179,8 @@ class AutoRestartTrick(Trick):
 
     def __init__(self, command, patterns=None, ignore_patterns=None,
                  ignore_directories=False, stop_signal=signal.SIGINT,
-                 kill_after=10, debounce_interval_seconds=0):
+                 kill_after=10, debounce_interval_seconds=0,
+                 restart_on_command_exit=True):
         if kill_after < 0:
             raise ValueError("kill_after must be non-negative.")
         if debounce_interval_seconds < 0:
@@ -193,6 +194,7 @@ def __init__(self, command, patterns=None, ignore_patterns=None,
         self.stop_signal = stop_signal
         self.kill_after = kill_after
         self.debounce_interval_seconds = debounce_interval_seconds
+        self.restart_on_command_exit = restart_on_command_exit
 
         self.process = None
         self.process_watcher = None
@@ -227,7 +229,8 @@ def stop(self):
         # Don't leak threads: Wait for background threads to stop.
         if self.event_debouncer is not None:
             self.event_debouncer.join()
-        process_watcher.join()
+        if process_watcher is not None:
+            process_watcher.join()
 
     def _start_process(self):
         if self._is_trick_stopping:
@@ -235,8 +238,9 @@ def _start_process(self):
 
         # windows doesn't have setsid
         self.process = subprocess.Popen(self.command, preexec_fn=getattr(os, 'setsid', None))
-        self.process_watcher = ProcessWatcher(self.process, self._restart_process)
-        self.process_watcher.start()
+        if self.restart_on_command_exit:
+            self.process_watcher = ProcessWatcher(self.process, self._restart_process)
+            self.process_watcher.start()
 
     def _stop_process(self):
         # Ensure the body of the function is not run in parallel in different threads.
diff --git a/src/watchdog/watchmedo.py b/src/watchdog/watchmedo.py
index 375365c..faa5f43 100755
--- a/src/watchdog/watchmedo.py
+++ b/src/watchdog/watchmedo.py
@@ -591,7 +591,12 @@ def shell_command(args):
                    default=0.0,
                    type=float,
                    help='After a file change, Wait until the specified interval (in '
-                   'seconds) passes with no file changes, and only then restart.')])
+                   'seconds) passes with no file changes, and only then restart.'),
+          argument('--no-restart-on-command-exit',
+                   dest='restart_on_command_exit',
+                   default=True,
+                   action='store_false',
+                   help="Don't auto-restart the command after it exits.")])
 def auto_restart(args):
     """
     Command to start a long-running subprocess and restart it on matched events.
@@ -640,7 +645,8 @@ def handler_termination_signal(_signum, _frame):
                                ignore_directories=args.ignore_directories,
                                stop_signal=stop_signal,
                                kill_after=args.kill_after,
-                               debounce_interval_seconds=args.debounce_interval)
+                               debounce_interval_seconds=args.debounce_interval,
+                               restart_on_command_exit=args.restart_on_command_exit)
     handler.start()
     observer = Observer(timeout=args.timeout)
     try:


Test patch:
diff --git a/tests/test_0_watchmedo.py b/tests/test_0_watchmedo.py
index 224cd55..d858287 100644
--- a/tests/test_0_watchmedo.py
+++ b/tests/test_0_watchmedo.py
@@ -153,7 +153,8 @@ def test_auto_restart_on_file_change_debounce(tmpdir, capfd):
     assert trick.restart_count == 2
 
 
-def test_auto_restart_subprocess_termination(tmpdir, capfd):
+@pytest.mark.parametrize("restart_on_command_exit", [True, False])
+def test_auto_restart_subprocess_termination(tmpdir, capfd, restart_on_command_exit):
     """Run auto-restart with a script that terminates in about 2 seconds.
 
     After 5 seconds, expect it to have been restarted at least once.
@@ -162,13 +163,17 @@ def test_auto_restart_subprocess_termination(tmpdir, capfd):
     import sys
     import time
     script = make_dummy_script(tmpdir, n=2)
-    trick = AutoRestartTrick([sys.executable, script])
+    trick = AutoRestartTrick([sys.executable, script], restart_on_command_exit=restart_on_command_exit)
     trick.start()
     time.sleep(5)
     trick.stop()
     cap = capfd.readouterr()
-    assert cap.out.splitlines(keepends=False).count('+++++ 0') > 1
-    assert trick.restart_count >= 1
+    if restart_on_command_exit:
+        assert cap.out.splitlines(keepends=False).count('+++++ 0') > 1
+        assert trick.restart_count >= 1
+    else:
+        assert cap.out.splitlines(keepends=False).count('+++++ 0') == 1
+        assert trick.restart_count == 0
 
 
 def test_auto_restart_arg_parsing_basic():