diff options
| author | Dennis Fink | 2026-09-19 17:27:56 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-09-19 17:27:56 +0200 |
| commit | 3a52d4cf01e4443aba1b9f78152f673330274295 (patch) | |
| tree | c0655285a2ebebd59a1aacbea9a5122a5ffe0b42 /prometheus_borgmatic_exporter/cli.py | |
| parent | 1eab7199f985a9f659105291ade7706e518f275d (diff) | |
| download | prometheus-borgmatic-exporter-3a52d4cf01e4443aba1b9f78152f673330274295.tar.gz prometheus-borgmatic-exporter-3a52d4cf01e4443aba1b9f78152f673330274295.zip | |
fix(cli): route diagnostic output to stderr
Keep diagnostic and progress messages separate from normal stdout output
so the exporter can be used safely in pipelines and command
substitutions.
Allow message helpers to operate without an active Click context, while
preserving quiet, verbose, and debug behavior when a context is
available.
Require Python 3.12 for the generic type parameter syntax used by the
new context-object decorator and refresh the lockfile accordingly.
Diffstat (limited to '')
| -rw-r--r-- | prometheus_borgmatic_exporter/cli.py | 143 |
1 files changed, 94 insertions, 49 deletions
diff --git a/prometheus_borgmatic_exporter/cli.py b/prometheus_borgmatic_exporter/cli.py index cc6f374..5d91b05 100644 --- a/prometheus_borgmatic_exporter/cli.py +++ b/prometheus_borgmatic_exporter/cli.py @@ -2,6 +2,12 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Define Click parameter types and styled terminal message helpers.""" + +from collections.abc import Callable +from functools import update_wrapper +from typing import Any, Concatenate + import click_extra as click @@ -11,22 +17,40 @@ class FlexibleColorOption(click.ColorOption): _gnu_optional_value = False -def emit(prefix: str, color: str, *message: str, enabled: bool = True) -> None: - """Render a styled prefix and message to stdout. +def pass_obj_silent[T, **P, R]( + f: Callable[Concatenate[T | None, P], R], +) -> Callable[P, R]: + """Pass the current context object to a callback, if available. + + The current Click context is retrieved silently. If no context exists or + its object is not a dictionary, ``None`` is passed instead. + + The context object is inserted as the first positional argument to *f*. + """ - Internal helper used by error(), msg(), warn(), verbose(), and debug() - to avoid duplicating the Click styling and echo logic. The first message - argument is rendered in bold, subsequent arguments are appended unstyled. + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + ctx = click.get_current_context(silent=True) + obj = ctx.obj if ctx is not None else None + return f(obj, *args, **kwargs) - Args: - prefix: The prefix string shown before the message (e.g. "==>", "==> ERROR:"). - color: A Click-compatible color name (e.g. "red", "green") applied to the prefix. - *message: One or more message parts to display. At least one is required. - enabled: If False, the function returns immediately without printing. - Defaults to True. + return update_wrapper(new_func, f) - Raises: - TypeError: If no message arguments are provided. + +def emit( + prefix: str, color: str, *message: str, enabled: bool = True, err: bool = False +) -> None: + """Render a styled prefix and message to stdout. + + This helper is used by :func:`error`, :func:`msg`, :func:`warn`, + :func:`verbose`, and :func:`debug`. The first message argument is rendered in + bold and subsequent arguments are appended unstyled. + + :param prefix: Prefix displayed before the message. + :param color: Click-compatible color name applied to the prefix. + :param message: One or more message parts to display. + :param enabled: Whether the message should be emitted. + :param err: Write to `stderr` instead of `stdout`. + :raises TypeError: If no message arguments are provided. """ if not message: raise TypeError("emit() missing 1 required positional argument: 'message'") @@ -41,76 +65,97 @@ def emit(prefix: str, color: str, *message: str, enabled: bool = True) -> None: click.style(message[0], bold=True), *message[1:], ] - ) + ), + err=err, ) def error(*message: str) -> None: """Print a formatted error message to stdout in red. - The first argument is rendered in bold, subsequent arguments are appended - unstyled. Always prints regardless of quiet or verbose flags. + The first argument is rendered in bold and subsequent arguments are appended + unstyled. Error messages are always printed regardless of quiet or verbose + flags. - Args: - *message: One or more message parts to display. + :param message: One or more message parts to display. """ - emit("==> ERROR:", "red", *message) + emit("==> ERROR:", "red", *message, err=True) -@click.pass_obj -def msg(obj: dict[str, bool], *message: str) -> None: +@pass_obj_silent +def msg(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None: """Print a formatted informational message to stdout in green. - Suppressed when the quiet flag is set. The first argument is rendered - in bold, subsequent arguments are appended unstyled. + The message is suppressed when the quiet flag is set. When called outside a + Click context, the message is emitted normally. The first argument is rendered + in bold and subsequent arguments are appended unstyled. - Args: - *message: One or more message parts to display. + :param message: One or more message parts to display. """ - emit("==>", "green", *message, enabled=not obj.get("quiet", False)) + emit( + "==>", + "green", + *message, + enabled=obj is None or not obj.get("quiet", False), + err=err, + ) -@click.pass_obj -def warn(obj: dict[str, bool], *message: str) -> None: +@pass_obj_silent +def warn(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None: """Print a formatted warning message to stdout in yellow. - Suppressed when the quiet flag is set. The first argument is rendered - in bold, subsequent arguments are appended unstyled. + The message is suppressed when the quiet flag is set. When called outside a + Click context, the message is emitted normally. The first argument is rendered + in bold and subsequent arguments are appended unstyled. - Args: - *message: One or more message parts to display. + :param message: One or more message parts to display. """ - emit("==>", "yellow", *message, enabled=not obj.get("quiet", False)) + emit( + "==>", + "yellow", + *message, + enabled=obj is None or not obj.get("quiet", False), + err=err, + ) -@click.pass_obj -def verbose(obj: dict[str, bool], *message: str) -> None: +@pass_obj_silent +def verbose(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None: """Print a formatted verbose message to stdout in blue. - Only prints when the verbose flag is set and the quiet flag is not. - The first argument is rendered in bold, subsequent arguments are - appended unstyled. + The message is printed only when called from a Click context with the verbose + flag set and the quiet flag unset. Calls made outside a Click context are a + no-op. - Args: - *message: One or more message parts to display. + :param message: One or more message parts to display. """ emit( "==>", "blue", *message, - enabled=obj.get("verbose", False) and not obj.get("quiet", False), + enabled=( + obj is not None + and obj.get("verbose", False) + and not obj.get("quiet", False) + ), + err=err, ) -@click.pass_obj -def debug(obj: dict[str, bool], *message: str) -> None: +@pass_obj_silent +def debug(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None: """Print a formatted debug message to stdout in magenta. - Only prints when the DEBUG environment variable is set to a truthy - value (1, true, yes). The first argument is rendered in bold, - subsequent arguments are appended unstyled. + The message is printed only when called from a Click context with the debug + flag enabled. Calls made outside a Click context are a no-op. - Args: - *message: One or more message parts to display. + :param message: One or more message parts to display. """ - emit("==>", "magenta", *message, enabled=obj.get("debug", False)) + emit( + "==>", + "magenta", + *message, + enabled=obj is not None and obj.get("debug", False), + err=err, + ) |
