Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/dev.py: 77%
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
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
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.
6"""
7Helpers that make development with *structlog* more pleasant.
9See also the narrative documentation in `console-output`.
10"""
12from __future__ import annotations
14import shutil
15import sys
16import warnings
18from dataclasses import dataclass
19from io import StringIO
20from types import ModuleType
21from typing import (
22 Any,
23 Callable,
24 Literal,
25 Protocol,
26 Sequence,
27 TextIO,
28 Type,
29 Union,
30 cast,
31)
33from ._frames import _format_exception
34from .processors import _figure_out_exc_info
35from .typing import EventDict, ExceptionRenderer, ExcInfo, WrappedLogger
38try:
39 import colorama
40except ImportError:
41 colorama = None
43try:
44 import better_exceptions
45except ImportError:
46 better_exceptions = None
48try:
49 import rich
51 from rich.console import Console
52 from rich.traceback import Traceback
53except ImportError:
54 rich = None # type: ignore[assignment]
56__all__ = [
57 "ConsoleRenderer",
58 "RichTracebackFormatter",
59 "better_traceback",
60 "plain_traceback",
61 "rich_traceback",
62]
64_IS_WINDOWS = sys.platform == "win32"
66_MISSING = "{who} requires the {package} package installed. "
67_EVENT_WIDTH = 30 # pad the event name to so many characters
70def _pad(s: str, length: int) -> str:
71 """
72 Pads *s* to length *length*.
73 """
74 missing = length - len(s)
76 return s + " " * (max(0, missing))
79if colorama is not None:
80 RESET_ALL = colorama.Style.RESET_ALL
81 BRIGHT = colorama.Style.BRIGHT
82 DIM = colorama.Style.DIM
83 RED = colorama.Fore.RED
84 BLUE = colorama.Fore.BLUE
85 CYAN = colorama.Fore.CYAN
86 MAGENTA = colorama.Fore.MAGENTA
87 YELLOW = colorama.Fore.YELLOW
88 GREEN = colorama.Fore.GREEN
89 RED_BACK = colorama.Back.RED
90else:
91 # These are the same values as the Colorama color codes. Redefining them
92 # here allows users to specify that they want color without having to
93 # install Colorama, which is only supposed to be necessary in Windows.
94 RESET_ALL = "\033[0m"
95 BRIGHT = "\033[1m"
96 DIM = "\033[2m"
97 RED = "\033[31m"
98 BLUE = "\033[34m"
99 CYAN = "\033[36m"
100 MAGENTA = "\033[35m"
101 YELLOW = "\033[33m"
102 GREEN = "\033[32m"
103 RED_BACK = "\033[41m"
105# On Windows, colors are only available if Colorama is installed.
106_has_colors = not _IS_WINDOWS or colorama is not None
108# Prevent breakage of packages that used the old name of the variable.
109_use_colors = _has_colors
112class _Styles(Protocol):
113 reset: str
114 bright: str
115 level_critical: str
116 level_exception: str
117 level_error: str
118 level_warn: str
119 level_info: str
120 level_debug: str
121 level_notset: str
123 timestamp: str
124 logger_name: str
125 kv_key: str
126 kv_value: str
129Styles = Union[_Styles, Type[_Styles]]
132class _ColorfulStyles:
133 reset = RESET_ALL
134 bright = BRIGHT
136 level_critical = RED
137 level_exception = RED
138 level_error = RED
139 level_warn = YELLOW
140 level_info = GREEN
141 level_debug = GREEN
142 level_notset = RED_BACK
144 timestamp = DIM
145 logger_name = BLUE
146 kv_key = CYAN
147 kv_value = MAGENTA
150class _PlainStyles:
151 reset = ""
152 bright = ""
154 level_critical = ""
155 level_exception = ""
156 level_error = ""
157 level_warn = ""
158 level_info = ""
159 level_debug = ""
160 level_notset = ""
162 timestamp = ""
163 logger_name = ""
164 kv_key = ""
165 kv_value = ""
168class ColumnFormatter(Protocol):
169 """
170 :class:`~typing.Protocol` for column formatters.
172 See `KeyValueColumnFormatter` and `LogLevelColumnFormatter` for examples.
174 .. versionadded:: 23.3.0
175 """
177 def __call__(self, key: str, value: object) -> str:
178 """
179 Format *value* for *key*.
181 This method is responsible for formatting, *key*, the ``=``, and the
182 *value*. That means that it can use any string instead of the ``=`` and
183 it can leave out both the *key* or the *value*.
185 If it returns an empty string, the column is omitted completely.
186 """
189@dataclass
190class Column:
191 """
192 A column defines the way a key-value pair is formatted, and, by it's
193 position to the *columns* argument of `ConsoleRenderer`, the order in which
194 it is rendered.
196 Args:
197 key:
198 The key for which this column is responsible. Leave empty to define
199 it as the default formatter.
201 formatter: The formatter for columns with *key*.
203 .. versionadded:: 23.3.0
204 """
206 key: str
207 formatter: ColumnFormatter
210@dataclass
211class KeyValueColumnFormatter:
212 """
213 Format a key-value pair.
215 Args:
216 key_style: The style to apply to the key. If None, the key is omitted.
218 value_style: The style to apply to the value.
220 reset_style: The style to apply whenever a style is no longer needed.
222 value_repr:
223 A callable that returns the string representation of the value.
225 width: The width to pad the value to. If 0, no padding is done.
227 prefix:
228 A string to prepend to the formatted key-value pair. May contain
229 styles.
231 postfix:
232 A string to append to the formatted key-value pair. May contain
233 styles.
235 .. versionadded:: 23.3.0
236 """
238 key_style: str | None
239 value_style: str
240 reset_style: str
241 value_repr: Callable[[object], str]
242 width: int = 0
243 prefix: str = ""
244 postfix: str = ""
246 def __call__(self, key: str, value: object) -> str:
247 sio = StringIO()
249 if self.prefix:
250 sio.write(self.prefix)
251 sio.write(self.reset_style)
253 if self.key_style is not None:
254 sio.write(self.key_style)
255 sio.write(key)
256 sio.write(self.reset_style)
257 sio.write("=")
259 sio.write(self.value_style)
260 sio.write(_pad(self.value_repr(value), self.width))
261 sio.write(self.reset_style)
263 if self.postfix:
264 sio.write(self.postfix)
265 sio.write(self.reset_style)
267 return sio.getvalue()
270class LogLevelColumnFormatter:
271 """
272 Format a log level according to *level_styles*.
274 The width is padded to the longest level name (if *level_styles* is passed
275 -- otherwise there's no way to know the lengths of all levels).
277 Args:
278 level_styles:
279 A dictionary of level names to styles that are applied to it. If
280 None, the level is formatted as a plain ``[level]``.
282 reset_style:
283 What to use to reset the style after the level name. Ignored if
284 if *level_styles* is None.
286 width:
287 The width to pad the level to. If 0, no padding is done.
289 .. versionadded:: 23.3.0
290 .. versionadded:: 24.2.0 *width*
291 """
293 level_styles: dict[str, str] | None
294 reset_style: str
295 width: int
297 def __init__(
298 self,
299 level_styles: dict[str, str],
300 reset_style: str,
301 width: int | None = None,
302 ) -> None:
303 self.level_styles = level_styles
304 if level_styles:
305 self.width = (
306 0
307 if width == 0
308 else len(max(self.level_styles.keys(), key=lambda e: len(e)))
309 )
310 self.reset_style = reset_style
311 else:
312 self.width = 0
313 self.reset_style = ""
315 def __call__(self, key: str, value: object) -> str:
316 level = cast(str, value)
317 style = (
318 ""
319 if self.level_styles is None
320 else self.level_styles.get(level, "")
321 )
323 return f"[{style}{_pad(level, self.width)}{self.reset_style}]"
326_NOTHING = object()
329def plain_traceback(sio: TextIO, exc_info: ExcInfo) -> None:
330 """
331 "Pretty"-print *exc_info* to *sio* using our own plain formatter.
333 To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument.
335 Used by default if neither Rich nor *better-exceptions* are present.
337 .. versionadded:: 21.2.0
338 """
339 sio.write("\n" + _format_exception(exc_info))
342@dataclass
343class RichTracebackFormatter:
344 """
345 A Rich traceback renderer with the given options.
347 Pass an instance as `ConsoleRenderer`'s ``exception_formatter`` argument.
349 See :class:`rich.traceback.Traceback` for details on the arguments.
351 If a *width* of -1 is passed, the terminal width is used. If the width
352 can't be determined, fall back to 80.
354 .. versionadded:: 23.2.0
355 """
357 color_system: Literal[
358 "auto", "standard", "256", "truecolor", "windows"
359 ] = "truecolor"
360 show_locals: bool = True
361 max_frames: int = 100
362 theme: str | None = None
363 word_wrap: bool = False
364 extra_lines: int = 3
365 width: int = 100
366 indent_guides: bool = True
367 locals_max_length: int = 10
368 locals_max_string: int = 80
369 locals_hide_dunder: bool = True
370 locals_hide_sunder: bool = False
371 suppress: Sequence[str | ModuleType] = ()
373 def __call__(self, sio: TextIO, exc_info: ExcInfo) -> None:
374 if self.width == -1:
375 self.width, _ = shutil.get_terminal_size((80, 0))
377 sio.write("\n")
379 Console(
380 file=sio, color_system=self.color_system, width=self.width
381 ).print(
382 Traceback.from_exception(
383 *exc_info,
384 show_locals=self.show_locals,
385 max_frames=self.max_frames,
386 theme=self.theme,
387 word_wrap=self.word_wrap,
388 extra_lines=self.extra_lines,
389 width=self.width,
390 indent_guides=self.indent_guides,
391 locals_max_length=self.locals_max_length,
392 locals_max_string=self.locals_max_string,
393 locals_hide_dunder=self.locals_hide_dunder,
394 locals_hide_sunder=self.locals_hide_sunder,
395 suppress=self.suppress,
396 )
397 )
400rich_traceback = RichTracebackFormatter()
401"""
402Pretty-print *exc_info* to *sio* using the Rich package.
404To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument.
406This is a `RichTracebackFormatter` with default arguments and used by default
407if Rich is installed.
409.. versionadded:: 21.2.0
410"""
413def better_traceback(sio: TextIO, exc_info: ExcInfo) -> None:
414 """
415 Pretty-print *exc_info* to *sio* using the *better-exceptions* package.
417 To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument.
419 Used by default if *better-exceptions* is installed and Rich is absent.
421 .. versionadded:: 21.2.0
422 """
423 sio.write("\n" + "".join(better_exceptions.format_exception(*exc_info)))
426if rich is not None:
427 default_exception_formatter = rich_traceback
428elif better_exceptions is not None:
429 default_exception_formatter = better_traceback
430else:
431 default_exception_formatter = plain_traceback
434class ConsoleRenderer:
435 r"""
436 Render ``event_dict`` nicely aligned, possibly in colors, and ordered.
438 If ``event_dict`` contains a true-ish ``exc_info`` key, it will be rendered
439 *after* the log line. If Rich_ or better-exceptions_ are present, in colors
440 and with extra context.
442 Args:
443 columns:
444 A list of `Column` objects defining both the order and format of
445 the key-value pairs in the output. If passed, most other arguments
446 become meaningless.
448 **Must** contain a column with ``key=''`` that defines the default
449 formatter.
451 .. seealso:: `columns-config`
453 pad_event:
454 Pad the event to this many characters. Ignored if *columns* are
455 passed.
457 colors:
458 Use colors for a nicer output. `True` by default. On Windows only
459 if Colorama_ is installed. Ignored if *columns* are passed.
461 force_colors:
462 Force colors even for non-tty destinations. Use this option if your
463 logs are stored in a file that is meant to be streamed to the
464 console. Only meaningful on Windows. Ignored if *columns* are
465 passed.
467 repr_native_str:
468 When `True`, `repr` is also applied to ``str``\ s. The ``event``
469 key is *never* `repr` -ed. Ignored if *columns* are passed.
471 level_styles:
472 When present, use these styles for colors. This must be a dict from
473 level names (strings) to terminal sequences (for example, Colorama)
474 styles. The default can be obtained by calling
475 `ConsoleRenderer.get_default_level_styles`. Ignored when *columns*
476 are passed.
478 exception_formatter:
479 A callable to render ``exc_infos``. If Rich_ or better-exceptions_
480 are installed, they are used for pretty-printing by default (rich_
481 taking precedence). You can also manually set it to
482 `plain_traceback`, `better_traceback`, an instance of
483 `RichTracebackFormatter` like `rich_traceback`, or implement your
484 own.
486 sort_keys:
487 Whether to sort keys when formatting. `True` by default. Ignored if
488 *columns* are passed.
490 event_key:
491 The key to look for the main log message. Needed when you rename it
492 e.g. using `structlog.processors.EventRenamer`. Ignored if
493 *columns* are passed.
495 timestamp_key:
496 The key to look for timestamp of the log message. Needed when you
497 rename it e.g. using `structlog.processors.EventRenamer`. Ignored
498 if *columns* are passed.
500 pad_level:
501 Whether to pad log level with blanks to the longest amongst all
502 level label.
504 Requires the Colorama_ package if *colors* is `True` **on Windows**.
506 Raises:
507 ValueError: If there's not exactly one default column formatter.
509 .. _Colorama: https://pypi.org/project/colorama/
510 .. _better-exceptions: https://pypi.org/project/better-exceptions/
511 .. _Rich: https://pypi.org/project/rich/
513 .. versionadded:: 16.0.0
514 .. versionadded:: 16.1.0 *colors*
515 .. versionadded:: 17.1.0 *repr_native_str*
516 .. versionadded:: 18.1.0 *force_colors*
517 .. versionadded:: 18.1.0 *level_styles*
518 .. versionchanged:: 19.2.0
519 Colorama now initializes lazily to avoid unwanted initializations as
520 ``ConsoleRenderer`` is used by default.
521 .. versionchanged:: 19.2.0 Can be pickled now.
522 .. versionchanged:: 20.1.0
523 Colorama does not initialize lazily on Windows anymore because it breaks
524 rendering.
525 .. versionchanged:: 21.1.0
526 It is additionally possible to set the logger name using the
527 ``logger_name`` key in the ``event_dict``.
528 .. versionadded:: 21.2.0 *exception_formatter*
529 .. versionchanged:: 21.2.0
530 `ConsoleRenderer` now handles the ``exc_info`` event dict key itself. Do
531 **not** use the `structlog.processors.format_exc_info` processor
532 together with `ConsoleRenderer` anymore! It will keep working, but you
533 can't have customize exception formatting and a warning will be raised
534 if you ask for it.
535 .. versionchanged:: 21.2.0
536 The colors keyword now defaults to True on non-Windows systems, and
537 either True or False in Windows depending on whether Colorama is
538 installed.
539 .. versionadded:: 21.3.0 *sort_keys*
540 .. versionadded:: 22.1.0 *event_key*
541 .. versionadded:: 23.2.0 *timestamp_key*
542 .. versionadded:: 23.3.0 *columns*
543 .. versionadded:: 24.2.0 *pad_level*
544 """
546 def __init__( # noqa: PLR0912, PLR0915
547 self,
548 pad_event: int = _EVENT_WIDTH,
549 colors: bool = _has_colors,
550 force_colors: bool = False,
551 repr_native_str: bool = False,
552 level_styles: dict[str, str] | None = None,
553 exception_formatter: ExceptionRenderer = default_exception_formatter,
554 sort_keys: bool = True,
555 event_key: str = "event",
556 timestamp_key: str = "timestamp",
557 columns: list[Column] | None = None,
558 pad_level: bool = True,
559 ):
560 self._exception_formatter = exception_formatter
561 self._sort_keys = sort_keys
563 if columns is not None:
564 to_warn = []
566 def add_meaningless_arg(arg: str) -> None:
567 to_warn.append(
568 f"The `{arg}` argument is ignored when passing `columns`.",
569 )
571 if pad_event != _EVENT_WIDTH:
572 add_meaningless_arg("pad_event")
574 if colors != _has_colors:
575 add_meaningless_arg("colors")
577 if force_colors is not False:
578 add_meaningless_arg("force_colors")
580 if repr_native_str is not False:
581 add_meaningless_arg("repr_native_str")
583 if level_styles is not None:
584 add_meaningless_arg("level_styles")
586 if event_key != "event":
587 add_meaningless_arg("event_key")
589 if timestamp_key != "timestamp":
590 add_meaningless_arg("timestamp_key")
592 for w in to_warn:
593 warnings.warn(w, stacklevel=2)
595 defaults = [col for col in columns if col.key == ""]
596 if not defaults:
597 raise ValueError(
598 "Must pass a default column formatter (a column with `key=''`)."
599 )
600 if len(defaults) > 1:
601 raise ValueError("Only one default column formatter allowed.")
603 self._default_column_formatter = defaults[0].formatter
604 self._columns = [col for col in columns if col.key]
606 return
608 # Create default columns configuration.
609 styles: Styles
610 if colors:
611 if _IS_WINDOWS: # pragma: no cover
612 # On Windows, we can't do colorful output without colorama.
613 if colorama is None:
614 classname = self.__class__.__name__
615 raise SystemError(
616 _MISSING.format(
617 who=classname + " with `colors=True`",
618 package="colorama",
619 )
620 )
621 # Colorama must be init'd on Windows, but must NOT be
622 # init'd on other OSes, because it can break colors.
623 if force_colors:
624 colorama.deinit()
625 colorama.init(strip=False)
626 else:
627 colorama.init()
629 styles = _ColorfulStyles
630 else:
631 styles = _PlainStyles
633 self._styles = styles
635 level_to_color = (
636 self.get_default_level_styles(colors)
637 if level_styles is None
638 else level_styles
639 ).copy()
641 for key in level_to_color:
642 level_to_color[key] += styles.bright
643 self._longest_level = len(
644 max(level_to_color.keys(), key=lambda e: len(e))
645 )
647 self._repr_native_str = repr_native_str
649 self._default_column_formatter = KeyValueColumnFormatter(
650 styles.kv_key,
651 styles.kv_value,
652 styles.reset,
653 value_repr=self._repr,
654 width=0,
655 )
657 logger_name_formatter = KeyValueColumnFormatter(
658 key_style=None,
659 value_style=styles.bright + styles.logger_name,
660 reset_style=styles.reset,
661 value_repr=str,
662 prefix="[",
663 postfix="]",
664 )
666 level_width = 0 if not pad_level else None
668 self._columns = [
669 Column(
670 timestamp_key,
671 KeyValueColumnFormatter(
672 key_style=None,
673 value_style=styles.timestamp,
674 reset_style=styles.reset,
675 value_repr=str,
676 ),
677 ),
678 Column(
679 "level",
680 LogLevelColumnFormatter(
681 level_to_color, reset_style=styles.reset, width=level_width
682 ),
683 ),
684 Column(
685 event_key,
686 KeyValueColumnFormatter(
687 key_style=None,
688 value_style=styles.bright,
689 reset_style=styles.reset,
690 value_repr=str,
691 width=pad_event,
692 ),
693 ),
694 Column("logger", logger_name_formatter),
695 Column("logger_name", logger_name_formatter),
696 ]
698 def _repr(self, val: Any) -> str:
699 """
700 Determine representation of *val* depending on its type &
701 self._repr_native_str.
702 """
703 if self._repr_native_str is True:
704 return repr(val)
706 if isinstance(val, str):
707 if set(val) & {" ", "\t", "=", "\r", "\n", '"', "'"}:
708 return repr(val)
709 return val
711 return repr(val)
713 def __call__(
714 self, logger: WrappedLogger, name: str, event_dict: EventDict
715 ) -> str:
716 stack = event_dict.pop("stack", None)
717 exc = event_dict.pop("exception", None)
718 exc_info = event_dict.pop("exc_info", None)
720 kvs = [
721 col.formatter(col.key, val)
722 for col in self._columns
723 if (val := event_dict.pop(col.key, _NOTHING)) is not _NOTHING
724 ] + [
725 self._default_column_formatter(key, event_dict[key])
726 for key in (sorted(event_dict) if self._sort_keys else event_dict)
727 ]
729 sio = StringIO()
730 sio.write((" ".join(kv for kv in kvs if kv)).rstrip(" "))
732 if stack is not None:
733 sio.write("\n" + stack)
734 if exc_info or exc is not None:
735 sio.write("\n\n" + "=" * 79 + "\n")
737 exc_info = _figure_out_exc_info(exc_info)
738 if exc_info:
739 self._exception_formatter(sio, exc_info)
740 elif exc is not None:
741 if self._exception_formatter is not plain_traceback:
742 warnings.warn(
743 "Remove `format_exc_info` from your processor chain "
744 "if you want pretty exceptions.",
745 stacklevel=2,
746 )
748 sio.write("\n" + exc)
750 return sio.getvalue()
752 @staticmethod
753 def get_default_level_styles(colors: bool = True) -> dict[str, str]:
754 """
755 Get the default styles for log levels
757 This is intended to be used with `ConsoleRenderer`'s ``level_styles``
758 parameter. For example, if you are adding custom levels in your
759 home-grown :func:`~structlog.stdlib.add_log_level` you could do::
761 my_styles = ConsoleRenderer.get_default_level_styles()
762 my_styles["EVERYTHING_IS_ON_FIRE"] = my_styles["critical"]
763 renderer = ConsoleRenderer(level_styles=my_styles)
765 Args:
766 colors:
767 Whether to use colorful styles. This must match the *colors*
768 parameter to `ConsoleRenderer`. Default: `True`.
769 """
770 styles: Styles
771 styles = _ColorfulStyles if colors else _PlainStyles
772 return {
773 "critical": styles.level_critical,
774 "exception": styles.level_exception,
775 "error": styles.level_error,
776 "warn": styles.level_warn,
777 "warning": styles.level_warn,
778 "info": styles.level_info,
779 "debug": styles.level_debug,
780 "notset": styles.level_notset,
781 }
784_SENTINEL = object()
787def set_exc_info(
788 logger: WrappedLogger, method_name: str, event_dict: EventDict
789) -> EventDict:
790 """
791 Set ``event_dict["exc_info"] = True`` if *method_name* is ``"exception"``.
793 Do nothing if the name is different or ``exc_info`` is already set.
794 """
795 if (
796 method_name != "exception"
797 or event_dict.get("exc_info", _SENTINEL) is not _SENTINEL
798 ):
799 return event_dict
801 event_dict["exc_info"] = True
803 return event_dict