Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2# cardinal_pythonlib/logs.py 

3 

4""" 

5=============================================================================== 

6 

7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of cardinal_pythonlib. 

10 

11 Licensed under the Apache License, Version 2.0 (the "License"); 

12 you may not use this file except in compliance with the License. 

13 You may obtain a copy of the License at 

14 

15 https://www.apache.org/licenses/LICENSE-2.0 

16 

17 Unless required by applicable law or agreed to in writing, software 

18 distributed under the License is distributed on an "AS IS" BASIS, 

19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

20 See the License for the specific language governing permissions and 

21 limitations under the License. 

22 

23=============================================================================== 

24 

25**Support functions for logging.** 

26 

27See https://docs.python.org/3.4/howto/logging.html#library-config 

28 

29USER CODE should use the following general methods. 

30 

31(a) Simple: 

32 

33 .. code-block:: python 

34 

35 import logging 

36 log = logging.getLogger(__name__) # for your own logs 

37 logging.basicConfig() 

38 

39(b) More complex: 

40 

41 .. code-block:: python 

42 

43 import logging 

44 log = logging.getLogger(__name__) 

45 logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATEFMT, 

46 level=loglevel) 

47 

48(c) Using colour conveniently: 

49 

50 .. code-block:: python 

51 

52 import logging 

53 mylogger = logging.getLogger(__name__) 

54 rootlogger = logging.getLogger() 

55 

56 from whisker.log import configure_logger_for_colour 

57 configure_logger_for_colour(rootlogger) 

58 

59 

60LIBRARY CODE should use the following general methods. 

61 

62.. code-block:: python 

63 

64 import logging 

65 log = logging.getLogger(__name__) 

66 

67 # ... and if you want to suppress output unless the user configures logs: 

68 log.addHandler(logging.NullHandler()) 

69 # ... which only needs to be done in the __init__.py for the package 

70 # https://stackoverflow.com/questions/12296214 

71 

72 # LIBRARY CODE SHOULD NOT ADD ANY OTHER HANDLERS; see above. 

73 

74DO NOT call this module "logging"! Many things may get confused. 

75 

76""" 

77 

78from html import escape 

79from inspect import Parameter, signature 

80import json 

81import logging 

82import os 

83from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple, Union 

84 

85from colorlog import ColoredFormatter 

86 

87# ============================================================================= 

88# Quick configuration of a specific log format 

89# ============================================================================= 

90 

91LOG_FORMAT = '%(asctime)s.%(msecs)03d:%(levelname)s:%(name)s:%(message)s' 

92LOG_FORMAT_WITH_PID = ( 

93 f'%(asctime)s.%(msecs)03d:%(levelname)s:{os.getpid()}:%(name)s:%(message)s' 

94) 

95 

96LOG_DATEFMT = '%Y-%m-%d %H:%M:%S' 

97 

98LOG_COLORS = {'DEBUG': 'cyan', 

99 'INFO': 'green', 

100 'WARNING': 'bold_yellow', 

101 'ERROR': 'bold_red', 

102 'CRITICAL': 'bold_white,bg_red'} 

103 

104 

105def get_monochrome_handler( 

106 extranames: List[str] = None, 

107 with_process_id: bool = False, 

108 with_thread_id: bool = False, 

109 stream: TextIO = None) -> logging.StreamHandler: 

110 """ 

111 Gets a monochrome log handler using a standard format. 

112 

113 Args: 

114 extranames: additional names to append to the logger's name 

115 with_process_id: include the process ID in the logger's name? 

116 with_thread_id: include the thread ID in the logger's name? 

117 stream: ``TextIO`` stream to send log output to 

118 

119 Returns: 

120 the :class:`logging.StreamHandler` 

121 

122 """ 

123 fmt = "%(asctime)s.%(msecs)03d" 

124 if with_process_id or with_thread_id: 

125 procinfo = [] # type: List[str] 

126 if with_process_id: 

127 procinfo.append("p%(process)d") 

128 if with_thread_id: 

129 procinfo.append("t%(thread)d") 

130 fmt += f" [{'.'.join(procinfo)}]" 

131 extras = ":" + ":".join(extranames) if extranames else "" 

132 fmt += f" %(name)s{extras}:%(levelname)s: " 

133 fmt += "%(message)s" 

134 f = logging.Formatter(fmt, datefmt=LOG_DATEFMT, style='%') 

135 h = logging.StreamHandler(stream) 

136 h.setFormatter(f) 

137 return h 

138 

139 

140def get_colour_handler(extranames: List[str] = None, 

141 with_process_id: bool = False, 

142 with_thread_id: bool = False, 

143 stream: TextIO = None) -> logging.StreamHandler: 

144 """ 

145 Gets a colour log handler using a standard format. 

146 

147 Args: 

148 extranames: additional names to append to the logger's name 

149 with_process_id: include the process ID in the logger's name? 

150 with_thread_id: include the thread ID in the logger's name? 

151 stream: ``TextIO`` stream to send log output to 

152 

153 Returns: 

154 the :class:`logging.StreamHandler` 

155 

156 """ 

157 fmt = "%(white)s%(asctime)s.%(msecs)03d" # this is dim white = grey 

158 if with_process_id or with_thread_id: 

159 procinfo = [] # type: List[str] 

160 if with_process_id: 

161 procinfo.append("p%(process)d") 

162 if with_thread_id: 

163 procinfo.append("t%(thread)d") 

164 fmt += f" [{'.'.join(procinfo)}]" 

165 extras = ":" + ":".join(extranames) if extranames else "" 

166 fmt += f" %(name)s{extras}:%(levelname)s: " 

167 fmt += "%(reset)s%(log_color)s%(message)s" 

168 cf = ColoredFormatter(fmt, 

169 datefmt=LOG_DATEFMT, 

170 reset=True, 

171 log_colors=LOG_COLORS, 

172 secondary_log_colors={}, 

173 style='%') 

174 ch = logging.StreamHandler(stream) 

175 ch.setFormatter(cf) 

176 return ch 

177 

178 

179def configure_logger_for_colour(logger: logging.Logger, 

180 level: int = logging.INFO, 

181 remove_existing: bool = False, 

182 extranames: List[str] = None, 

183 with_process_id: bool = False, 

184 with_thread_id: bool = False) -> None: 

185 """ 

186 Applies a preconfigured datetime/colour scheme to a logger. 

187 

188 Should ONLY be called from the ``if __name__ == 'main'`` script; 

189 see https://docs.python.org/3.4/howto/logging.html#library-config. 

190 

191 Args: 

192 logger: logger to modify 

193 level: log level to set 

194 remove_existing: remove existing handlers from logger first? 

195 extranames: additional names to append to the logger's name 

196 with_process_id: include the process ID in the logger's name? 

197 with_thread_id: include the thread ID in the logger's name? 

198 """ 

199 if remove_existing: 

200 logger.handlers = [] # https://stackoverflow.com/questions/7484454 

201 handler = get_colour_handler(extranames, 

202 with_process_id=with_process_id, 

203 with_thread_id=with_thread_id) 

204 handler.setLevel(level) 

205 logger.addHandler(handler) 

206 logger.setLevel(level) 

207 

208 

209def main_only_quicksetup_rootlogger(level: int = logging.DEBUG, 

210 with_process_id: bool = False, 

211 with_thread_id: bool = False) -> None: 

212 """ 

213 Quick function to set up the root logger for colour. 

214 

215 Should ONLY be called from the ``if __name__ == 'main'`` script; 

216 see https://docs.python.org/3.4/howto/logging.html#library-config. 

217 

218 Args: 

219 level: log level to set 

220 with_process_id: include the process ID in the logger's name? 

221 with_thread_id: include the thread ID in the logger's name? 

222 """ 

223 # Nasty. Only call from "if __name__ == '__main__'" clauses! 

224 rootlogger = logging.getLogger() 

225 configure_logger_for_colour(rootlogger, level, remove_existing=True, 

226 with_process_id=with_process_id, 

227 with_thread_id=with_thread_id) 

228 # logging.basicConfig(level=level) 

229 

230 

231# ============================================================================= 

232# Generic log functions 

233# ============================================================================= 

234 

235def remove_all_logger_handlers(logger: logging.Logger) -> None: 

236 """ 

237 Remove all handlers from a logger. 

238 

239 Args: 

240 logger: logger to modify 

241 """ 

242 while logger.handlers: 

243 h = logger.handlers[0] 

244 logger.removeHandler(h) 

245 

246 

247def reset_logformat(logger: logging.Logger, 

248 fmt: str, 

249 datefmt: str = '%Y-%m-%d %H:%M:%S') -> None: 

250 """ 

251 Create a new formatter and apply it to the logger. 

252 

253 :func:`logging.basicConfig` won't reset the formatter if another module 

254 has called it, so always set the formatter like this. 

255 

256 Args: 

257 logger: logger to modify 

258 fmt: passed to the ``fmt=`` argument of :class:`logging.Formatter` 

259 datefmt: passed to the ``datefmt=`` argument of 

260 :class:`logging.Formatter` 

261 """ 

262 handler = logging.StreamHandler() 

263 formatter = logging.Formatter(fmt=fmt, datefmt=datefmt) 

264 handler.setFormatter(formatter) 

265 remove_all_logger_handlers(logger) 

266 logger.addHandler(handler) 

267 logger.propagate = False 

268 

269 

270def reset_logformat_timestamped(logger: logging.Logger, 

271 extraname: str = "", 

272 level: int = logging.INFO) -> None: 

273 """ 

274 Apply a simple time-stamped log format to an existing logger, and set 

275 its loglevel to either ``logging.DEBUG`` or ``logging.INFO``. 

276 

277 Args: 

278 logger: logger to modify 

279 extraname: additional name to append to the logger's name 

280 level: log level to set 

281 """ 

282 namebit = extraname + ":" if extraname else "" 

283 fmt = ("%(asctime)s.%(msecs)03d:%(levelname)s:%(name)s:" + namebit + 

284 "%(message)s") 

285 # logger.info(fmt) 

286 reset_logformat(logger, fmt=fmt) 

287 # logger.info(fmt) 

288 logger.setLevel(level) 

289 

290 

291# ============================================================================= 

292# Helper functions 

293# ============================================================================= 

294 

295def configure_all_loggers_for_colour(remove_existing: bool = True) -> None: 

296 """ 

297 Applies a preconfigured datetime/colour scheme to ALL logger. 

298 

299 Should ONLY be called from the ``if __name__ == 'main'`` script; 

300 see https://docs.python.org/3.4/howto/logging.html#library-config. 

301 

302 Generally MORE SENSIBLE just to apply a handler to the root logger. 

303 

304 Args: 

305 remove_existing: remove existing handlers from logger first? 

306 

307 """ 

308 handler = get_colour_handler() 

309 apply_handler_to_all_logs(handler, remove_existing=remove_existing) 

310 

311 

312def apply_handler_to_root_log(handler: logging.Handler, 

313 remove_existing: bool = False) -> None: 

314 """ 

315 Applies a handler to all logs, optionally removing existing handlers. 

316 

317 Should ONLY be called from the ``if __name__ == 'main'`` script; 

318 see https://docs.python.org/3.4/howto/logging.html#library-config. 

319 

320 Generally MORE SENSIBLE just to apply a handler to the root logger. 

321 

322 Args: 

323 handler: the handler to apply 

324 remove_existing: remove existing handlers from logger first? 

325 """ 

326 rootlog = logging.getLogger() 

327 if remove_existing: 

328 rootlog.handlers = [] 

329 rootlog.addHandler(handler) 

330 

331 

332def apply_handler_to_all_logs(handler: logging.Handler, 

333 remove_existing: bool = False) -> None: 

334 """ 

335 Applies a handler to all logs, optionally removing existing handlers. 

336 

337 Should ONLY be called from the ``if __name__ == 'main'`` script; 

338 see https://docs.python.org/3.4/howto/logging.html#library-config. 

339 

340 Generally MORE SENSIBLE just to apply a handler to the root logger. 

341 

342 Args: 

343 handler: the handler to apply 

344 remove_existing: remove existing handlers from logger first? 

345 """ 

346 # noinspection PyUnresolvedReferences 

347 for name, obj in logging.Logger.manager.loggerDict.items(): 

348 if remove_existing: 

349 obj.handlers = [] # https://stackoverflow.com/questions/7484454 

350 obj.addHandler(handler) 

351 

352 

353def copy_root_log_to_file(filename: str, 

354 fmt: str = LOG_FORMAT, 

355 datefmt: str = LOG_DATEFMT) -> None: 

356 """ 

357 Copy all currently configured logs to the specified file. 

358 

359 Should ONLY be called from the ``if __name__ == 'main'`` script; 

360 see https://docs.python.org/3.4/howto/logging.html#library-config. 

361 """ 

362 fh = logging.FileHandler(filename) 

363 # default file mode is 'a' for append 

364 formatter = logging.Formatter(fmt=fmt, datefmt=datefmt) 

365 fh.setFormatter(formatter) 

366 apply_handler_to_root_log(fh) 

367 

368 

369def copy_all_logs_to_file(filename: str, 

370 fmt: str = LOG_FORMAT, 

371 datefmt: str = LOG_DATEFMT) -> None: 

372 """ 

373 Copy all currently configured logs to the specified file. 

374 

375 Should ONLY be called from the ``if __name__ == 'main'`` script; 

376 see https://docs.python.org/3.4/howto/logging.html#library-config. 

377 

378 Args: 

379 filename: file to send log output to 

380 fmt: passed to the ``fmt=`` argument of :class:`logging.Formatter` 

381 datefmt: passed to the ``datefmt=`` argument of 

382 :class:`logging.Formatter` 

383 """ 

384 fh = logging.FileHandler(filename) 

385 # default file mode is 'a' for append 

386 formatter = logging.Formatter(fmt=fmt, datefmt=datefmt) 

387 fh.setFormatter(formatter) 

388 apply_handler_to_all_logs(fh) 

389 

390 

391# noinspection PyProtectedMember 

392def get_formatter_report(f: logging.Formatter) -> Optional[Dict[str, str]]: 

393 """ 

394 Returns information on a log formatter, as a dictionary. 

395 For debugging. 

396 """ 

397 if f is None: 

398 return None 

399 return { 

400 '_fmt': f._fmt, 

401 'datefmt': f.datefmt, 

402 '_style': str(f._style), 

403 } 

404 

405 

406def get_handler_report(h: logging.Handler) -> Dict[str, Any]: 

407 """ 

408 Returns information on a log handler, as a dictionary. 

409 For debugging. 

410 """ 

411 # noinspection PyUnresolvedReferences 

412 return { 

413 'get_name()': h.get_name(), 

414 'level': h.level, 

415 'formatter': get_formatter_report(h.formatter), 

416 'filters': h.filters, 

417 } 

418 

419 

420def get_log_report(log: Union[logging.Logger, 

421 logging.PlaceHolder]) -> Dict[str, Any]: 

422 """ 

423 Returns information on a log, as a dictionary. For debugging. 

424 """ 

425 if isinstance(log, logging.Logger): 

426 # suppress invalid error for Logger.manager: 

427 # noinspection PyUnresolvedReferences 

428 return { 

429 '(object)': str(log), 

430 'level': log.level, 

431 'disabled': log.disabled, 

432 'propagate': log.propagate, 

433 'parent': str(log.parent), 

434 'manager': str(log.manager), 

435 'handlers': [get_handler_report(h) for h in log.handlers], 

436 } 

437 elif isinstance(log, logging.PlaceHolder): 

438 return { 

439 "(object)": str(log), 

440 } 

441 else: 

442 raise ValueError(f"Unknown object type: {log!r}") 

443 

444 

445def print_report_on_all_logs() -> None: 

446 """ 

447 Use :func:`print` to report information on all logs. 

448 """ 

449 d = {} 

450 # noinspection PyUnresolvedReferences 

451 for name, obj in logging.Logger.manager.loggerDict.items(): 

452 d[name] = get_log_report(obj) 

453 rootlogger = logging.getLogger() 

454 d['(root logger)'] = get_log_report(rootlogger) 

455 print(json.dumps(d, sort_keys=True, indent=4, separators=(',', ': '))) 

456 

457 

458def set_level_for_logger_and_its_handlers(log: logging.Logger, 

459 level: int) -> None: 

460 """ 

461 Set a log level for a log and all its handlers. 

462 

463 Args: 

464 log: log to modify 

465 level: log level to set 

466 """ 

467 log.setLevel(level) 

468 for h in log.handlers: # type: logging.Handler 

469 h.setLevel(level) 

470 

471 

472# ============================================================================= 

473# HTML formatter 

474# ============================================================================= 

475 

476class HtmlColorFormatter(logging.Formatter): 

477 """ 

478 Class to format Python logs in coloured HTML. 

479 """ 

480 log_colors = { 

481 logging.DEBUG: '#008B8B', # dark cyan 

482 logging.INFO: '#00FF00', # green 

483 logging.WARNING: '#FFFF00', # yellow 

484 logging.ERROR: '#FF0000', # red 

485 logging.CRITICAL: '#FF0000', # red 

486 } 

487 log_background_colors = { 

488 logging.DEBUG: None, 

489 logging.INFO: None, 

490 logging.WARNING: None, 

491 logging.ERROR: None, 

492 logging.CRITICAL: '#FFFFFF', # white 

493 } 

494 

495 def __init__(self, append_br: bool = False, 

496 replace_nl_with_br: bool = True) -> None: 

497 r""" 

498 Args: 

499 append_br: append ``<br>`` to each line? 

500 replace_nl_with_br: replace ``\n`` with ``<br>`` in messages? 

501 

502 See https://hg.python.org/cpython/file/3.5/Lib/logging/__init__.py 

503 """ 

504 super().__init__( 

505 fmt='%(message)s', 

506 datefmt='%Y-%m-%d %H:%M:%S', 

507 style='%' 

508 ) 

509 self.append_br = append_br 

510 self.replace_nl_with_br = replace_nl_with_br 

511 

512 def format(self, record: logging.LogRecord) -> str: 

513 """ 

514 Internal function to format the :class:`LogRecord` as HTML. 

515 

516 See https://docs.python.org/3.4/library/logging.html#logging.LogRecord 

517 """ 

518 

519 # message = super().format(record) 

520 super().format(record) 

521 # Since fmt does not contain asctime, the Formatter.format() 

522 # will not write asctime (since its usesTime()) function will be 

523 # false. Therefore: 

524 record.asctime = self.formatTime(record, self.datefmt) 

525 bg_col = self.log_background_colors[record.levelno] 

526 msg = escape(record.getMessage()) 

527 # escape() won't replace \n but will replace & etc. 

528 if self.replace_nl_with_br: 

529 msg = msg.replace("\n", "<br>") 

530 html = ( 

531 '<span style="color:#008B8B">{time}.{ms:03d} {name}:{lvname}: ' 

532 '</span><span style="color:{color}{bg}">{msg}</font>{br}'.format( 

533 time=record.asctime, 

534 ms=int(record.msecs), 

535 name=record.name, 

536 lvname=record.levelname, 

537 color=self.log_colors[record.levelno], 

538 msg=msg, 

539 bg=f";background-color:{bg_col}" if bg_col else "", 

540 br="<br>" if self.append_br else "", 

541 ) 

542 ) 

543 # print("record.__dict__: {}".format(record.__dict__)) 

544 # print("html: {}".format(html)) 

545 return html 

546 

547 

548# ============================================================================= 

549# HtmlColorHandler 

550# ============================================================================= 

551 

552class HtmlColorHandler(logging.StreamHandler): 

553 """ 

554 HTML handler (using :class:`HtmlColorFormatter`) that sends output to a 

555 function, e.g. for display in a Qt window 

556 """ 

557 def __init__(self, logfunction: Callable[[str], None], 

558 level: int = logging.INFO) -> None: 

559 super().__init__() 

560 self.logfunction = logfunction 

561 self.setFormatter(HtmlColorFormatter()) 

562 self.setLevel(level) 

563 

564 def emit(self, record: logging.LogRecord) -> None: 

565 """ 

566 Internal function to process a :class:`LogRecord`. 

567 """ 

568 # noinspection PyBroadException 

569 try: 

570 html = self.format(record) 

571 self.logfunction(html) 

572 except: # nopep8 

573 self.handleError(record) 

574 

575 

576# ============================================================================= 

577# Brace formatters, for log.info("{}, {}", "hello", "world") 

578# ============================================================================= 

579 

580# - https://docs.python.org/3/howto/logging-cookbook.html#use-of-alternative-formatting-styles # noqa 

581# - https://stackoverflow.com/questions/13131400/logging-variable-data-with-new-format-string # noqa 

582# - https://stackoverflow.com/questions/13131400/logging-variable-data-with-new-format-string/24683360#24683360 # noqa 

583# ... plus modifications to use inspect.signature() not inspect.getargspec() 

584# ... plus a performance tweak so we're not calling signature() every time 

585# See also: 

586# - https://www.simonmweber.com/2014/11/24/python-logging-traps.html 

587 

588class BraceMessage(object): 

589 """ 

590 Class to represent a message that includes a message including braces 

591 (``{}``) and a set of ``args``/``kwargs``. When converted to a ``str``, 

592 the message is realized via ``msg.format(*args, **kwargs)``. 

593 """ 

594 def __init__(self, 

595 fmt: str, 

596 args: Tuple[Any, ...], 

597 kwargs: Dict[str, Any]) -> None: 

598 # This version uses args and kwargs, not *args and **kwargs, for 

599 # performance reasons: 

600 # https://stackoverflow.com/questions/31992424/performance-implications-of-unpacking-dictionaries-in-python # noqa 

601 # ... and since we control creation entirely, we may as well go fast 

602 self.fmt = fmt 

603 self.args = args 

604 self.kwargs = kwargs 

605 # print("Creating BraceMessage with: fmt={}, args={}, " 

606 # "kwargs={}".format(repr(fmt), repr(args), repr(kwargs))) 

607 

608 def __str__(self) -> str: 

609 return self.fmt.format(*self.args, **self.kwargs) 

610 

611 

612class BraceStyleAdapter(logging.LoggerAdapter): 

613 def __init__(self, 

614 logger: logging.Logger, 

615 pass_special_logger_args: bool = True, 

616 strip_special_logger_args_from_fmt: bool = False) -> None: 

617 """ 

618 Wraps a logger so we can use ``{}``-style string formatting. 

619 

620 Args: 

621 logger: 

622 a logger 

623 pass_special_logger_args: 

624 should we continue to pass any special arguments to the logger 

625 itself? True is standard; False probably brings a slight 

626 performance benefit, but prevents log.exception() from working 

627 properly, as the 'exc_info' parameter will be stripped. 

628 strip_special_logger_args_from_fmt: 

629 If we're passing special arguments to the logger, should we 

630 remove them from the argments passed to the string formatter? 

631 There is no obvious cost to saying no. 

632  

633 Specimen use: 

634  

635 .. code-block:: python 

636  

637 import logging 

638 from cardinal_pythonlib.logs import BraceStyleAdapter, main_only_quicksetup_rootlogger  

639  

640 log = BraceStyleAdapter(logging.getLogger(__name__)) 

641  

642 main_only_quicksetup_rootlogger(level=logging.DEBUG) 

643  

644 log.info("Hello {}, {title} {surname}!", "world", title="Mr", surname="Smith")  

645 # 2018-09-17 16:13:50.404 __main__:INFO: Hello world, Mr Smith! 

646  

647 """ # noqa 

648 # noinspection PyTypeChecker 

649 super().__init__(logger=logger, extra=None) 

650 self.pass_special_logger_args = pass_special_logger_args 

651 self.strip_special_logger_args_from_fmt = strip_special_logger_args_from_fmt # noqa 

652 # getargspec() returns: 

653 # named tuple: ArgSpec(args, varargs, keywords, defaults) 

654 # ... args = list of parameter names 

655 # ... varargs = names of the * parameters, or None 

656 # ... keywords = names of the ** parameters, or None 

657 # ... defaults = tuple of default argument values, or None 

658 # signature() returns a Signature object: 

659 # ... parameters: ordered mapping of name -> Parameter 

660 # ... ... https://docs.python.org/3/library/inspect.html#inspect.Parameter # noqa 

661 # Direct equivalence: 

662 # https://github.com/praw-dev/praw/issues/541 

663 # So, old: 

664 # logargnames = getargspec(self.logger._log).args[1:] 

665 # and new: 

666 # noinspection PyProtectedMember 

667 sig = signature(self.logger._log) 

668 self.logargnames = [p.name for p in sig.parameters.values() 

669 if p.kind == Parameter.POSITIONAL_OR_KEYWORD] 

670 # e.g.: ['level', 'msg', 'args', 'exc_info', 'extra', 'stack_info'] 

671 # print("self.logargnames: " + repr(self.logargnames)) 

672 

673 def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None: 

674 if self.isEnabledFor(level): 

675 # print("log: msg={}, args={}, kwargs={}".format( 

676 # repr(msg), repr(args), repr(kwargs))) 

677 if self.pass_special_logger_args: 

678 msg, log_kwargs = self.process(msg, kwargs) 

679 # print("... log: msg={}, log_kwargs={}".format( 

680 # repr(msg), repr(log_kwargs))) 

681 else: 

682 log_kwargs = {} 

683 # noinspection PyProtectedMember 

684 self.logger._log(level, BraceMessage(msg, args, kwargs), (), 

685 **log_kwargs) 

686 

687 def process(self, msg: str, 

688 kwargs: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]: 

689 special_param_names = [k for k in kwargs.keys() 

690 if k in self.logargnames] 

691 log_kwargs = {k: kwargs[k] for k in special_param_names} 

692 # ... also: remove them from the starting kwargs? 

693 if self.strip_special_logger_args_from_fmt: 

694 for k in special_param_names: 

695 kwargs.pop(k) 

696 return msg, log_kwargs 

697 

698 

699def get_log_with_null_handler(name: str) -> logging.Logger: 

700 """ 

701 For use by library functions. Returns a log with the specifed name that 

702 has a null handler attached, and a :class:`BraceStyleAdapter`. 

703 """ 

704 log = logging.getLogger(name) 

705 log.addHandler(logging.NullHandler()) 

706 return log 

707 

708 

709def get_brace_style_log_with_null_handler(name: str) -> BraceStyleAdapter: 

710 """ 

711 For use by library functions. Returns a log with the specifed name that 

712 has a null handler attached, and a :class:`BraceStyleAdapter`. 

713 """ 

714 log = logging.getLogger(name) 

715 log.addHandler(logging.NullHandler()) 

716 return BraceStyleAdapter(log) 

717 

718 

719# ============================================================================= 

720# Testing 

721# ============================================================================= 

722 

723if __name__ == '__main__': 

724 """ 

725 Command-line validation checks. 

726 """ 

727 main_only_quicksetup_rootlogger(logging.INFO) 

728 _log = BraceStyleAdapter(logging.getLogger(__name__)) 

729 _log.info("1. Hello!") 

730 _log.info("1. Hello, {}!", "world") 

731 _log.info("1. Hello, foo={foo}, bar={bar}!", foo="foo", bar="bar") 

732 _log.info("1. Hello, {}; foo={foo}, bar={bar}!", "world", foo="foo", 

733 bar="bar") 

734 _log.info("1. Hello, {}; foo={foo}, bar={bar}!", "world", foo="foo", 

735 bar="bar", extra={'somekey': 'somevalue'})