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

1# /////////////////////////////////////////////////////////////// 

2# LIB_MODE - Passive stdlib-compatible logger and printer for library authors 

3# Project: ezpl 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7Passive logger and printer proxies for library authors. 

8 

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. 

12 

13Recommended imports for library code: 

14 

15 from ezpl.lib_mode import get_logger, get_printer 

16 

17These imports are intentionally lightweight — no dependency on rich or loguru 

18is triggered at import time. The lazy delegation happens at call time only. 

19 

20Usage pattern in a library: 

21 

22 from ezpl.lib_mode import get_logger, get_printer 

23 

24 log = get_logger(__name__) # stdlib Logger, silent by default 

25 printer = get_printer() # lazy EzPrinter proxy, silent by default 

26 

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""" 

31 

32from __future__ import annotations 

33 

34# /////////////////////////////////////////////////////////////// 

35# IMPORTS 

36# /////////////////////////////////////////////////////////////// 

37# Standard library imports 

38import logging 

39from collections.abc import Generator 

40from contextlib import contextmanager 

41from typing import Any 

42 

43# /////////////////////////////////////////////////////////////// 

44# CLASSES — PRINTER PROXIES 

45# /////////////////////////////////////////////////////////////// 

46 

47 

48class _LazyWizard: 

49 """ 

50 Lazy proxy for RichWizard. 

51 

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 """ 

56 

57 def _get_real(self) -> Any: 

58 from .ezpl import Ezpl 

59 

60 if Ezpl.is_initialized() and Ezpl.is_lib_printer_hook_enabled(): 

61 return Ezpl().get_printer().wizard 

62 return None 

63 

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 

69 

70 

71class _LazyPrinter: 

72 """ 

73 Lazy proxy for EzPrinter. 

74 

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. 

78 

79 The proxy holds a _LazyWizard so that printer.wizard.xxx() calls also 

80 resolve correctly. 

81 """ 

82 

83 def __init__(self) -> None: 

84 self._lazy_wizard = _LazyWizard() 

85 

86 # --- Internal --- 

87 

88 def _get_real(self) -> Any: 

89 from .ezpl import Ezpl 

90 

91 if Ezpl.is_initialized() and Ezpl.is_lib_printer_hook_enabled(): 

92 return Ezpl().get_printer() 

93 return None 

94 

95 # --- Properties --- 

96 

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" 

102 

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 

108 

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 ">" 

114 

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 "~" 

120 

121 @property 

122 def wizard(self) -> _LazyWizard: 

123 """Return the RichWizard proxy (lazy delegation).""" 

124 return self._lazy_wizard 

125 

126 # --- Core log methods --- 

127 

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) 

133 

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) 

139 

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) 

145 

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) 

151 

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) 

157 

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) 

163 

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) 

169 

170 # --- Pattern methods --- 

171 

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) 

177 

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) 

183 

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) 

189 

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) 

195 

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) 

201 

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) 

207 

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) 

213 

214 # --- Rich features --- 

215 

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) 

221 

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) 

229 

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) 

241 

242 # --- Indentation --- 

243 

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 "~" 

248 

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() 

254 

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() 

260 

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() 

266 

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 

276 

277 # --- Misc --- 

278 

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) 

284 

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() 

290 

291 def __str__(self) -> str: 

292 real = self._get_real() 

293 return str(real) if real is not None else "LazyPrinter(uninitialized)" 

294 

295 def __repr__(self) -> str: 

296 real = self._get_real() 

297 return repr(real) if real is not None else "LazyPrinter(uninitialized)" 

298 

299 

300# /////////////////////////////////////////////////////////////// 

301# MODULE-LEVEL SINGLETONS 

302# /////////////////////////////////////////////////////////////// 

303 

304# Single shared LazyPrinter instance — stateless proxy, safe to share 

305_PRINTER: _LazyPrinter = _LazyPrinter() 

306 

307# /////////////////////////////////////////////////////////////// 

308# FUNCTIONS 

309# /////////////////////////////////////////////////////////////// 

310 

311 

312def get_logger(name: str) -> logging.Logger: 

313 """ 

314 Return a stdlib-compatible logger for use in library code. 

315 

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. 

321 

322 This follows the official Python recommendation for library logging: 

323 https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library 

324 

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. 

328 

329 Returns: 

330 logging.Logger: A stdlib logger, passive by design. 

331 

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 

343 

344 

345def get_printer() -> _LazyPrinter: 

346 """ 

347 Return the shared lazy printer proxy for use in library code. 

348 

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. 

352 

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. 

356 

357 No configuration is triggered by calling this function — it is safe 

358 to call at module level in library code. 

359 

360 Returns: 

361 _LazyPrinter: A lazy proxy implementing the full EzPrinter interface. 

362 

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 

372 

373 

374# /////////////////////////////////////////////////////////////// 

375# PUBLIC API 

376# /////////////////////////////////////////////////////////////// 

377 

378__all__ = [ 

379 "get_logger", 

380 "get_printer", 

381]