Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/dev.py: 69%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

363 statements  

1# SPDX-License-Identifier: MIT OR Apache-2.0 

2# This file is dual licensed under the terms of the Apache License, Version 

3# 2.0, and the MIT License. See the LICENSE file in the root of this 

4# repository for complete details. 

5 

6""" 

7Helpers that make development with *structlog* more pleasant. 

8 

9See also the narrative documentation in `console-output`. 

10""" 

11 

12from __future__ import annotations 

13 

14import sys 

15import warnings 

16 

17from collections.abc import Callable, Sequence 

18from dataclasses import dataclass 

19from io import StringIO 

20from types import ModuleType 

21from typing import ( 

22 Any, 

23 Literal, 

24 Protocol, 

25 TextIO, 

26 cast, 

27) 

28 

29from ._frames import _format_exception 

30from .exceptions import ( 

31 MultipleConsoleRenderersConfiguredError, 

32 NoConsoleRendererConfiguredError, 

33) 

34from .processors import _figure_out_exc_info 

35from .typing import EventDict, ExceptionRenderer, ExcInfo, WrappedLogger 

36 

37 

38try: 

39 import colorama 

40except ImportError: 

41 colorama = None 

42 

43try: 

44 import better_exceptions 

45except ImportError: 

46 better_exceptions = None 

47 

48try: 

49 import rich 

50 

51 from rich.console import Console 

52 from rich.traceback import Traceback 

53except ImportError: 

54 rich = None # type: ignore[assignment] 

55 

56__all__ = [ 

57 "ConsoleRenderer", 

58 "RichTracebackFormatter", 

59 "better_traceback", 

60 "plain_traceback", 

61 "rich_traceback", 

62] 

63 

64_IS_WINDOWS = sys.platform == "win32" 

65 

66_MISSING = "{who} requires the {package} package installed. " 

67_EVENT_WIDTH = 30 # pad the event name to so many characters 

68 

69 

70if _IS_WINDOWS: # pragma: no cover 

71 

72 def _init_terminal(who: str, force_colors: bool) -> None: 

73 """ 

74 Initialize colorama on Windows systems for colorful console output. 

75 

76 Args: 

77 who: The name of the caller for error messages. 

78 

79 force_colors: 

80 Force colorful output even in non-interactive environments. 

81 

82 Raises: 

83 SystemError: 

84 When colorama is not installed. 

85 """ 

86 # On Windows, we can't do colorful output without colorama. 

87 if colorama is None: 

88 raise SystemError( 

89 _MISSING.format( 

90 who=who + " with `colors=True` on Windows", 

91 package="colorama", 

92 ) 

93 ) 

94 # Colorama must be init'd on Windows, but must NOT be 

95 # init'd on other OSes, because it can break colors. 

96 if force_colors: 

97 colorama.deinit() 

98 colorama.init(strip=False) 

99 else: 

100 colorama.init() 

101else: 

102 

103 def _init_terminal(who: str, force_colors: bool) -> None: 

104 """ 

105 Currently, nothing to be done on non-Windows systems. 

106 """ 

107 

108 

109def _pad(s: str, length: int) -> str: 

110 """ 

111 Pads *s* to length *length*. 

112 """ 

113 missing = length - len(s) 

114 

115 return s + " " * (max(0, missing)) 

116 

117 

118if colorama is not None: 

119 RESET_ALL = colorama.Style.RESET_ALL 

120 BRIGHT = colorama.Style.BRIGHT 

121 DIM = colorama.Style.DIM 

122 RED = colorama.Fore.RED 

123 BLUE = colorama.Fore.BLUE 

124 CYAN = colorama.Fore.CYAN 

125 MAGENTA = colorama.Fore.MAGENTA 

126 YELLOW = colorama.Fore.YELLOW 

127 GREEN = colorama.Fore.GREEN 

128 RED_BACK = colorama.Back.RED 

129else: 

130 # These are the same values as the Colorama color codes. Redefining them 

131 # here allows users to specify that they want color without having to 

132 # install Colorama, which is only supposed to be necessary in Windows. 

133 RESET_ALL = "\033[0m" 

134 BRIGHT = "\033[1m" 

135 DIM = "\033[2m" 

136 RED = "\033[31m" 

137 BLUE = "\033[34m" 

138 CYAN = "\033[36m" 

139 MAGENTA = "\033[35m" 

140 YELLOW = "\033[33m" 

141 GREEN = "\033[32m" 

142 RED_BACK = "\033[41m" 

143 

144# On Windows, colors are only available if Colorama is installed. 

145_has_colors = not _IS_WINDOWS or colorama is not None 

146 

147# Prevent breakage of packages that used the old name of the variable. 

148_use_colors = _has_colors 

149 

150 

151@dataclass(frozen=True) 

152class ColumnStyles: 

153 """ 

154 Column styles settings for console rendering. 

155 

156 These are console ANSI codes that are printed before the respective fields. 

157 This allows for a certain amount of customization if you don't want to 

158 configure your columns. 

159 

160 .. versionadded:: 25.5.0 

161 It was handled by private structures before. 

162 """ 

163 

164 reset: str 

165 bright: str 

166 

167 level_critical: str 

168 level_exception: str 

169 level_error: str 

170 level_warn: str 

171 level_info: str 

172 level_debug: str 

173 level_notset: str 

174 

175 timestamp: str 

176 logger_name: str 

177 kv_key: str 

178 kv_value: str 

179 

180 

181_colorful_styles = ColumnStyles( 

182 reset=RESET_ALL, 

183 bright=BRIGHT, 

184 level_critical=RED, 

185 level_exception=RED, 

186 level_error=RED, 

187 level_warn=YELLOW, 

188 level_info=GREEN, 

189 level_debug=GREEN, 

190 level_notset=RED_BACK, 

191 timestamp=DIM, 

192 logger_name=BLUE, 

193 kv_key=CYAN, 

194 kv_value=MAGENTA, 

195) 

196 

197_plain_styles = ColumnStyles( 

198 reset="", 

199 bright="", 

200 level_critical="", 

201 level_exception="", 

202 level_error="", 

203 level_warn="", 

204 level_info="", 

205 level_debug="", 

206 level_notset="", 

207 timestamp="", 

208 logger_name="", 

209 kv_key="", 

210 kv_value="", 

211) 

212 

213# Backward compatibility aliases 

214_ColorfulStyles = _colorful_styles 

215_PlainStyles = _plain_styles 

216 

217 

218class ColumnFormatter(Protocol): 

219 """ 

220 :class:`~typing.Protocol` for column formatters. 

221 

222 See `KeyValueColumnFormatter` and `LogLevelColumnFormatter` for examples. 

223 

224 .. versionadded:: 23.3.0 

225 """ 

226 

227 def __call__(self, key: str, value: object) -> str: 

228 """ 

229 Format *value* for *key*. 

230 

231 This method is responsible for formatting, *key*, the ``=``, and the 

232 *value*. That means that it can use any string instead of the ``=`` and 

233 it can leave out both the *key* or the *value*. 

234 

235 If it returns an empty string, the column is omitted completely. 

236 """ 

237 

238 

239@dataclass 

240class Column: 

241 """ 

242 A column defines the way a key-value pair is formatted, and, by it's 

243 position to the *columns* argument of `ConsoleRenderer`, the order in which 

244 it is rendered. 

245 

246 Args: 

247 key: 

248 The key for which this column is responsible. Leave empty to define 

249 it as the default formatter. 

250 

251 formatter: The formatter for columns with *key*. 

252 

253 .. versionadded:: 23.3.0 

254 """ 

255 

256 key: str 

257 formatter: ColumnFormatter 

258 

259 

260@dataclass 

261class KeyValueColumnFormatter: 

262 """ 

263 Format a key-value pair. 

264 

265 Args: 

266 key_style: The style to apply to the key. If None, the key is omitted. 

267 

268 value_style: The style to apply to the value. 

269 

270 reset_style: The style to apply whenever a style is no longer needed. 

271 

272 value_repr: 

273 A callable that returns the string representation of the value. 

274 

275 width: The width to pad the value to. If 0, no padding is done. 

276 

277 prefix: 

278 A string to prepend to the formatted key-value pair. May contain 

279 styles. 

280 

281 postfix: 

282 A string to append to the formatted key-value pair. May contain 

283 styles. 

284 

285 .. versionadded:: 23.3.0 

286 """ 

287 

288 key_style: str | None 

289 value_style: str 

290 reset_style: str 

291 value_repr: Callable[[object], str] 

292 width: int = 0 

293 prefix: str = "" 

294 postfix: str = "" 

295 

296 def __call__(self, key: str, value: object) -> str: 

297 sio = StringIO() 

298 

299 if self.prefix: 

300 sio.write(self.prefix) 

301 sio.write(self.reset_style) 

302 

303 if self.key_style is not None: 

304 sio.write(self.key_style) 

305 sio.write(key) 

306 sio.write(self.reset_style) 

307 sio.write("=") 

308 

309 sio.write(self.value_style) 

310 sio.write(_pad(self.value_repr(value), self.width)) 

311 sio.write(self.reset_style) 

312 

313 if self.postfix: 

314 sio.write(self.postfix) 

315 sio.write(self.reset_style) 

316 

317 return sio.getvalue() 

318 

319 

320class LogLevelColumnFormatter: 

321 """ 

322 Format a log level according to *level_styles*. 

323 

324 The width is padded to the longest level name (if *level_styles* is passed 

325 -- otherwise there's no way to know the lengths of all levels). 

326 

327 Args: 

328 level_styles: 

329 A dictionary of level names to styles that are applied to it. If 

330 None, the level is formatted as a plain ``[level]``. 

331 

332 reset_style: 

333 What to use to reset the style after the level name. Ignored if 

334 if *level_styles* is None. 

335 

336 width: 

337 The width to pad the level to. If 0, no padding is done. 

338 

339 .. versionadded:: 23.3.0 

340 .. versionadded:: 24.2.0 *width* 

341 """ 

342 

343 level_styles: dict[str, str] | None 

344 reset_style: str 

345 width: int 

346 

347 def __init__( 

348 self, 

349 level_styles: dict[str, str], 

350 reset_style: str, 

351 width: int | None = None, 

352 ) -> None: 

353 self.level_styles = level_styles 

354 if level_styles: 

355 self.width = ( 

356 0 

357 if width == 0 

358 else len(max(self.level_styles.keys(), key=len)) 

359 ) 

360 self.reset_style = reset_style 

361 else: 

362 self.width = 0 

363 self.reset_style = "" 

364 

365 def __call__(self, key: str, value: object) -> str: 

366 level = cast(str, value) 

367 style = ( 

368 "" 

369 if self.level_styles is None 

370 else self.level_styles.get(level, "") 

371 ) 

372 

373 return f"[{style}{_pad(level, self.width)}{self.reset_style}]" 

374 

375 

376_NOTHING = object() 

377 

378 

379def plain_traceback(sio: TextIO, exc_info: ExcInfo) -> None: 

380 """ 

381 "Pretty"-print *exc_info* to *sio* using our own plain formatter. 

382 

383 To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument. 

384 

385 Used by default if neither Rich nor *better-exceptions* are present. 

386 

387 .. versionadded:: 21.2.0 

388 """ 

389 sio.write("\n" + _format_exception(exc_info)) 

390 

391 

392@dataclass 

393class RichTracebackFormatter: 

394 """ 

395 A Rich traceback renderer with the given options. 

396 

397 Pass an instance as `ConsoleRenderer`'s ``exception_formatter`` argument. 

398 

399 See :class:`rich.traceback.Traceback` for details on the arguments. 

400 

401 If *width* is `None`, the terminal width is used. If the width can't be 

402 determined, fall back to 80. 

403 

404 .. versionadded:: 23.2.0 

405 

406 .. versionchanged:: 25.4.0 

407 Default *width* is ``None`` to have full width and reflow support. 

408 Passing ``-1`` as width is deprecated, use ``None`` instead. 

409 *word_wrap* is now True by default. 

410 

411 .. versionadded:: 25.4.0 *code_width* 

412 

413 .. versionchanged:: 26.1.0 

414 ``None`` is now valid for *color_system* and disables color output. 

415 """ 

416 

417 color_system: ( 

418 Literal["auto", "standard", "256", "truecolor", "windows"] | None 

419 ) = "truecolor" 

420 show_locals: bool = True 

421 max_frames: int = 100 

422 theme: str | None = None 

423 word_wrap: bool = True 

424 extra_lines: int = 3 

425 width: int | None = None 

426 code_width: int | None = 88 

427 indent_guides: bool = True 

428 locals_max_length: int = 10 

429 locals_max_string: int = 80 

430 locals_hide_dunder: bool = True 

431 locals_hide_sunder: bool = False 

432 suppress: Sequence[str | ModuleType] = () 

433 

434 def __call__(self, sio: TextIO, exc_info: ExcInfo) -> None: 

435 if self.width == -1: 

436 warnings.warn( 

437 "Use None to use the terminal width instead of -1.", 

438 DeprecationWarning, 

439 stacklevel=2, 

440 ) 

441 self.width = None 

442 

443 sio.write("\n") 

444 

445 console = Console( 

446 file=sio, color_system=self.color_system, width=self.width 

447 ) 

448 tb = Traceback.from_exception( 

449 *exc_info, 

450 show_locals=self.show_locals, 

451 max_frames=self.max_frames, 

452 theme=self.theme, 

453 word_wrap=self.word_wrap, 

454 extra_lines=self.extra_lines, 

455 width=self.width, 

456 indent_guides=self.indent_guides, 

457 locals_max_length=self.locals_max_length, 

458 locals_max_string=self.locals_max_string, 

459 locals_hide_dunder=self.locals_hide_dunder, 

460 locals_hide_sunder=self.locals_hide_sunder, 

461 suppress=self.suppress, 

462 ) 

463 if hasattr(tb, "code_width"): 

464 # `code_width` requires `rich>=13.8.0` 

465 tb.code_width = self.code_width 

466 console.print(tb) 

467 

468 

469if rich is None: 

470 

471 def rich_traceback(*args, **kw): 

472 raise ModuleNotFoundError( 

473 "RichTracebackFormatter requires Rich to be installed.", 

474 name="rich", 

475 ) 

476 

477 rich_monochrome_traceback = rich_traceback 

478 

479else: 

480 rich_traceback = RichTracebackFormatter() 

481 """ 

482 Pretty-print *exc_info* to *sio* using the Rich package. 

483 

484 To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument. 

485 

486 This is a `RichTracebackFormatter` with default arguments and used by default 

487 if Rich is installed. 

488 

489 .. versionadded:: 21.2.0 

490 """ 

491 

492 rich_monochrome_traceback = RichTracebackFormatter(color_system=None) 

493 """ 

494 Pretty-print *exc_info* to *sio* using the Rich package w/o colors. 

495 

496 To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument. 

497 

498 This is a `RichTracebackFormatter` with default arguments except for 

499 ``color_system=None``, and used by default if Rich is installed and colors 

500 are disabled. 

501 

502 .. versionadded:: 26.1.0 

503 """ 

504 

505 

506def better_traceback(sio: TextIO, exc_info: ExcInfo) -> None: 

507 """ 

508 Pretty-print *exc_info* to *sio* using the *better-exceptions* package. 

509 

510 To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument. 

511 

512 Used by default if *better-exceptions* is installed and Rich is absent. 

513 

514 .. versionadded:: 21.2.0 

515 .. deprecated:: 26.1.0 

516 *better-exceptions* support is deprecated and will be removed in a 

517 future release. Use Rich instead. 

518 """ 

519 warnings.warn( 

520 "better-exceptions support is deprecated and will be removed " 

521 "in a future release. Use Rich instead.", 

522 DeprecationWarning, 

523 stacklevel=2, 

524 ) 

525 sio.write("\n" + "".join(better_exceptions.format_exception(*exc_info))) 

526 

527 

528if rich is not None: 

529 default_exception_formatter = rich_traceback 

530 default_monochrome_exception_formatter = rich_monochrome_traceback 

531elif better_exceptions is not None: 

532 default_exception_formatter = default_monochrome_exception_formatter = ( 

533 better_traceback 

534 ) 

535 warnings.warn( 

536 "better-exceptions support is deprecated and will be removed " 

537 "in a future release. Use Rich instead.", 

538 DeprecationWarning, 

539 stacklevel=2, 

540 ) 

541else: 

542 default_exception_formatter = default_monochrome_exception_formatter = ( 

543 plain_traceback 

544 ) 

545 

546 

547class ConsoleRenderer: 

548 r""" 

549 Render ``event_dict`` nicely aligned, possibly in colors, and ordered. 

550 

551 If ``event_dict`` contains a true-ish ``exc_info`` key, it will be rendered 

552 *after* the log line. If Rich_ is present, in colors and with extra 

553 context. 

554 

555 Tip: 

556 Since `ConsoleRenderer` is mainly a development helper, it is less 

557 strict about immutability than the rest of *structlog* for better 

558 ergonomics. Notably, the currently active instance can be obtained by 

559 calling `ConsoleRenderer.get_active()` and it offers properties to 

560 configure its behavior after instantiation. 

561 

562 Args: 

563 columns: 

564 A list of `Column` objects defining both the order and format of 

565 the key-value pairs in the output. If passed, most other arguments 

566 become meaningless. 

567 

568 **Must** contain a column with ``key=''`` that defines the default 

569 formatter. 

570 

571 .. seealso:: `columns-config` 

572 

573 pad_event_to: 

574 Pad the event to this many characters. Ignored if *columns* are 

575 passed. 

576 

577 colors: 

578 Use colors for a nicer output. `True` by default. On Windows only 

579 if Colorama_ is installed. Ignored if *columns* are passed. 

580 

581 force_colors: 

582 Force colors even for non-tty destinations. Use this option if your 

583 logs are stored in a file that is meant to be streamed to the 

584 console. Only meaningful on Windows. Ignored if *columns* are 

585 passed. 

586 

587 repr_native_str: 

588 When `True`, `repr` is also applied to ``str``\ s. The ``event`` 

589 key is *never* `repr` -ed. Ignored if *columns* are passed. 

590 

591 level_styles: 

592 When present, use these styles for colors. This must be a dict from 

593 level names (strings) to terminal sequences (for example, Colorama) 

594 styles. The default can be obtained by calling 

595 `ConsoleRenderer.get_default_level_styles`. Ignored when *columns* 

596 are passed. 

597 

598 exception_formatter: 

599 A callable to render ``exc_infos``. If Rich_ is installed, it is 

600 used for pretty-printing by default. You can also manually set it 

601 to `plain_traceback`, an instance of `RichTracebackFormatter` like 

602 `rich_traceback`, or implement your own. 

603 

604 sort_keys: 

605 Whether to sort keys when formatting. `True` by default. Ignored if 

606 *columns* are passed. 

607 

608 event_key: 

609 The key to look for the main log message. Needed when you rename it 

610 e.g. using `structlog.processors.EventRenamer`. Ignored if 

611 *columns* are passed. 

612 

613 timestamp_key: 

614 The key to look for timestamp of the log message. Needed when you 

615 rename it e.g. using `structlog.processors.EventRenamer`. Ignored 

616 if *columns* are passed. 

617 

618 pad_level: 

619 Whether to pad log level with blanks to the longest amongst all 

620 level label. 

621 

622 Requires the Colorama_ package if *colors* is `True` **on Windows**. 

623 

624 Raises: 

625 ValueError: If there's not exactly one default column formatter. 

626 

627 .. _Colorama: https://pypi.org/project/colorama/ 

628 .. _better-exceptions: https://pypi.org/project/better-exceptions/ 

629 .. _Rich: https://pypi.org/project/rich/ 

630 

631 .. versionadded:: 16.0.0 

632 .. versionadded:: 16.1.0 *colors* 

633 .. versionadded:: 17.1.0 *repr_native_str* 

634 .. versionadded:: 18.1.0 *force_colors* 

635 .. versionadded:: 18.1.0 *level_styles* 

636 .. versionchanged:: 19.2.0 

637 Colorama now initializes lazily to avoid unwanted initializations as 

638 ``ConsoleRenderer`` is used by default. 

639 .. versionchanged:: 19.2.0 Can be pickled now. 

640 .. versionchanged:: 20.1.0 

641 Colorama does not initialize lazily on Windows anymore because it breaks 

642 rendering. 

643 .. versionchanged:: 21.1.0 

644 It is additionally possible to set the logger name using the 

645 ``logger_name`` key in the ``event_dict``. 

646 .. versionadded:: 21.2.0 *exception_formatter* 

647 .. versionchanged:: 21.2.0 

648 `ConsoleRenderer` now handles the ``exc_info`` event dict key itself. Do 

649 **not** use the `structlog.processors.format_exc_info` processor 

650 together with `ConsoleRenderer` anymore! It will keep working, but you 

651 can't have customize exception formatting and a warning will be raised 

652 if you ask for it. 

653 .. versionchanged:: 21.2.0 

654 The colors keyword now defaults to True on non-Windows systems, and 

655 either True or False in Windows depending on whether Colorama is 

656 installed. 

657 .. versionadded:: 21.3.0 *sort_keys* 

658 .. versionadded:: 22.1.0 *event_key* 

659 .. versionadded:: 23.2.0 *timestamp_key* 

660 .. versionadded:: 23.3.0 *columns* 

661 .. versionadded:: 24.2.0 *pad_level* 

662 .. versionchanged:: 26.1.0 

663 The default exception formatter is now monochrome if colors are disabled. 

664 """ 

665 

666 _default_column_formatter: ColumnFormatter 

667 

668 def __init__( 

669 self, 

670 pad_event_to: int = _EVENT_WIDTH, 

671 colors: bool = _has_colors, 

672 force_colors: bool = False, 

673 repr_native_str: bool = False, 

674 level_styles: dict[str, str] | None = None, 

675 exception_formatter: ExceptionRenderer = default_exception_formatter, 

676 sort_keys: bool = True, 

677 event_key: str = "event", 

678 timestamp_key: str = "timestamp", 

679 columns: list[Column] | None = None, 

680 pad_level: bool = True, 

681 pad_event: int | None = None, 

682 ): 

683 if pad_event is not None: 

684 if pad_event_to != _EVENT_WIDTH: 

685 raise ValueError( 

686 "Cannot set both `pad_event` and `pad_event_to`." 

687 ) 

688 warnings.warn( 

689 "The `pad_event` argument is deprecated. Use `pad_event_to` instead.", 

690 DeprecationWarning, 

691 stacklevel=2, 

692 ) 

693 pad_event_to = pad_event 

694 

695 # Store all settings in case the user later switches from columns to 

696 # defaults. 

697 self.exception_formatter = exception_formatter 

698 self._sort_keys = sort_keys 

699 self._repr_native_str = repr_native_str 

700 self._styles = self.get_default_column_styles(colors, force_colors) 

701 self._colors = colors 

702 self._force_colors = force_colors 

703 self._level_styles = ( 

704 self.get_default_level_styles(colors) 

705 if level_styles is None 

706 else level_styles 

707 ) 

708 self._pad_event_to = pad_event_to 

709 self._timestamp_key = timestamp_key 

710 self._event_key = event_key 

711 self._pad_level = pad_level 

712 

713 if exception_formatter is default_exception_formatter and not colors: 

714 self.exception_formatter = default_monochrome_exception_formatter 

715 

716 if columns is None: 

717 self._configure_columns() 

718 return 

719 

720 self.columns = columns 

721 

722 to_warn = [] 

723 

724 def add_meaningless_arg(arg: str) -> None: 

725 to_warn.append( 

726 f"The `{arg}` argument is ignored when passing `columns`.", 

727 ) 

728 

729 if pad_event_to != _EVENT_WIDTH: 

730 add_meaningless_arg("pad_event_to") 

731 

732 if colors != _has_colors: 

733 add_meaningless_arg("colors") 

734 

735 if force_colors is not False: 

736 add_meaningless_arg("force_colors") 

737 

738 if repr_native_str is not False: 

739 add_meaningless_arg("repr_native_str") 

740 

741 if level_styles is not None: 

742 add_meaningless_arg("level_styles") 

743 

744 if event_key != "event": 

745 add_meaningless_arg("event_key") 

746 

747 if timestamp_key != "timestamp": 

748 add_meaningless_arg("timestamp_key") 

749 

750 for w in to_warn: 

751 warnings.warn(w, stacklevel=2) 

752 

753 @classmethod 

754 def get_active(cls) -> ConsoleRenderer: 

755 """ 

756 If *structlog* is configured to use `ConsoleRenderer`, it's returned. 

757 

758 It does not have to be the last processor. 

759 

760 Raises: 

761 NoConsoleRendererConfiguredError: 

762 If no ConsoleRenderer is found in the current configuration. 

763 

764 MultipleConsoleRenderersConfiguredError: 

765 If more than one is found in the current configuration. This is 

766 almost certainly a bug. 

767 

768 .. versionadded:: 25.5.0 

769 """ 

770 from ._config import get_config 

771 

772 cr = None 

773 for p in get_config()["processors"]: 

774 if isinstance(p, ConsoleRenderer): 

775 if cr is not None: 

776 raise MultipleConsoleRenderersConfiguredError 

777 

778 cr = p 

779 

780 if cr is None: 

781 raise NoConsoleRendererConfiguredError 

782 

783 return cr 

784 

785 @classmethod 

786 def get_default_column_styles( 

787 cls, colors: bool, force_colors: bool = False 

788 ) -> ColumnStyles: 

789 """ 

790 Configure and return the appropriate styles class for console output. 

791 

792 This method handles the setup of colorful or plain styles, including 

793 proper colorama initialization on Windows systems when colors are 

794 enabled. 

795 

796 Args: 

797 colors: Whether to use colorful output styles. 

798 

799 force_colors: 

800 Force colorful output even in non-interactive environments. 

801 Only relevant on Windows with colorama. 

802 

803 Returns: 

804 The configured styles. 

805 

806 Raises: 

807 SystemError: 

808 On Windows when colors=True but colorama is not installed. 

809 

810 .. versionadded:: 25.5.0 

811 """ 

812 if not colors: 

813 return _plain_styles 

814 

815 _init_terminal(cls.__name__, force_colors) 

816 

817 return _colorful_styles 

818 

819 @staticmethod 

820 def get_default_level_styles(colors: bool = True) -> dict[str, str]: 

821 """ 

822 Get the default styles for log levels 

823 

824 This is intended to be used with `ConsoleRenderer`'s ``level_styles`` 

825 parameter. For example, if you are adding custom levels in your 

826 home-grown :func:`~structlog.stdlib.add_log_level` you could do:: 

827 

828 my_styles = ConsoleRenderer.get_default_level_styles() 

829 my_styles["EVERYTHING_IS_ON_FIRE"] = my_styles["critical"] 

830 renderer = ConsoleRenderer(level_styles=my_styles) 

831 

832 Args: 

833 colors: 

834 Whether to use colorful styles. This must match the *colors* 

835 parameter to `ConsoleRenderer`. Default: `True`. 

836 """ 

837 styles: ColumnStyles 

838 styles = _colorful_styles if colors else _plain_styles 

839 return { 

840 "critical": styles.level_critical, 

841 "exception": styles.level_exception, 

842 "error": styles.level_error, 

843 "warn": styles.level_warn, 

844 "warning": styles.level_warn, 

845 "info": styles.level_info, 

846 "debug": styles.level_debug, 

847 "notset": styles.level_notset, 

848 } 

849 

850 def _configure_columns(self) -> None: 

851 """ 

852 Re-configure self._columns and self._default_column_formatter 

853 according to our current settings. 

854 

855 Overwrite existing columns settings, regardless of whether they were 

856 explicitly passed by the user or derived by us. 

857 """ 

858 level_to_color = self._level_styles.copy() 

859 

860 for key in level_to_color: 

861 level_to_color[key] += self._styles.bright 

862 self._longest_level = len(max(level_to_color.keys(), key=len)) 

863 

864 self._default_column_formatter = KeyValueColumnFormatter( 

865 self._styles.kv_key, 

866 self._styles.kv_value, 

867 self._styles.reset, 

868 value_repr=self._repr, 

869 width=0, 

870 ) 

871 

872 logger_name_formatter = KeyValueColumnFormatter( 

873 key_style=None, 

874 value_style=self._styles.bright + self._styles.logger_name, 

875 reset_style=self._styles.reset, 

876 value_repr=str, 

877 prefix="[", 

878 postfix="]", 

879 ) 

880 

881 level_width = 0 if not self._pad_level else None 

882 

883 self._columns = [ 

884 Column( 

885 self._timestamp_key, 

886 KeyValueColumnFormatter( 

887 key_style=None, 

888 value_style=self._styles.timestamp, 

889 reset_style=self._styles.reset, 

890 value_repr=str, 

891 ), 

892 ), 

893 Column( 

894 "level", 

895 LogLevelColumnFormatter( 

896 level_to_color, 

897 reset_style=self._styles.reset, 

898 width=level_width, 

899 ), 

900 ), 

901 Column( 

902 self._event_key, 

903 KeyValueColumnFormatter( 

904 key_style=None, 

905 value_style=self._styles.bright, 

906 reset_style=self._styles.reset, 

907 value_repr=str, 

908 width=self._pad_event_to, 

909 ), 

910 ), 

911 Column("logger", logger_name_formatter), 

912 Column("logger_name", logger_name_formatter), 

913 ] 

914 

915 def _repr(self, val: Any) -> str: 

916 """ 

917 Determine representation of *val* depending on its type & 

918 self._repr_native_str. 

919 """ 

920 if self._repr_native_str is True: 

921 return repr(val) 

922 

923 if isinstance(val, str): 

924 if set(val) & {" ", "\t", "=", "\r", "\n", '"', "'"}: 

925 return repr(val) 

926 return val 

927 

928 return repr(val) 

929 

930 def __call__( 

931 self, logger: WrappedLogger, name: str, event_dict: EventDict 

932 ) -> str: 

933 stack = event_dict.pop("stack", None) 

934 exc = event_dict.pop("exception", None) 

935 exc_info = event_dict.pop("exc_info", None) 

936 

937 kvs = [ 

938 col.formatter(col.key, val) 

939 for col in self.columns 

940 if (val := event_dict.pop(col.key, _NOTHING)) is not _NOTHING 

941 ] + [ 

942 self._default_column_formatter(key, event_dict[key]) 

943 for key in (sorted(event_dict) if self._sort_keys else event_dict) 

944 ] 

945 

946 sio = StringIO() 

947 sio.write((" ".join(kv for kv in kvs if kv)).rstrip(" ")) 

948 

949 if stack is not None: 

950 sio.write("\n" + stack) 

951 if exc_info or exc is not None: 

952 sio.write("\n\n" + "=" * 79 + "\n") 

953 

954 exc_info = _figure_out_exc_info(exc_info) 

955 if exc_info: 

956 self._exception_formatter(sio, exc_info) 

957 elif exc is not None: 

958 sio.write("\n" + exc) 

959 

960 return sio.getvalue() 

961 

962 @property 

963 def exception_formatter(self) -> ExceptionRenderer: 

964 """ 

965 The exception formatter used by this console renderer. 

966 

967 .. versionadded:: 25.5.0 

968 """ 

969 return self._exception_formatter 

970 

971 @exception_formatter.setter 

972 def exception_formatter(self, value: ExceptionRenderer) -> None: 

973 """ 

974 .. versionadded:: 25.5.0 

975 """ 

976 self._exception_formatter = value 

977 

978 @property 

979 def sort_keys(self) -> bool: 

980 """ 

981 Whether to sort keys when formatting. 

982 

983 .. versionadded:: 25.5.0 

984 """ 

985 return self._sort_keys 

986 

987 @sort_keys.setter 

988 def sort_keys(self, value: bool) -> None: 

989 """ 

990 .. versionadded:: 25.5.0 

991 """ 

992 # _sort_keys is a format-time setting, so we can just set it directly. 

993 self._sort_keys = value 

994 

995 @property 

996 def columns(self) -> list[Column]: 

997 """ 

998 The columns configuration for this console renderer. 

999 

1000 Warning: 

1001 Just like with passing *columns* argument, many of the other 

1002 arguments you may have passed are ignored. 

1003 

1004 Args: 

1005 value: 

1006 A list of `Column` objects defining both the order and format 

1007 of the key-value pairs in the output. 

1008 

1009 **Must** contain a column with ``key=''`` that defines the 

1010 default formatter. 

1011 

1012 Raises: 

1013 ValueError: If there's not exactly one default column formatter. 

1014 

1015 .. versionadded:: 25.5.0 

1016 """ 

1017 return [Column("", self._default_column_formatter), *self._columns] 

1018 

1019 @columns.setter 

1020 def columns(self, value: list[Column]) -> None: 

1021 """ 

1022 .. versionadded:: 25.5.0 

1023 """ 

1024 defaults = [col for col in value if col.key == ""] 

1025 if not defaults: 

1026 raise ValueError( 

1027 "Must pass a default column formatter (a column with `key=''`)." 

1028 ) 

1029 if len(defaults) > 1: 

1030 raise ValueError("Only one default column formatter allowed.") 

1031 

1032 self._default_column_formatter = defaults[0].formatter 

1033 self._columns = [col for col in value if col.key] 

1034 

1035 @property 

1036 def colors(self) -> bool: 

1037 """ 

1038 Whether to use colorful output styles. 

1039 

1040 Setting this will update the renderer's styles immediately and reset 

1041 level styles to the defaults according to the new color setting -- even 

1042 if the color value is the same. 

1043 

1044 .. versionadded:: 25.5.0 

1045 """ 

1046 return self._colors 

1047 

1048 @colors.setter 

1049 def colors(self, value: bool) -> None: 

1050 """ 

1051 .. versionadded:: 25.5.0 

1052 

1053 .. versionchanged:: 26.1.0 

1054 Also switches to monochrome exception formatting if colors are disabled. 

1055 """ 

1056 self._colors = value 

1057 self._styles = self.get_default_column_styles( 

1058 value, self._force_colors 

1059 ) 

1060 self._level_styles = self.get_default_level_styles(value) 

1061 

1062 # Flip default exception formatter if configured to use one of the 

1063 # default ones. 

1064 if self.exception_formatter is ( 

1065 default_exception_formatter 

1066 ) or self.exception_formatter is ( 

1067 default_monochrome_exception_formatter 

1068 ): 

1069 self.exception_formatter = ( 

1070 default_exception_formatter 

1071 if value 

1072 else default_monochrome_exception_formatter 

1073 ) 

1074 

1075 self._configure_columns() 

1076 

1077 @property 

1078 def force_colors(self) -> bool: 

1079 """ 

1080 Force colorful output even in non-interactive environments. 

1081 

1082 Setting this will update the renderer's styles immediately and reset 

1083 level styles to the defaults according to the current color setting -- 

1084 even if the value is the same. 

1085 

1086 .. versionadded:: 25.5.0 

1087 """ 

1088 return self._force_colors 

1089 

1090 @force_colors.setter 

1091 def force_colors(self, value: bool) -> None: 

1092 """ 

1093 .. versionadded:: 25.5.0 

1094 """ 

1095 self._force_colors = value 

1096 self._styles = self.get_default_column_styles(self._colors, value) 

1097 self._level_styles = self.get_default_level_styles(self._colors) 

1098 

1099 self._configure_columns() 

1100 

1101 @property 

1102 def level_styles(self) -> dict[str, str]: 

1103 """ 

1104 The level styles mapping for this console renderer. 

1105 

1106 Setting this property will reset to defaults if set to None, otherwise 

1107 it applies the provided mapping. In all cases, columns are rebuilt to 

1108 reflect the change. 

1109 

1110 .. versionadded:: 25.5.0 

1111 """ 

1112 return self._level_styles 

1113 

1114 @level_styles.setter 

1115 def level_styles(self, value: dict[str, str] | None) -> None: 

1116 """ 

1117 .. versionadded:: 25.5.0 

1118 """ 

1119 self._level_styles = ( 

1120 self.get_default_level_styles(self._colors) 

1121 if value is None 

1122 else value 

1123 ) 

1124 self._configure_columns() 

1125 

1126 @property 

1127 def pad_level(self) -> bool: 

1128 """ 

1129 Whether to pad log level with blanks to the longest amongst all 

1130 level labels. 

1131 

1132 Setting this will rebuild columns to reflect the change. 

1133 

1134 .. versionadded:: 25.5.0 

1135 """ 

1136 return self._pad_level 

1137 

1138 @pad_level.setter 

1139 def pad_level(self, value: bool) -> None: 

1140 """ 

1141 .. versionadded:: 25.5.0 

1142 """ 

1143 self._pad_level = value 

1144 self._configure_columns() 

1145 

1146 @property 

1147 def pad_event_to(self) -> int: 

1148 """ 

1149 The number of characters to pad the event to. 

1150 

1151 Setting this will rebuild columns to reflect the change. 

1152 

1153 .. versionadded:: 25.5.0 

1154 """ 

1155 return self._pad_event_to 

1156 

1157 @pad_event_to.setter 

1158 def pad_event_to(self, value: int) -> None: 

1159 """ 

1160 .. versionadded:: 25.5.0 

1161 """ 

1162 self._pad_event_to = value 

1163 self._configure_columns() 

1164 

1165 @property 

1166 def event_key(self) -> str: 

1167 """ 

1168 The key to look for the main log message. 

1169 

1170 Setting this will rebuild columns to reflect the change. 

1171 

1172 .. versionadded:: 25.5.0 

1173 """ 

1174 return self._event_key 

1175 

1176 @event_key.setter 

1177 def event_key(self, value: str) -> None: 

1178 """ 

1179 .. versionadded:: 25.5.0 

1180 """ 

1181 self._event_key = value 

1182 self._configure_columns() 

1183 

1184 @property 

1185 def timestamp_key(self) -> str: 

1186 """ 

1187 The key to look for the timestamp of the log message. 

1188 

1189 Setting this will rebuild columns to reflect the change. 

1190 

1191 .. versionadded:: 25.5.0 

1192 """ 

1193 return self._timestamp_key 

1194 

1195 @timestamp_key.setter 

1196 def timestamp_key(self, value: str) -> None: 

1197 """ 

1198 .. versionadded:: 25.5.0 

1199 """ 

1200 self._timestamp_key = value 

1201 self._configure_columns() 

1202 

1203 @property 

1204 def repr_native_str(self) -> bool: 

1205 """ 

1206 Whether native strings are passed through repr() in non-event values. 

1207 

1208 .. versionadded:: 25.5.0 

1209 """ 

1210 return self._repr_native_str 

1211 

1212 @repr_native_str.setter 

1213 def repr_native_str(self, value: bool) -> None: 

1214 """ 

1215 .. versionadded:: 25.5.0 

1216 """ 

1217 self._repr_native_str = value 

1218 

1219 

1220_SENTINEL = object() 

1221 

1222 

1223def set_exc_info( 

1224 logger: WrappedLogger, method_name: str, event_dict: EventDict 

1225) -> EventDict: 

1226 """ 

1227 Set ``event_dict["exc_info"] = True`` if *method_name* is ``"exception"``. 

1228 

1229 Do nothing if the name is different or ``exc_info`` is already set. 

1230 

1231 .. versionadded:: 19.2.0 

1232 """ 

1233 if ( 

1234 method_name != "exception" 

1235 or event_dict.get("exc_info", _SENTINEL) is not _SENTINEL 

1236 ): 

1237 return event_dict 

1238 

1239 event_dict["exc_info"] = True 

1240 

1241 return event_dict