Coverage for src / ezplog / lib_mode.py: 33.33%
153 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:27 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:27 +0000
1# ///////////////////////////////////////////////////////////////
2# LIB_MODE - Passive stdlib-compatible logger and printer for library authors
3# Project: ezpl
4# ///////////////////////////////////////////////////////////////
6"""
7Passive logger and printer proxies for library authors.
9Libraries should never configure logging or printing themselves. Use the
10functions below to obtain objects that are silent by default and automatically
11active once the host application initializes Ezpl.
13Recommended imports for library code:
15 from ezpl.lib_mode import get_logger, get_printer
17These imports are intentionally lightweight — no dependency on rich or loguru
18is triggered at import time. The lazy delegation happens at call time only.
20Usage pattern in a library:
22 from ezpl.lib_mode import get_logger, get_printer
24 log = get_logger(__name__) # stdlib Logger, silent by default
25 printer = get_printer() # lazy EzPrinter proxy, silent by default
27 def initialize():
28 log.info("Service ready") # captured if app enables logger hooks
29 printer.success("Service ready") # delegated to real EzPrinter if app initialized Ezpl
30"""
32from __future__ import annotations
34# ///////////////////////////////////////////////////////////////
35# IMPORTS
36# ///////////////////////////////////////////////////////////////
37# Standard library imports
38import logging
39from collections.abc import Generator
40from contextlib import contextmanager
41from typing import Any
43# ///////////////////////////////////////////////////////////////
44# CLASSES — PRINTER PROXIES
45# ///////////////////////////////////////////////////////////////
48class _LazyWizard:
49 """
50 Lazy proxy for RichWizard.
52 Delegates any attribute access / method call to the real RichWizard
53 when Ezpl is initialized. Returns a silent no-op callable otherwise.
54 This covers the full RichWizard API without enumerating every method.
55 """
57 def _get_real(self) -> Any:
58 from .ezpl import Ezpl
60 if Ezpl.is_initialized() and Ezpl.is_lib_printer_hook_enabled():
61 return Ezpl().get_printer().wizard
62 return None
64 def __getattr__(self, name: str) -> Any:
65 real = self._get_real()
66 if real is not None:
67 return getattr(real, name)
68 return lambda *_args, **_kwargs: None
71class _LazyPrinter:
72 """
73 Lazy proxy for EzPrinter.
75 All method calls are silently discarded when Ezpl is not initialized.
76 Once the host application calls Ezpl(...), every subsequent call is
77 transparently forwarded to the real EzPrinter instance.
79 The proxy holds a _LazyWizard so that printer.wizard.xxx() calls also
80 resolve correctly.
81 """
83 def __init__(self) -> None:
84 self._lazy_wizard = _LazyWizard()
86 # --- Internal ---
88 def _get_real(self) -> Any:
89 from .ezpl import Ezpl
91 if Ezpl.is_initialized() and Ezpl.is_lib_printer_hook_enabled():
92 return Ezpl().get_printer()
93 return None
95 # --- Properties ---
97 @property
98 def level(self) -> str:
99 """Return the current logging level, or 'INFO' if Ezpl is not initialized."""
100 real = self._get_real()
101 return real.level if real is not None else "INFO"
103 @property
104 def indent_step(self) -> int:
105 """Return the configured indentation step, or 3 if Ezpl is not initialized."""
106 real = self._get_real()
107 return real.indent_step if real is not None else 3
109 @property
110 def indent_symbol(self) -> str:
111 """Return the configured indentation symbol, or '>' if Ezpl is not initialized."""
112 real = self._get_real()
113 return real.indent_symbol if real is not None else ">"
115 @property
116 def base_indent_symbol(self) -> str:
117 """Return the base indentation symbol, or '~' if Ezpl is not initialized."""
118 real = self._get_real()
119 return real.base_indent_symbol if real is not None else "~"
121 @property
122 def wizard(self) -> _LazyWizard:
123 """Return the RichWizard proxy (lazy delegation)."""
124 return self._lazy_wizard
126 # --- Core log methods ---
128 def log(self, level: str, message: Any) -> None:
129 """Log a message at the given level (no-op if Ezpl not initialized)."""
130 real = self._get_real()
131 if real is not None:
132 real.log(level, message)
134 def info(self, message: Any) -> None:
135 """Log an info message (no-op if Ezpl not initialized)."""
136 real = self._get_real()
137 if real is not None:
138 real.info(message)
140 def debug(self, message: Any) -> None:
141 """Log a debug message (no-op if Ezpl not initialized)."""
142 real = self._get_real()
143 if real is not None:
144 real.debug(message)
146 def success(self, message: Any) -> None:
147 """Log a success message (no-op if Ezpl not initialized)."""
148 real = self._get_real()
149 if real is not None:
150 real.success(message)
152 def warning(self, message: Any) -> None:
153 """Log a warning message (no-op if Ezpl not initialized)."""
154 real = self._get_real()
155 if real is not None:
156 real.warning(message)
158 def error(self, message: Any) -> None:
159 """Log an error message (no-op if Ezpl not initialized)."""
160 real = self._get_real()
161 if real is not None:
162 real.error(message)
164 def critical(self, message: Any) -> None:
165 """Log a critical message (no-op if Ezpl not initialized)."""
166 real = self._get_real()
167 if real is not None:
168 real.critical(message)
170 # --- Pattern methods ---
172 def tip(self, message: Any) -> None:
173 """Display a tip message (no-op if Ezpl not initialized)."""
174 real = self._get_real()
175 if real is not None:
176 real.tip(message)
178 def system(self, message: Any) -> None:
179 """Display a system message (no-op if Ezpl not initialized)."""
180 real = self._get_real()
181 if real is not None:
182 real.system(message)
184 def install(self, message: Any) -> None:
185 """Display an install message (no-op if Ezpl not initialized)."""
186 real = self._get_real()
187 if real is not None:
188 real.install(message)
190 def detect(self, message: Any) -> None:
191 """Display a detect message (no-op if Ezpl not initialized)."""
192 real = self._get_real()
193 if real is not None:
194 real.detect(message)
196 def config(self, message: Any) -> None:
197 """Display a config message (no-op if Ezpl not initialized)."""
198 real = self._get_real()
199 if real is not None:
200 real.config(message)
202 def deps(self, message: Any) -> None:
203 """Display a deps message (no-op if Ezpl not initialized)."""
204 real = self._get_real()
205 if real is not None:
206 real.deps(message)
208 def print_pattern(self, pattern: Any, message: Any, level: str = "INFO") -> None:
209 """Display a message with pattern format (no-op if Ezpl not initialized)."""
210 real = self._get_real()
211 if real is not None:
212 real.print_pattern(pattern, message, level)
214 # --- Rich features ---
216 def print_table(self, data: list[dict[str, Any]], title: str | None = None) -> None:
217 """Display a table (no-op if Ezpl not initialized)."""
218 real = self._get_real()
219 if real is not None:
220 real.print_table(data, title=title)
222 def print_panel(
223 self, content: str, title: str | None = None, style: str = "blue"
224 ) -> None:
225 """Display a panel (no-op if Ezpl not initialized)."""
226 real = self._get_real()
227 if real is not None:
228 real.print_panel(content, title=title, style=style)
230 def print_json(
231 self,
232 data: str | dict | list,
233 title: str | None = None,
234 indent: int | None = None,
235 highlight: bool = True,
236 ) -> None:
237 """Display JSON data (no-op if Ezpl not initialized)."""
238 real = self._get_real()
239 if real is not None:
240 real.print_json(data, title=title, indent=indent, highlight=highlight)
242 # --- Indentation ---
244 def get_indent(self) -> str:
245 """Return the current indentation string, or '~' if Ezpl not initialized."""
246 real = self._get_real()
247 return real.get_indent() if real is not None else "~"
249 def add_indent(self) -> None:
250 """Increase indentation level (no-op if Ezpl not initialized)."""
251 real = self._get_real()
252 if real is not None:
253 real.add_indent()
255 def del_indent(self) -> None:
256 """Decrease indentation level (no-op if Ezpl not initialized)."""
257 real = self._get_real()
258 if real is not None:
259 real.del_indent()
261 def reset_indent(self) -> None:
262 """Reset indentation to zero (no-op if Ezpl not initialized)."""
263 real = self._get_real()
264 if real is not None:
265 real.reset_indent()
267 @contextmanager
268 def manage_indent(self) -> Generator[None, None, None]:
269 """Context manager for temporary indentation (pass-through if Ezpl not initialized)."""
270 real = self._get_real()
271 if real is not None:
272 with real.manage_indent():
273 yield
274 else:
275 yield
277 # --- Misc ---
279 def set_level(self, level: str) -> None:
280 """Set the logging level (no-op if Ezpl not initialized)."""
281 real = self._get_real()
282 if real is not None:
283 real.set_level(level)
285 def mark_level_as_configured(self) -> None:
286 """Mark the level as configured (no-op if Ezpl not initialized)."""
287 real = self._get_real()
288 if real is not None:
289 real.mark_level_as_configured()
291 def __str__(self) -> str:
292 real = self._get_real()
293 return str(real) if real is not None else "LazyPrinter(uninitialized)"
295 def __repr__(self) -> str:
296 real = self._get_real()
297 return repr(real) if real is not None else "LazyPrinter(uninitialized)"
300# ///////////////////////////////////////////////////////////////
301# MODULE-LEVEL SINGLETONS
302# ///////////////////////////////////////////////////////////////
304# Single shared LazyPrinter instance — stateless proxy, safe to share
305_PRINTER: _LazyPrinter = _LazyPrinter()
307# ///////////////////////////////////////////////////////////////
308# FUNCTIONS
309# ///////////////////////////////////////////////////////////////
312def get_logger(name: str) -> logging.Logger:
313 """
314 Return a stdlib-compatible logger for use in library code.
316 The returned logger has a NullHandler attached so that no output is
317 produced when the host application has not configured any handler.
318 When the host application enables logger hooks in Ezpl, all
319 records emitted by this logger are automatically forwarded to the
320 Rich/loguru pipeline.
322 This follows the official Python recommendation for library logging:
323 https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library
325 Args:
326 name: Logger name. Use __name__ to follow the module-name convention,
327 which allows the host application to filter by package prefix.
329 Returns:
330 logging.Logger: A stdlib logger, passive by design.
332 Example:
333 >>> from ezpl.lib_mode import get_logger
334 >>> log = get_logger(__name__)
335 >>> log.info("Service initialized") # silent without host config
336 >>> log.warning("Unexpected state") # forwarded if intercepted
337 """
338 log = logging.getLogger(name)
339 # Attach NullHandler only once — avoids duplicate handlers on re-import
340 if not log.handlers: 340 ↛ 342line 340 didn't jump to line 342 because the condition on line 340 was always true
341 log.addHandler(logging.NullHandler())
342 return log
345def get_printer() -> _LazyPrinter:
346 """
347 Return the shared lazy printer proxy for use in library code.
349 The returned proxy silently discards all method calls when the host
350 application has not initialized Ezpl. Once the app calls Ezpl(...),
351 every subsequent call is transparently forwarded to the real EzPrinter.
353 The same instance is returned on every call (module-level singleton).
354 The proxy is stateless — it holds no indentation or level state of its
355 own, delegating everything to the real EzPrinter when available.
357 No configuration is triggered by calling this function — it is safe
358 to call at module level in library code.
360 Returns:
361 _LazyPrinter: A lazy proxy implementing the full EzPrinter interface.
363 Example:
364 >>> from ezpl.lib_mode import get_printer
365 >>> printer = get_printer()
366 >>> printer.success("Service ready") # silent without host config
367 >>> printer.info("Processing...") # delegated once app initializes Ezpl
368 >>> with printer.manage_indent():
369 ... printer.debug("detail")
370 """
371 return _PRINTER
374# ///////////////////////////////////////////////////////////////
375# PUBLIC API
376# ///////////////////////////////////////////////////////////////
378__all__ = [
379 "get_logger",
380 "get_printer",
381]