From 9a5df9595bc79faba14ff734f18a0a0f85c0b5a6 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 15 May 2022 12:30:10 +0300 Subject: [PATCH] [watchmedo] Support setting output verbosity with `-q, --quiet`, and `-v, --verbose` (#889) * support setting output verbosity This changes the internal use of echo to use a logger instead of sys.stdout.write. This uses the stdlib logging library, with the common convention of logger = logging.getLogger(__name__), so that all watchdog logs are under the "watchdog" logging context. Finally, this adds -q/--quiet and -v/--verbose command line options to watchmedo. * fix handling no verbosity flags and add tests * add a changelog entry * fix: add missing adding to command_parsers * use a custom exception class for log level "parsing" --- changelog.rst | 1 + src/watchdog/tricks/__init__.py | 16 ++++++++---- src/watchdog/watchmedo.py | 42 ++++++++++++++++++++++++++---- tests/test_0_watchmedo.py | 46 +++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 10 deletions(-) diff --git a/changelog.rst b/changelog.rst index 33f8edb2..71765ad1 100644 --- a/changelog.rst +++ b/changelog.rst @@ -13,6 +13,7 @@ Changelog - [watchmedo] Fix broken parsing of boolean arguments. (`#855 `_) - [watchmedo] Fix broken parsing of commands from ``auto-restart``, and ``shell-command``. (`#855 `_) - [inotify] Fix hang when unscheduling watch on a path in an unmounted filesystem. (`#869 `_) +- [watchmedo] Support setting verbosity level via ``-q/--quiet`` and ``-v/--verbose`` arguments. (`#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 300d3597..e15f970c 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 da81b2d3..20d0f772 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 . +epilog = '''\ +Copyright 2011 Yesudeep Mangalapilly . 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()) diff --git a/tests/test_0_watchmedo.py b/tests/test_0_watchmedo.py index 2719122f..3ab0ac96 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"