GRM-14: feat: use click.echo() for user-facing messages with dual logging
- Create ui.py with say() helper that routes messages to both click.echo()
(console/stdout, user-facing) and logging.getLogger('grm') (file audit trail)
- Update executor.py: replace logger.info() with say() for start, status, done
messages; use say(level=ERROR, err=True) before raising AnsibleError
- Update report.py: replace logger.info() with say() for operation report lines
- Update all unit tests to patch say() instead of using capsys or get_logger
- Add test_ui.py with coverage for say() calling both click.echo and logging
- 117 tests, 100% coverage, pyright clean, ruff clean
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -11,6 +12,7 @@ from pathlib import Path
|
||||
from .exceptions import AnsibleError
|
||||
from .i18n import _
|
||||
from .logging_config import get_logger
|
||||
from .ui import say
|
||||
|
||||
|
||||
class AnsibleExecutor:
|
||||
@@ -25,8 +27,8 @@ class AnsibleExecutor:
|
||||
log_file = self._prepare_log(cmd)
|
||||
desc = description or _("Running Ansible playbook")
|
||||
|
||||
self._logger.info(desc)
|
||||
self._logger.info(_("Full log: {log_file}", log_file=log_file))
|
||||
say(f"[GRM] {desc}")
|
||||
say(f"[GRM] {_('Full log: {log_file}', log_file=log_file)}")
|
||||
|
||||
returncode = self._stream(cmd, log_file)
|
||||
|
||||
@@ -36,14 +38,14 @@ class AnsibleExecutor:
|
||||
code=returncode,
|
||||
log_file=log_file,
|
||||
)
|
||||
self._logger.error(msg)
|
||||
say(f"[GRM] {msg}", level=logging.ERROR, err=True)
|
||||
raise AnsibleError(msg)
|
||||
|
||||
status = self._extract_status(log_file)
|
||||
if status:
|
||||
self._logger.info(status)
|
||||
say(f"[GRM] {status}")
|
||||
|
||||
self._logger.info(_("Done. See full log: {log_file}", log_file=log_file))
|
||||
say(f"[GRM] {_('Done. See full log: {log_file}', log_file=log_file)}")
|
||||
|
||||
def _prepare_log(self, cmd: list[str]) -> Path:
|
||||
"""Create a log file with header."""
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .i18n import _
|
||||
from .logging_config import get_logger
|
||||
from .ui import say
|
||||
|
||||
|
||||
class Step:
|
||||
@@ -57,15 +57,14 @@ def track_steps() -> Generator[StepTracker, None, None]:
|
||||
|
||||
def _print_report(steps: list[Step]) -> None:
|
||||
"""Print a translated operation report to stdout."""
|
||||
logger = get_logger()
|
||||
icons = {
|
||||
"completed": "✓",
|
||||
"failed": "✗",
|
||||
"pending": "○",
|
||||
"in_progress": "◌",
|
||||
}
|
||||
logger.info(_("=== Operation Report ==="))
|
||||
say(f"[GRM] {_('=== Operation Report ===')}")
|
||||
for step in steps:
|
||||
icon = icons.get(step.status, "?")
|
||||
status_label = _(step.status)
|
||||
logger.info(f" {icon} {step.name} ({status_label})")
|
||||
say(f"[GRM] {icon} {step.name} ({status_label})")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""User-facing output utilities for GRM."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def say(msg: str, level: int = logging.INFO, err: bool = False) -> None:
|
||||
"""Output a message to the user and also log it for auditing.
|
||||
|
||||
Console output goes via ``click.echo`` (handles encoding, CliRunner,
|
||||
Windows colorama). The same message is also sent to the ``grm`` logger
|
||||
so it appears in the persistent log file.
|
||||
"""
|
||||
click.echo(msg, err=err)
|
||||
logging.getLogger("grm").log(level, msg)
|
||||
Reference in New Issue
Block a user