Coverage for src / ezplog / lib_mode.py: 25.36%
155 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 19:43 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 19:43 +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 uses intercept_stdlib=True
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():
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():
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 warn(self, message: Any) -> None:
159 """Alias for warning()."""
160 self.warning(message)
162 def error(self, message: Any) -> None:
163 """Log an error message (no-op if Ezpl not initialized)."""
164 real = self._get_real()
165 if real is not None:
166 real.error(message)
168 def critical(self, message: Any) -> None:
169 """Log a critical message (no-op if Ezpl not initialized)."""
170 real = self._get_real()
171 if real is not None:
172 real.critical(message)
174 # --- Pattern methods ---
176 def tip(self, message: Any) -> None:
177 """Display a tip message (no-op if Ezpl not initialized)."""
178 real = self._get_real()
179 if real is not None:
180 real.tip(message)
182 def system(self, message: Any) -> None:
183 """Display a system message (no-op if Ezpl not initialized)."""
184 real = self._get_real()
185 if real is not None:
186 real.system(message)
188 def install(self, message: Any) -> None:
189 """Display an install message (no-op if Ezpl not initialized)."""
190 real = self._get_real()
191 if real is not None:
192 real.install(message)
194 def detect(self, message: Any) -> None:
195 """Display a detect message (no-op if Ezpl not initialized)."""
196 real = self._get_real()
197 if real is not None:
198 real.detect(message)
200 def config(self, message: Any) -> None:
201 """Display a config message (no-op if Ezpl not initialized)."""
202 real = self._get_real()
203 if real is not None:
204 real.config(message)
206 def deps(self, message: Any) -> None:
207 """Display a deps message (no-op if Ezpl not initialized)."""
208 real = self._get_real()
209 if real is not None:
210 real.deps(message)
212 def print_pattern(self, pattern: Any, message: Any, level: str = "INFO") -> None:
213 """Display a message with pattern format (no-op if Ezpl not initialized)."""
214 real = self._get_real()
215 if real is not None:
216 real.print_pattern(pattern, message, level)
218 # --- Rich features ---
220 def print_table(self, data: list[dict[str, Any]], title: str | None = None) -> None:
221 """Display a table (no-op if Ezpl not initialized)."""
222 real = self._get_real()
223 if real is not None:
224 real.print_table(data, title=title)
226 def print_panel(
227 self, content: str, title: str | None = None, style: str = "blue"
228 ) -> None:
229 """Display a panel (no-op if Ezpl not initialized)."""
230 real = self._get_real()
231 if real is not None:
232 real.print_panel(content, title=title, style=style)
234 def print_json(
235 self,
236 data: str | dict | list,
237 title: str | None = None,
238 indent: int | None = None,
239 highlight: bool = True,
240 ) -> None:
241 """Display JSON data (no-op if Ezpl not initialized)."""
242 real = self._get_real()
243 if real is not None:
244 real.print_json(data, title=title, indent=indent, highlight=highlight)
246 # --- Indentation ---
248 def get_indent(self) -> str:
249 """Return the current indentation string, or '~' if Ezpl not initialized."""
250 real = self._get_real()
251 return real.get_indent() if real is not None else "~"
253 def add_indent(self) -> None:
254 """Increase indentation level (no-op if Ezpl not initialized)."""
255 real = self._get_real()
256 if real is not None:
257 real.add_indent()
259 def del_indent(self) -> None:
260 """Decrease indentation level (no-op if Ezpl not initialized)."""
261 real = self._get_real()
262 if real is not None:
263 real.del_indent()
265 def reset_indent(self) -> None:
266 """Reset indentation to zero (no-op if Ezpl not initialized)."""
267 real = self._get_real()
268 if real is not None:
269 real.reset_indent()
271 @contextmanager
272 def manage_indent(self) -> Generator[None, None, None]:
273 """Context manager for temporary indentation (pass-through if Ezpl not initialized)."""
274 real = self._get_real()
275 if real is not None:
276 with real.manage_indent():
277 yield
278 else:
279 yield
281 # --- Misc ---
283 def set_level(self, level: str) -> None:
284 """Set the logging level (no-op if Ezpl not initialized)."""
285 real = self._get_real()
286 if real is not None:
287 real.set_level(level)
289 def mark_level_as_configured(self) -> None:
290 """Mark the level as configured (no-op if Ezpl not initialized)."""
291 real = self._get_real()
292 if real is not None:
293 real.mark_level_as_configured()
295 def __str__(self) -> str:
296 real = self._get_real()
297 return str(real) if real is not None else "LazyPrinter(uninitialized)"
299 def __repr__(self) -> str:
300 real = self._get_real()
301 return repr(real) if real is not None else "LazyPrinter(uninitialized)"
304# ///////////////////////////////////////////////////////////////
305# MODULE-LEVEL SINGLETONS
306# ///////////////////////////////////////////////////////////////
308# Single shared LazyPrinter instance — stateless proxy, safe to share
309_PRINTER: _LazyPrinter = _LazyPrinter()
311# ///////////////////////////////////////////////////////////////
312# FUNCTIONS
313# ///////////////////////////////////////////////////////////////
316def get_logger(name: str) -> logging.Logger:
317 """
318 Return a stdlib-compatible logger for use in library code.
320 The returned logger has a NullHandler attached so that no output is
321 produced when the host application has not configured any handler.
322 When the host application uses Ezpl with intercept_stdlib=True, all
323 records emitted by this logger are automatically forwarded to the
324 Rich/loguru pipeline.
326 This follows the official Python recommendation for library logging:
327 https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library
329 Args:
330 name: Logger name. Use __name__ to follow the module-name convention,
331 which allows the host application to filter by package prefix.
333 Returns:
334 logging.Logger: A stdlib logger, passive by design.
336 Example:
337 >>> from ezpl.lib_mode import get_logger
338 >>> log = get_logger(__name__)
339 >>> log.info("Service initialized") # silent without host config
340 >>> log.warning("Unexpected state") # forwarded if intercepted
341 """
342 log = logging.getLogger(name)
343 # Attach NullHandler only once — avoids duplicate handlers on re-import
344 if not log.handlers:
345 log.addHandler(logging.NullHandler())
346 return log
349def get_printer() -> _LazyPrinter:
350 """
351 Return the shared lazy printer proxy for use in library code.
353 The returned proxy silently discards all method calls when the host
354 application has not initialized Ezpl. Once the app calls Ezpl(...),
355 every subsequent call is transparently forwarded to the real EzPrinter.
357 The same instance is returned on every call (module-level singleton).
358 The proxy is stateless — it holds no indentation or level state of its
359 own, delegating everything to the real EzPrinter when available.
361 No configuration is triggered by calling this function — it is safe
362 to call at module level in library code.
364 Returns:
365 _LazyPrinter: A lazy proxy implementing the full EzPrinter interface.
367 Example:
368 >>> from ezpl.lib_mode import get_printer
369 >>> printer = get_printer()
370 >>> printer.success("Service ready") # silent without host config
371 >>> printer.info("Processing...") # delegated once app initializes Ezpl
372 >>> with printer.manage_indent():
373 ... printer.debug("detail")
374 """
375 return _PRINTER
378# ///////////////////////////////////////////////////////////////
379# PUBLIC API
380# ///////////////////////////////////////////////////////////////
382__all__ = [
383 "get_logger",
384 "get_printer",
385]