Fix usage of `--debug`, `--verbose` and `--quiet` options
Marco Ricci

Marco Ricci commited on 2024-12-13 14:25:44
Zeige 3 geänderte Dateien mit 50 Einfügungen und 70 Löschungen.


The three "verbosity" options `--debug`, `--verbose` and `--quiet` set
the logging level for `derivepassphrase`, globally.  Because `click`
does not support mutually exclusive options by default, these options
are implemented as boolean flags, and it is possible to override earlier
verbosity settings with later options.

The previous setup treated the three options as disjointed flags, so
each callback was called once, irrespective of the others, so the
aforementioned intended overriding behavior was not working properly.
In addition, the callback was faulty, always enabling each of the flags.

The new setup now links all three options to the same flag, with
different flag values.  We further expose the option objects at the top
level, and unify the three callback functions to a single one.

As a side effect, we also fix a misclassified diagnostic during
configuration file migration ("vault" derivation scheme).
... ...
@@ -343,53 +343,25 @@ P = ParamSpec('P')
343 343
 R = TypeVar('R')
344 344
 
345 345
 
346
-def log_debug(
346
+def adjust_logging_level(
347 347
     ctx: click.Context,
348 348
     /,
349 349
     param: click.Parameter | None = None,
350
-    value: Any = None,  # noqa: ANN401
350
+    value: int | None = None,
351 351
 ) -> None:
352
-    """Request that DEBUG-level logs be emitted to standard error.
352
+    """Change the logs that are emitted to standard error.
353 353
 
354 354
     This modifies the [`StandardCLILogging`][] settings such that log
355
-    records at level [`logging.DEBUG`][] and [`logging.INFO`][] are
356
-    emitted as well.
355
+    records at the respective level are emitted, based on the `param`
356
+    and the `value`.
357 357
 
358 358
     """
359
-    del ctx, param, value
360
-    StandardCLILogging.cli_handler.setLevel(logging.DEBUG)
361
-
362
-
363
-def log_info(
364
-    ctx: click.Context,
365
-    /,
366
-    param: click.Parameter | None = None,
367
-    value: Any = None,  # noqa: ANN401
368
-) -> None:
369
-    """Request that INFO-level logs be emitted to standard error.
370
-
371
-    This modifies the [`StandardCLILogging`][] settings such that log
372
-    records at level [`logging.INFO`][] are emitted as well.
373
-
374
-    """
375
-    del ctx, param, value
376
-    StandardCLILogging.cli_handler.setLevel(logging.INFO)
377
-
378
-
379
-def silence_warnings(
380
-    ctx: click.Context,
381
-    /,
382
-    param: click.Parameter | None = None,
383
-    value: Any = None,  # noqa: ANN401
384
-) -> None:
385
-    """Request that WARNING-level logs not be emitted to standard error.
386
-
387
-    This modifies the [`StandardCLILogging`][] settings such that log
388
-    records at level [`logging.WARNING`][] and below are *not* emitted.
389
-
390
-    """
391
-    del ctx, param, value
392
-    StandardCLILogging.cli_handler.setLevel(logging.ERROR)
359
+    # Note: If multiple options use this callback, then we will be
360
+    # called multiple times.  Ensure the runs are idempotent.
361
+    if param is None or value is None or ctx.resilient_parsing:
362
+        return
363
+    StandardCLILogging.cli_handler.setLevel(value)
364
+    logging.getLogger(StandardCLILogging.package_name).setLevel(value)
393 365
 
394 366
 
395 367
 # Option parsing and grouping
... ...
@@ -498,50 +470,55 @@ class LoggingOption(OptionGroupOption):
498 470
     epilog = ''
499 471
 
500 472
 
501
-def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:
502
-    """Decorate the function with standard logging click options.
503
-
504
-    Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and
505
-    `--debug`, which issue callbacks to the [`log_info`][],
506
-    [`silence_warnings`][] and [`log_debug`][] functions, respectively.
507
-
508
-    Args:
509
-        f: A callable to decorate.
510
-
511
-    Returns:
512
-        The decorated callable.
513
-
514
-    """
515
-    dec1 = click.option(
516
-        '-q',
517
-        '--quiet',
473
+debug_option = click.option(
474
+    '--debug',
475
+    'logging_level',
518 476
     is_flag=True,
519
-        is_eager=True,
477
+    flag_value=logging.DEBUG,
520 478
     expose_value=False,
521
-        callback=silence_warnings,
522
-        help='suppress even warnings, emit only errors',
479
+    callback=adjust_logging_level,
480
+    help='also emit debug information (implies --verbose)',
523 481
     cls=LoggingOption,
524 482
 )
525
-    dec2 = click.option(
483
+verbose_option = click.option(
526 484
     '-v',
527 485
     '--verbose',
486
+    'logging_level',
528 487
     is_flag=True,
529
-        is_eager=True,
488
+    flag_value=logging.INFO,
530 489
     expose_value=False,
531
-        callback=log_info,
490
+    callback=adjust_logging_level,
532 491
     help='emit extra/progress information to standard error',
533 492
     cls=LoggingOption,
534 493
 )
535
-    dec3 = click.option(
536
-        '--debug',
494
+quiet_option = click.option(
495
+    '-q',
496
+    '--quiet',
497
+    'logging_level',
537 498
     is_flag=True,
538
-        is_eager=True,
499
+    flag_value=logging.ERROR,
539 500
     expose_value=False,
540
-        callback=log_debug,
541
-        help='also emit debug information (implies --verbose)',
501
+    callback=adjust_logging_level,
502
+    help='suppress even warnings, emit only errors',
542 503
     cls=LoggingOption,
543 504
 )
544
-    return dec1(dec2(dec3(f)))
505
+
506
+
507
+def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:
508
+    """Decorate the function with standard logging click options.
509
+
510
+    Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and
511
+    `--debug`, which issue callbacks to the [`log_info`][],
512
+    [`silence_warnings`][] and [`log_debug`][] functions, respectively.
513
+
514
+    Args:
515
+        f: A callable to decorate.
516
+
517
+    Returns:
518
+        The decorated callable.
519
+
520
+    """
521
+    return debug_option(verbose_option(quiet_option(f)))
545 522
 
546 523
 
547 524
 # Top-level
... ...
@@ -1754,7 +1731,7 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
1754 1731
                     exc.filename,
1755 1732
                 )
1756 1733
             else:
1757
-                logger.info('Successfully migrated to %r.', new_name)
1734
+                deprecation.info('Successfully migrated to %r.', new_name)
1758 1735
             return backup_config
1759 1736
         except OSError as e:
1760 1737
             err('Cannot load config: %s: %r', e.strerror, e.filename)
... ...
@@ -1713,4 +1713,7 @@ warning_emitted = message_emitted_factory(logging.WARNING)
1713 1713
 deprecation_warning_emitted = message_emitted_factory(
1714 1714
     logging.WARNING, logger_name=f'{cli.PROG_NAME}.deprecation'
1715 1715
 )
1716
+deprecation_info_emitted = message_emitted_factory(
1717
+    logging.INFO, logger_name=f'{cli.PROG_NAME}.deprecation'
1718
+)
1716 1719
 error_emitted = message_emitted_factory(logging.ERROR)
... ...
@@ -2279,7 +2279,7 @@ class TestCLITransition:
2279 2279
         assert tests.deprecation_warning_emitted(
2280 2280
             'v0.1-style config file', caplog.record_tuples
2281 2281
         ), 'expected known warning message in stderr'
2282
-        assert tests.info_emitted(
2282
+        assert tests.deprecation_info_emitted(
2283 2283
             'Successfully migrated to ', caplog.record_tuples
2284 2284
         ), 'expected known warning message in stderr'
2285 2285
 
2286 2286