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

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 uses intercept_stdlib=True 

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

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

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 warn(self, message: Any) -> None: 

159 """Alias for warning().""" 

160 self.warning(message) 

161 

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) 

167 

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) 

173 

174 # --- Pattern methods --- 

175 

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) 

181 

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) 

187 

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) 

193 

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) 

199 

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) 

205 

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) 

211 

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) 

217 

218 # --- Rich features --- 

219 

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) 

225 

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) 

233 

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) 

245 

246 # --- Indentation --- 

247 

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

252 

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

258 

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

264 

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

270 

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 

280 

281 # --- Misc --- 

282 

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) 

288 

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

294 

295 def __str__(self) -> str: 

296 real = self._get_real() 

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

298 

299 def __repr__(self) -> str: 

300 real = self._get_real() 

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

302 

303 

304# /////////////////////////////////////////////////////////////// 

305# MODULE-LEVEL SINGLETONS 

306# /////////////////////////////////////////////////////////////// 

307 

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

309_PRINTER: _LazyPrinter = _LazyPrinter() 

310 

311# /////////////////////////////////////////////////////////////// 

312# FUNCTIONS 

313# /////////////////////////////////////////////////////////////// 

314 

315 

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

317 """ 

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

319 

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. 

325 

326 This follows the official Python recommendation for library logging: 

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

328 

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. 

332 

333 Returns: 

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

335 

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 

347 

348 

349def get_printer() -> _LazyPrinter: 

350 """ 

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

352 

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. 

356 

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. 

360 

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

362 to call at module level in library code. 

363 

364 Returns: 

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

366 

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 

376 

377 

378# /////////////////////////////////////////////////////////////// 

379# PUBLIC API 

380# /////////////////////////////////////////////////////////////// 

381 

382__all__ = [ 

383 "get_logger", 

384 "get_printer", 

385]