Coverage for src / kemi / cli_writer.py: 94%
49 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-05 15:47 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-05 15:47 +0000
1"""CLI output writer abstraction.
3Provides three writers that share a uniform ``write``/``error`` interface:
5- :class:`ConsoleWriter` — human-readable, the default.
6- :class:`JsonWriter` — one JSON object per line (NDJSON), for scripts.
7- :class:`SilentWriter` — drops ``info`` messages, keeps ``error`` and
8 ``warn``.
10The CLI entry point parses ``--json`` / ``--quiet`` flags and passes
11the chosen writer to every subcommand as ``args.writer``.
12"""
14from __future__ import annotations
16import json
17import sys
18from typing import Any, Protocol, TextIO
21class Writer(Protocol):
22 """Protocol every CLI writer satisfies."""
24 def write(self, message: str, *, kind: str = "info", flush: bool = False, end: str = "\n") -> None:
25 """Emit a message. ``kind`` is one of info/warn/error.
27 ``flush`` is honoured for streaming use cases (recall-stream).
28 ``end`` controls the trailing newline (default ``"\n"``; pass
29 ``""`` for interactive prompts).
30 """
31 ...
33 def error(self, message: str) -> None:
34 """Emit an error message. Always shown, even in --quiet mode."""
35 ...
37 def warn(self, message: str) -> None:
38 """Emit a warning message."""
39 ...
42class ConsoleWriter:
43 """Default human-readable writer. Prints to ``stream`` (default stdout).
45 The default stream is looked up lazily so that ``capsys``/``capfd``
46 redirections from ``pytest`` take effect.
47 """
49 def __init__(self, stream: TextIO | None = None) -> None:
50 self._explicit_stream = stream
52 def _stream(self) -> TextIO:
53 return self._explicit_stream or sys.stdout
55 def write(self, message: str, *, kind: str = "info", flush: bool = False, end: str = "\n") -> None:
56 s = self._stream()
57 print(message, file=s, flush=flush)
59 def error(self, message: str) -> None:
60 print(f"Error: {message}", file=sys.stderr)
62 def warn(self, message: str) -> None:
63 print(f"Warning: {message}", file=sys.stderr)
66class JsonWriter:
67 """NDJSON writer — one JSON object per line, on stdout.
69 Useful for piping into ``jq`` or other tools. Use ``kind`` to
70 distinguish info/warn/error entries.
71 """
73 def __init__(self, stream: TextIO | None = None) -> None:
74 self._stream = stream or sys.stdout
76 def _emit(self, payload: dict[str, Any]) -> None:
77 self._stream.write(json.dumps(payload, default=str) + "\n")
78 self._stream.flush()
80 def write(self, message: str, *, kind: str = "info", flush: bool = False, end: str = "\n") -> None:
81 self._emit({"level": kind, "message": message})
83 def error(self, message: str) -> None:
84 self._emit({"level": "error", "message": message})
86 def warn(self, message: str) -> None:
87 self._emit({"level": "warn", "message": message})
90class SilentWriter:
91 """Drops ``info`` messages; keeps ``error`` and ``warn``."""
93 def write(self, message: str, *, kind: str = "info", flush: bool = False, end: str = "\n") -> None:
94 if kind != "info":
95 print(message, file=sys.stderr)
97 def error(self, message: str) -> None:
98 print(f"Error: {message}", file=sys.stderr)
100 def warn(self, message: str) -> None:
101 print(f"Warning: {message}", file=sys.stderr)
104def make_writer(json_mode: bool = False, quiet: bool = False) -> Writer:
105 """Construct the writer chosen by ``--json`` and ``--quiet`` flags.
107 - ``json_mode=True, quiet=False`` → JsonWriter
108 - ``json_mode=False, quiet=True`` → SilentWriter
109 - ``json_mode=True, quiet=True`` → JsonWriter (quiet ignored)
110 - default → ConsoleWriter
111 """
112 if json_mode:
113 return JsonWriter()
114 if quiet:
115 return SilentWriter()
116 return ConsoleWriter()