1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the MIT License.  See the LICENSE file in the root of this
3# repository for complete details.
4
5"""
6Helpers that make development with ``structlog`` more pleasant.
7"""
8
9from __future__ import absolute_import, division, print_function
10
11from six import StringIO
12
13
14try:
15    import colorama
16except ImportError:
17    colorama = None
18
19
20__all__ = ["ConsoleRenderer"]
21
22
23_MISSING = "{who} requires the {package} package installed.  "
24_EVENT_WIDTH = 30  # pad the event name to so many characters
25
26
27def _pad(s, l):
28    """
29    Pads *s* to length *l*.
30    """
31    missing = l - len(s)
32    return s + " " * (missing if missing > 0 else 0)
33
34
35if colorama is not None:
36    _has_colorama = True
37
38    RESET_ALL = colorama.Style.RESET_ALL
39    BRIGHT = colorama.Style.BRIGHT
40    DIM = colorama.Style.DIM
41    RED = colorama.Fore.RED
42    BLUE = colorama.Fore.BLUE
43    CYAN = colorama.Fore.CYAN
44    MAGENTA = colorama.Fore.MAGENTA
45    YELLOW = colorama.Fore.YELLOW
46    GREEN = colorama.Fore.GREEN
47    RED_BACK = colorama.Back.RED
48else:
49    _has_colorama = False
50
51    RESET_ALL = (
52        BRIGHT
53    ) = DIM = RED = BLUE = CYAN = MAGENTA = YELLOW = GREEN = RED_BACK = ""
54
55
56class _ColorfulStyles(object):
57    reset = RESET_ALL
58    bright = BRIGHT
59
60    level_critical = RED
61    level_exception = RED
62    level_error = RED
63    level_warn = YELLOW
64    level_info = GREEN
65    level_debug = GREEN
66    level_notset = RED_BACK
67
68    timestamp = DIM
69    logger_name = BLUE
70    kv_key = CYAN
71    kv_value = MAGENTA
72
73
74class _PlainStyles(object):
75    reset = ""
76    bright = ""
77
78    level_critical = ""
79    level_exception = ""
80    level_error = ""
81    level_warn = ""
82    level_info = ""
83    level_debug = ""
84    level_notset = ""
85
86    timestamp = ""
87    logger_name = ""
88    kv_key = ""
89    kv_value = ""
90
91
92class ConsoleRenderer(object):
93    """
94    Render `event_dict` nicely aligned, possibly in colors, and ordered.
95
96    :param int pad_event: Pad the event to this many characters.
97    :param bool colors: Use colors for a nicer output.
98    :param bool force_colors: Force colors even for non-tty destinations.
99        Use this option if your logs are stored in a file that is meant
100        to be streamed to the console.
101    :param bool repr_native_str: When ``True``, :func:`repr()` is also applied
102        to native strings (i.e. unicode on Python 3 and bytes on Python 2).
103        Setting this to ``False`` is useful if you want to have human-readable
104        non-ASCII output on Python 2.  The `event` key is *never*
105        :func:`repr()` -ed.
106    :param dict level_styles: When present, use these styles for colors. This
107        must be a dict from level names (strings) to colorama styles. The
108        default can be obtained by calling
109        :meth:`ConsoleRenderer.get_default_level_styles`
110
111    Requires the colorama_ package if *colors* is ``True``.
112
113    .. _colorama: https://pypi.org/project/colorama/
114
115    .. versionadded:: 16.0
116    .. versionadded:: 16.1 *colors*
117    .. versionadded:: 17.1 *repr_native_str*
118    .. versionadded:: 18.1 *force_colors*
119    .. versionadded:: 18.1 *level_styles*
120    """
121
122    def __init__(
123        self,
124        pad_event=_EVENT_WIDTH,
125        colors=True,
126        force_colors=False,
127        repr_native_str=False,
128        level_styles=None,
129    ):
130        if colors is True:
131            if colorama is None:
132                raise SystemError(
133                    _MISSING.format(
134                        who=self.__class__.__name__ + " with `colors=True`",
135                        package="colorama",
136                    )
137                )
138
139            if force_colors:
140                colorama.deinit()
141                colorama.init(strip=False)
142            else:
143                colorama.init()
144
145            styles = _ColorfulStyles
146        else:
147            styles = _PlainStyles
148
149        self._styles = styles
150        self._pad_event = pad_event
151
152        if level_styles is None:
153            self._level_to_color = self.get_default_level_styles(colors)
154        else:
155            self._level_to_color = level_styles
156
157        for key in self._level_to_color.keys():
158            self._level_to_color[key] += styles.bright
159        self._longest_level = len(
160            max(self._level_to_color.keys(), key=lambda e: len(e))
161        )
162
163        if repr_native_str is True:
164            self._repr = repr
165        else:
166
167            def _repr(inst):
168                if isinstance(inst, str):
169                    return inst
170                else:
171                    return repr(inst)
172
173            self._repr = _repr
174
175    def __call__(self, _, __, event_dict):
176        sio = StringIO()
177
178        ts = event_dict.pop("timestamp", None)
179        if ts is not None:
180            sio.write(
181                # can be a number if timestamp is UNIXy
182                self._styles.timestamp
183                + str(ts)
184                + self._styles.reset
185                + " "
186            )
187        level = event_dict.pop("level", None)
188        if level is not None:
189            sio.write(
190                "["
191                + self._level_to_color[level]
192                + _pad(level, self._longest_level)
193                + self._styles.reset
194                + "] "
195            )
196
197        event = event_dict.pop("event")
198        if event_dict:
199            event = _pad(event, self._pad_event) + self._styles.reset + " "
200        else:
201            event += self._styles.reset
202        sio.write(self._styles.bright + event)
203
204        logger_name = event_dict.pop("logger", None)
205        if logger_name is not None:
206            sio.write(
207                "["
208                + self._styles.logger_name
209                + self._styles.bright
210                + logger_name
211                + self._styles.reset
212                + "] "
213            )
214
215        stack = event_dict.pop("stack", None)
216        exc = event_dict.pop("exception", None)
217        sio.write(
218            " ".join(
219                self._styles.kv_key
220                + key
221                + self._styles.reset
222                + "="
223                + self._styles.kv_value
224                + self._repr(event_dict[key])
225                + self._styles.reset
226                for key in sorted(event_dict.keys())
227            )
228        )
229
230        if stack is not None:
231            sio.write("\n" + stack)
232            if exc is not None:
233                sio.write("\n\n" + "=" * 79 + "\n")
234        if exc is not None:
235            sio.write("\n" + exc)
236
237        return sio.getvalue()
238
239    @staticmethod
240    def get_default_level_styles(colors=True):
241        """
242        Get the default styles for log levels
243
244        This is intended to be used with :class:`ConsoleRenderer`'s
245        ``level_styles`` parameter.  For example, if you are adding
246        custom levels in your home-grown
247        :func:`~structlog.stdlib.add_log_level` you could do::
248
249            my_styles = ConsoleRenderer.get_default_level_styles()
250            my_styles["EVERYTHING_IS_ON_FIRE"] = my_styles["critical"]
251            renderer = ConsoleRenderer(level_styles=my_styles)
252
253        :param bool colors: Whether to use colorful styles. This must match the
254            `colors` parameter to :class:`ConsoleRenderer`. Default: True.
255        """
256        if colors:
257            styles = _ColorfulStyles
258        else:
259            styles = _PlainStyles
260        return {
261            "critical": styles.level_critical,
262            "exception": styles.level_exception,
263            "error": styles.level_error,
264            "warn": styles.level_warn,
265            "warning": styles.level_warn,
266            "info": styles.level_info,
267            "debug": styles.level_debug,
268            "notset": styles.level_notset,
269        }
270