Repository: gorakhargosh/watchdog

Issue:
Suppress watchmedo on_any_event log messages
When using watchmedo auto-restart how can I suppress all the event messages logged to the console? It's noise I do not want to see. 

Thanks.

Eg:
```shell
$ watchmedo auto-restart test.sh
on_any_event(self=<watchdog.tricks.AutoRestartTrick object at 0xb634ab30>, event=<FileCreatedEvent: src_path='./4913'>)
on_any_event(self=<watchdog.tricks.AutoRestartTrick object at 0xb634ab30>, event=<DirModifiedEvent: src_path='.'>)
on_any_event(self=<watchdog.tricks.AutoRestartTrick object at 0xb634ab30>, event=<FileModifiedEvent: src_path='./4913'>)
on_any_event(self=<watchdog.tricks.AutoRestartTrick object at 0xb634ab30>, event=<FileDeletedEvent: src_path='./4913'>)
on_any_event(self=<watchdog.tricks.AutoRestartTrick object at 0xb634ab30>, event=<DirModifiedEvent: src_path='.'>)
```

Discussion:
BoboTiG: Thanks @taleinat :)

When running `watchmedo` without `-q|-v`, I got:

```python
Traceback (most recent call last):
  File "/bin/watchmedo", line 33, in <module>
    sys.exit(load_entry_point('watchdog', 'console_scripts', 'watchmedo')())
  File "src/watchdog/watchmedo.py", line 650, in main
    verbosity = sum(args.verbosity)
TypeError: 'NoneType' object is not iterable
```

And would you mind adding some documentation in the README + a line in the changelog?
taleinat: Done as suggested, @BoboTiG.

Regarding adding to the README, it currently gives two examples and refers the reader to running `--help`, rather than including a comprehensive description of all command line options. Given that, I think expanding on verbosity there would require more rewriting than I think is prudent in this context.
BoboTiG: You are right :+1: 
BoboTiG: I am OK to merge if you are done.
taleinat: I'm done, so if you've reviewed the latest changes and are okay with them, let's merge :)
taleinat: Please wait before merging...

taleinat: I've made the code use a specific custom exception rather than raising Exception. Ready now as far as I'm concerned.
BoboTiG: Perfect! Thanks a lot :champagne: :confetti_ball: 

Patch:
diff --git a/changelog.rst b/changelog.rst
index 33f8edb..71765ad 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -13,6 +13,7 @@ Changelog
 - [watchmedo] Fix broken parsing of boolean arguments. (`#855 <https://github.com/gorakhargosh/watchdog/issues/855>`_)
 - [watchmedo] Fix broken parsing of commands from ``auto-restart``, and ``shell-command``. (`#855 <https://github.com/gorakhargosh/watchdog/issues/855>`_)
 - [inotify] Fix hang when unscheduling watch on a path in an unmounted filesystem. (`#869 <https://github.com/gorakhargosh/watchdog/pull/869>`_)
+- [watchmedo] Support setting verbosity level via ``-q/--quiet`` and ``-v/--verbose`` arguments. (`#889 <https://github.com/gorakhargosh/watchdog/pull/889>`_)
 - Thanks to our beloved contributors: @taleinat, @kianmeng, @palfrey, @IlayRosenberg, @BoboTiG
 
 2.1.7
diff --git a/src/watchdog/tricks/__init__.py b/src/watchdog/tricks/__init__.py
index 300d359..e15f970 100644
--- a/src/watchdog/tricks/__init__.py
+++ b/src/watchdog/tricks/__init__.py
@@ -41,6 +41,8 @@
 
 """
 
+import functools
+import logging
 import os
 import signal
 import subprocess
@@ -50,6 +52,10 @@
 from watchdog.events import PatternMatchingEventHandler
 
 
+logger = logging.getLogger(__name__)
+echo_events = functools.partial(echo.echo, write=lambda msg: logger.info(msg))
+
+
 class Trick(PatternMatchingEventHandler):
 
     """Your tricks should subclass this class."""
@@ -80,19 +86,19 @@ class LoggerTrick(Trick):
     def on_any_event(self, event):
         pass
 
-    @echo.echo
+    @echo_events
     def on_modified(self, event):
         pass
 
-    @echo.echo
+    @echo_events
     def on_deleted(self, event):
         pass
 
-    @echo.echo
+    @echo_events
     def on_created(self, event):
         pass
 
-    @echo.echo
+    @echo_events
     def on_moved(self, event):
         pass
 
@@ -202,7 +208,7 @@ def kill_process(stop_signal):
                     pass
         self.process = None
 
-    @echo.echo
+    @echo_events
     def on_any_event(self, event):
         self.stop()
         self.start()
diff --git a/src/watchdog/watchmedo.py b/src/watchdog/watchmedo.py
index da81b2d..20d0f77 100755
--- a/src/watchdog/watchmedo.py
+++ b/src/watchdog/watchmedo.py
@@ -60,7 +60,8 @@ def _split_lines(self, text, width):
         return text.splitlines()
 
 
-epilog = '''Copyright 2011 Yesudeep Mangalapilly <yesudeep@gmail.com>.
+epilog = '''\
+Copyright 2011 Yesudeep Mangalapilly <yesudeep@gmail.com>.
 Copyright 2012 Google, Inc & contributors.
 
 Licensed under the terms of the Apache license, version 2.0. Please see
@@ -69,6 +70,7 @@ def _split_lines(self, text, width):
 cli = ArgumentParser(epilog=epilog, formatter_class=HelpFormatter)
 cli.add_argument('--version', action='version', version=VERSION_STRING)
 subparsers = cli.add_subparsers(dest='top_command')
+command_parsers = {}
 
 
 def argument(*name_or_flags, **kwargs):
@@ -94,6 +96,12 @@ def decorator(func):
                                    description=desc,
                                    aliases=cmd_aliases,
                                    formatter_class=HelpFormatter)
+        command_parsers[name] = parser
+        verbosity_group = parser.add_mutually_exclusive_group()
+        verbosity_group.add_argument('-q', '--quiet', dest='verbosity',
+                                     action='append_const', const=-1)
+        verbosity_group.add_argument('-v', '--verbose', dest='verbosity',
+                                     action='append_const', const=1)
         for arg in args:
             parser.add_argument(*arg[0], **arg[1])
             parser.set_defaults(func=func)
@@ -397,7 +405,8 @@ def log(args):
     from watchdog.tricks import LoggerTrick
 
     if args.trace:
-        echo.echo_class(LoggerTrick)
+        class_module_logger = logging.getLogger(LoggerTrick.__module__)
+        echo.echo_class(LoggerTrick, write=lambda msg: class_module_logger.info(msg))
 
     patterns, ignore_patterns =\
         parse_patterns(args.patterns, args.ignore_patterns)
@@ -632,14 +641,37 @@ def handler_termination_signal(_signum, _frame):
         handler.stop()
 
 
+class LogLevelException(Exception):
+    pass
+
+
+def _get_log_level_from_args(args):
+    verbosity = sum(args.verbosity or [])
+    if verbosity < -1:
+        raise LogLevelException("-q/--quiet may be specified only once.")
+    if verbosity > 2:
+        raise LogLevelException("-v/--verbose may be specified up to 2 times.")
+    return ['ERROR', 'WARNING', 'INFO', 'DEBUG'][1 + verbosity]
+
+
 def main():
     """Entry-point function."""
     args = cli.parse_args()
     if args.top_command is None:
         cli.print_help()
-    else:
-        args.func(args)
+        return 1
+
+    try:
+        log_level = _get_log_level_from_args(args)
+    except LogLevelException as exc:
+        print("Error: " + exc.args[0], file=sys.stderr)
+        command_parsers[args.top_command].print_help()
+        return 1
+    logging.getLogger('watchdog').setLevel(log_level)
+
+    args.func(args)
+    return 0
 
 
 if __name__ == '__main__':
-    main()
+    sys.exit(main())


Test patch:
diff --git a/tests/test_0_watchmedo.py b/tests/test_0_watchmedo.py
index 2719122..3ab0ac9 100644
--- a/tests/test_0_watchmedo.py
+++ b/tests/test_0_watchmedo.py
@@ -96,6 +96,52 @@ def test_shell_command_arg_parsing():
     assert args.command == "'cmd'"
 
 
+@pytest.mark.parametrize("cmdline", [
+    ["auto-restart", "-d", ".", "cmd"],
+    ["log", "."]
+])
+@pytest.mark.parametrize("verbosity", [
+    ([], "WARNING"),
+    (["-q"], "ERROR"),
+    (["--quiet"], "ERROR"),
+    (["-v"], "INFO"),
+    (["--verbose"], "INFO"),
+    (["-vv"], "DEBUG"),
+    (["-v", "-v"], "DEBUG"),
+    (["--verbose", "-v"], "DEBUG"),
+])
+def test_valid_verbosity(cmdline, verbosity):
+    (verbosity_cmdline_args, expected_log_level) = verbosity
+    cmd = [cmdline[0], *verbosity_cmdline_args, *cmdline[1:]]
+    args = watchmedo.cli.parse_args(cmd)
+    log_level = watchmedo._get_log_level_from_args(args)
+    assert log_level == expected_log_level
+
+
+@pytest.mark.parametrize("cmdline", [
+    ["auto-restart", "-d", ".", "cmd"],
+    ["log", "."]
+])
+@pytest.mark.parametrize("verbosity_cmdline_args", [
+    ["-q", "-v"],
+    ["-v", "-q"],
+    ["-qq"],
+    ["-q", "-q"],
+    ["--quiet", "--quiet"],
+    ["--quiet", "-q"],
+    ["-vvv"],
+    ["-vvvv"],
+    ["-v", "-v", "-v"],
+    ["-vv", "-v"],
+    ["--verbose", "-vv"],
+])
+def test_invalid_verbosity(cmdline, verbosity_cmdline_args):
+    cmd = [cmdline[0], *verbosity_cmdline_args, *cmdline[1:]]
+    with pytest.raises((watchmedo.LogLevelException, SystemExit)):
+        args = watchmedo.cli.parse_args(cmd)
+        watchmedo._get_log_level_from_args(args)
+
+
 @pytest.mark.parametrize("command", ["tricks-from", "tricks"])
 def test_tricks_from_file(command, tmp_path):
     tricks_file = tmp_path / "tricks.yaml"