Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/coloredlogs/__init__.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 # Colored terminal output for Python's logging module. | |
2 # | |
3 # Author: Peter Odding <peter@peterodding.com> | |
4 # Last Change: December 10, 2020 | |
5 # URL: https://coloredlogs.readthedocs.io | |
6 | |
7 """ | |
8 Colored terminal output for Python's :mod:`logging` module. | |
9 | |
10 .. contents:: | |
11 :local: | |
12 | |
13 Getting started | |
14 =============== | |
15 | |
16 The easiest way to get started is by importing :mod:`coloredlogs` and calling | |
17 :mod:`coloredlogs.install()` (similar to :func:`logging.basicConfig()`): | |
18 | |
19 >>> import coloredlogs, logging | |
20 >>> coloredlogs.install(level='DEBUG') | |
21 >>> logger = logging.getLogger('some.module.name') | |
22 >>> logger.info("this is an informational message") | |
23 2015-10-22 19:13:52 peter-macbook some.module.name[28036] INFO this is an informational message | |
24 | |
25 The :mod:`~coloredlogs.install()` function creates a :class:`ColoredFormatter` | |
26 that injects `ANSI escape sequences`_ into the log output. | |
27 | |
28 .. _ANSI escape sequences: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors | |
29 | |
30 Environment variables | |
31 ===================== | |
32 | |
33 The following environment variables can be used to configure the | |
34 :mod:`coloredlogs` module without writing any code: | |
35 | |
36 ============================= ============================ ================================== | |
37 Environment variable Default value Type of value | |
38 ============================= ============================ ================================== | |
39 ``$COLOREDLOGS_AUTO_INSTALL`` 'false' a boolean that controls whether | |
40 :func:`auto_install()` is called | |
41 ``$COLOREDLOGS_LOG_LEVEL`` 'INFO' a log level name | |
42 ``$COLOREDLOGS_LOG_FORMAT`` :data:`DEFAULT_LOG_FORMAT` a log format string | |
43 ``$COLOREDLOGS_DATE_FORMAT`` :data:`DEFAULT_DATE_FORMAT` a date/time format string | |
44 ``$COLOREDLOGS_LEVEL_STYLES`` :data:`DEFAULT_LEVEL_STYLES` see :func:`parse_encoded_styles()` | |
45 ``$COLOREDLOGS_FIELD_STYLES`` :data:`DEFAULT_FIELD_STYLES` see :func:`parse_encoded_styles()` | |
46 ============================= ============================ ================================== | |
47 | |
48 If the environment variable `$NO_COLOR`_ is set (the value doesn't matter, even | |
49 an empty string will do) then :func:`coloredlogs.install()` will take this as a | |
50 hint that colors should not be used (unless the ``isatty=True`` override was | |
51 passed by the caller). | |
52 | |
53 .. _$NO_COLOR: https://no-color.org/ | |
54 | |
55 Examples of customization | |
56 ========================= | |
57 | |
58 Here we'll take a look at some examples of how you can customize | |
59 :mod:`coloredlogs` using environment variables. | |
60 | |
61 .. contents:: | |
62 :local: | |
63 | |
64 About the defaults | |
65 ------------------ | |
66 | |
67 Here's a screen shot of the default configuration for easy comparison with the | |
68 screen shots of the following customizations (this is the same screen shot that | |
69 is shown in the introduction): | |
70 | |
71 .. image:: images/defaults.png | |
72 :alt: Screen shot of colored logging with defaults. | |
73 | |
74 The screen shot above was taken from ``urxvt`` which doesn't support faint text | |
75 colors, otherwise the color of green used for `debug` messages would have | |
76 differed slightly from the color of green used for `spam` messages. | |
77 | |
78 Apart from the `faint` style of the `spam` level, the default configuration of | |
79 `coloredlogs` sticks to the eight color palette defined by the original ANSI | |
80 standard, in order to provide a somewhat consistent experience across terminals | |
81 and terminal emulators. | |
82 | |
83 Available text styles and colors | |
84 -------------------------------- | |
85 | |
86 Of course you are free to customize the default configuration, in this case you | |
87 can use any text style or color that you know is supported by your terminal. | |
88 You can use the ``humanfriendly --demo`` command to try out the supported text | |
89 styles and colors: | |
90 | |
91 .. image:: http://humanfriendly.readthedocs.io/en/latest/_images/ansi-demo.png | |
92 :alt: Screen shot of the 'humanfriendly --demo' command. | |
93 | |
94 Changing the log format | |
95 ----------------------- | |
96 | |
97 The simplest customization is to change the log format, for example: | |
98 | |
99 .. literalinclude:: examples/custom-log-format.txt | |
100 :language: console | |
101 | |
102 Here's what that looks like in a terminal (I always work in terminals with a | |
103 black background and white text): | |
104 | |
105 .. image:: images/custom-log-format.png | |
106 :alt: Screen shot of colored logging with custom log format. | |
107 | |
108 Changing the date/time format | |
109 ----------------------------- | |
110 | |
111 You can also change the date/time format, for example you can remove the date | |
112 part and leave only the time: | |
113 | |
114 .. literalinclude:: examples/custom-datetime-format.txt | |
115 :language: console | |
116 | |
117 Here's what it looks like in a terminal: | |
118 | |
119 .. image:: images/custom-datetime-format.png | |
120 :alt: Screen shot of colored logging with custom date/time format. | |
121 | |
122 Changing the colors/styles | |
123 -------------------------- | |
124 | |
125 Finally you can customize the colors and text styles that are used: | |
126 | |
127 .. literalinclude:: examples/custom-colors.txt | |
128 :language: console | |
129 | |
130 Here's an explanation of the features used here: | |
131 | |
132 - The numbers used in ``$COLOREDLOGS_LEVEL_STYLES`` demonstrate the use of 256 | |
133 color mode (the numbers refer to the 256 color mode palette which is fixed). | |
134 | |
135 - The `success` level demonstrates the use of a text style (bold). | |
136 | |
137 - The `critical` level demonstrates the use of a background color (red). | |
138 | |
139 Of course none of this can be seen in the shell transcript quoted above, but | |
140 take a look at the following screen shot: | |
141 | |
142 .. image:: images/custom-colors.png | |
143 :alt: Screen shot of colored logging with custom colors. | |
144 | |
145 .. _notes about log levels: | |
146 | |
147 Some notes about log levels | |
148 =========================== | |
149 | |
150 With regards to the handling of log levels, the :mod:`coloredlogs` package | |
151 differs from Python's :mod:`logging` module in two aspects: | |
152 | |
153 1. While the :mod:`logging` module uses the default logging level | |
154 :data:`logging.WARNING`, the :mod:`coloredlogs` package has always used | |
155 :data:`logging.INFO` as its default log level. | |
156 | |
157 2. When logging to the terminal or system log is initialized by | |
158 :func:`install()` or :func:`.enable_system_logging()` the effective | |
159 level [#]_ of the selected logger [#]_ is compared against the requested | |
160 level [#]_ and if the effective level is more restrictive than the requested | |
161 level, the logger's level will be set to the requested level (this happens | |
162 in :func:`adjust_level()`). The reason for this is to work around a | |
163 combination of design choices in Python's :mod:`logging` module that can | |
164 easily confuse people who aren't already intimately familiar with it: | |
165 | |
166 - All loggers are initialized with the level :data:`logging.NOTSET`. | |
167 | |
168 - When a logger's level is set to :data:`logging.NOTSET` the | |
169 :func:`~logging.Logger.getEffectiveLevel()` method will | |
170 fall back to the level of the parent logger. | |
171 | |
172 - The parent of all loggers is the root logger and the root logger has its | |
173 level set to :data:`logging.WARNING` by default (after importing the | |
174 :mod:`logging` module). | |
175 | |
176 Effectively all user defined loggers inherit the default log level | |
177 :data:`logging.WARNING` from the root logger, which isn't very intuitive for | |
178 those who aren't already familiar with the hierarchical nature of the | |
179 :mod:`logging` module. | |
180 | |
181 By avoiding this potentially confusing behavior (see `#14`_, `#18`_, `#21`_, | |
182 `#23`_ and `#24`_), while at the same time allowing the caller to specify a | |
183 logger object, my goal and hope is to provide sane defaults that can easily | |
184 be changed when the need arises. | |
185 | |
186 .. [#] Refer to :func:`logging.Logger.getEffectiveLevel()` for details. | |
187 .. [#] The logger that is passed as an argument by the caller or the root | |
188 logger which is selected as a default when no logger is provided. | |
189 .. [#] The log level that is passed as an argument by the caller or the | |
190 default log level :data:`logging.INFO` when no level is provided. | |
191 | |
192 .. _#14: https://github.com/xolox/python-coloredlogs/issues/14 | |
193 .. _#18: https://github.com/xolox/python-coloredlogs/issues/18 | |
194 .. _#21: https://github.com/xolox/python-coloredlogs/pull/21 | |
195 .. _#23: https://github.com/xolox/python-coloredlogs/pull/23 | |
196 .. _#24: https://github.com/xolox/python-coloredlogs/issues/24 | |
197 | |
198 Classes and functions | |
199 ===================== | |
200 """ | |
201 | |
202 # Standard library modules. | |
203 import collections | |
204 import logging | |
205 import os | |
206 import re | |
207 import socket | |
208 import sys | |
209 | |
210 # External dependencies. | |
211 from humanfriendly import coerce_boolean | |
212 from humanfriendly.compat import coerce_string, is_string, on_windows | |
213 from humanfriendly.terminal import ANSI_COLOR_CODES, ansi_wrap, enable_ansi_support, terminal_supports_colors | |
214 from humanfriendly.text import format, split | |
215 | |
216 # Semi-standard module versioning. | |
217 __version__ = '15.0' | |
218 | |
219 DEFAULT_LOG_LEVEL = logging.INFO | |
220 """The default log level for :mod:`coloredlogs` (:data:`logging.INFO`).""" | |
221 | |
222 DEFAULT_LOG_FORMAT = '%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s' | |
223 """The default log format for :class:`ColoredFormatter` objects (a string).""" | |
224 | |
225 DEFAULT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' | |
226 """The default date/time format for :class:`ColoredFormatter` objects (a string).""" | |
227 | |
228 CHROOT_FILES = ['/etc/debian_chroot'] | |
229 """A list of filenames that indicate a chroot and contain the name of the chroot.""" | |
230 | |
231 DEFAULT_FIELD_STYLES = dict( | |
232 asctime=dict(color='green'), | |
233 hostname=dict(color='magenta'), | |
234 levelname=dict(color='black', bold=True), | |
235 name=dict(color='blue'), | |
236 programname=dict(color='cyan'), | |
237 username=dict(color='yellow'), | |
238 ) | |
239 """Mapping of log format names to default font styles.""" | |
240 | |
241 DEFAULT_LEVEL_STYLES = dict( | |
242 spam=dict(color='green', faint=True), | |
243 debug=dict(color='green'), | |
244 verbose=dict(color='blue'), | |
245 info=dict(), | |
246 notice=dict(color='magenta'), | |
247 warning=dict(color='yellow'), | |
248 success=dict(color='green', bold=True), | |
249 error=dict(color='red'), | |
250 critical=dict(color='red', bold=True), | |
251 ) | |
252 """Mapping of log level names to default font styles.""" | |
253 | |
254 DEFAULT_FORMAT_STYLE = '%' | |
255 """The default logging format style (a single character).""" | |
256 | |
257 FORMAT_STYLE_PATTERNS = { | |
258 '%': r'%\((\w+)\)[#0 +-]*\d*(?:\.\d+)?[hlL]?[diouxXeEfFgGcrs%]', | |
259 '{': r'{(\w+)[^}]*}', | |
260 '$': r'\$(\w+)|\${(\w+)}', | |
261 } | |
262 """ | |
263 A dictionary that maps the `style` characters ``%``, ``{`` and ``$`` (see the | |
264 documentation of the :class:`python3:logging.Formatter` class in Python 3.2+) | |
265 to strings containing regular expression patterns that can be used to parse | |
266 format strings in the corresponding style: | |
267 | |
268 ``%`` | |
269 A string containing a regular expression that matches a "percent conversion | |
270 specifier" as defined in the `String Formatting Operations`_ section of the | |
271 Python documentation. Here's an example of a logging format string in this | |
272 format: ``%(levelname)s:%(name)s:%(message)s``. | |
273 | |
274 ``{`` | |
275 A string containing a regular expression that matches a "replacement field" as | |
276 defined in the `Format String Syntax`_ section of the Python documentation. | |
277 Here's an example of a logging format string in this format: | |
278 ``{levelname}:{name}:{message}``. | |
279 | |
280 ``$`` | |
281 A string containing a regular expression that matches a "substitution | |
282 placeholder" as defined in the `Template Strings`_ section of the Python | |
283 documentation. Here's an example of a logging format string in this format: | |
284 ``$levelname:$name:$message``. | |
285 | |
286 These regular expressions are used by :class:`FormatStringParser` to introspect | |
287 and manipulate logging format strings. | |
288 | |
289 .. _String Formatting Operations: https://docs.python.org/2/library/stdtypes.html#string-formatting | |
290 .. _Format String Syntax: https://docs.python.org/2/library/string.html#formatstrings | |
291 .. _Template Strings: https://docs.python.org/3/library/string.html#template-strings | |
292 """ | |
293 | |
294 | |
295 def auto_install(): | |
296 """ | |
297 Automatically call :func:`install()` when ``$COLOREDLOGS_AUTO_INSTALL`` is set. | |
298 | |
299 The `coloredlogs` package includes a `path configuration file`_ that | |
300 automatically imports the :mod:`coloredlogs` module and calls | |
301 :func:`auto_install()` when the environment variable | |
302 ``$COLOREDLOGS_AUTO_INSTALL`` is set. | |
303 | |
304 This function uses :func:`~humanfriendly.coerce_boolean()` to check whether | |
305 the value of ``$COLOREDLOGS_AUTO_INSTALL`` should be considered :data:`True`. | |
306 | |
307 .. _path configuration file: https://docs.python.org/2/library/site.html#module-site | |
308 """ | |
309 if coerce_boolean(os.environ.get('COLOREDLOGS_AUTO_INSTALL', 'false')): | |
310 install() | |
311 | |
312 | |
313 def install(level=None, **kw): | |
314 """ | |
315 Enable colored terminal output for Python's :mod:`logging` module. | |
316 | |
317 :param level: The default logging level (an integer or a string with a | |
318 level name, defaults to :data:`DEFAULT_LOG_LEVEL`). | |
319 :param logger: The logger to which the stream handler should be attached (a | |
320 :class:`~logging.Logger` object, defaults to the root logger). | |
321 :param fmt: Set the logging format (a string like those accepted by | |
322 :class:`~logging.Formatter`, defaults to | |
323 :data:`DEFAULT_LOG_FORMAT`). | |
324 :param datefmt: Set the date/time format (a string, defaults to | |
325 :data:`DEFAULT_DATE_FORMAT`). | |
326 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to | |
327 :data:`DEFAULT_FORMAT_STYLE`). See the documentation of the | |
328 :class:`python3:logging.Formatter` class in Python 3.2+. On | |
329 older Python versions only ``%`` is supported. | |
330 :param milliseconds: :data:`True` to show milliseconds like :mod:`logging` | |
331 does by default, :data:`False` to hide milliseconds | |
332 (the default is :data:`False`, see `#16`_). | |
333 :param level_styles: A dictionary with custom level styles (defaults to | |
334 :data:`DEFAULT_LEVEL_STYLES`). | |
335 :param field_styles: A dictionary with custom field styles (defaults to | |
336 :data:`DEFAULT_FIELD_STYLES`). | |
337 :param stream: The stream where log messages should be written to (a | |
338 file-like object). This defaults to :data:`None` which | |
339 means :class:`StandardErrorHandler` is used. | |
340 :param isatty: :data:`True` to use a :class:`ColoredFormatter`, | |
341 :data:`False` to use a normal :class:`~logging.Formatter` | |
342 (defaults to auto-detection using | |
343 :func:`~humanfriendly.terminal.terminal_supports_colors()`). | |
344 :param reconfigure: If :data:`True` (the default) multiple calls to | |
345 :func:`coloredlogs.install()` will each override | |
346 the previous configuration. | |
347 :param use_chroot: Refer to :class:`HostNameFilter`. | |
348 :param programname: Refer to :class:`ProgramNameFilter`. | |
349 :param username: Refer to :class:`UserNameFilter`. | |
350 :param syslog: If :data:`True` then :func:`.enable_system_logging()` will | |
351 be called without arguments (defaults to :data:`False`). The | |
352 `syslog` argument may also be a number or string, in this | |
353 case it is assumed to be a logging level which is passed on | |
354 to :func:`.enable_system_logging()`. | |
355 | |
356 The :func:`coloredlogs.install()` function is similar to | |
357 :func:`logging.basicConfig()`, both functions take a lot of optional | |
358 keyword arguments but try to do the right thing by default: | |
359 | |
360 1. If `reconfigure` is :data:`True` (it is by default) and an existing | |
361 :class:`~logging.StreamHandler` is found that is connected to either | |
362 :data:`~sys.stdout` or :data:`~sys.stderr` the handler will be removed. | |
363 This means that first calling :func:`logging.basicConfig()` and then | |
364 calling :func:`coloredlogs.install()` will replace the stream handler | |
365 instead of adding a duplicate stream handler. If `reconfigure` is | |
366 :data:`False` and an existing handler is found no further steps are | |
367 taken (to avoid installing a duplicate stream handler). | |
368 | |
369 2. A :class:`~logging.StreamHandler` is created and connected to the stream | |
370 given by the `stream` keyword argument (:data:`sys.stderr` by | |
371 default). The stream handler's level is set to the value of the `level` | |
372 keyword argument. | |
373 | |
374 3. A :class:`ColoredFormatter` is created if the `isatty` keyword argument | |
375 allows it (or auto-detection allows it), otherwise a normal | |
376 :class:`~logging.Formatter` is created. The formatter is initialized | |
377 with the `fmt` and `datefmt` keyword arguments (or their computed | |
378 defaults). | |
379 | |
380 The environment variable ``$NO_COLOR`` is taken as a hint by | |
381 auto-detection that colors should not be used. | |
382 | |
383 4. :func:`HostNameFilter.install()`, :func:`ProgramNameFilter.install()` | |
384 and :func:`UserNameFilter.install()` are called to enable the use of | |
385 additional fields in the log format. | |
386 | |
387 5. If the logger's level is too restrictive it is relaxed (refer to `notes | |
388 about log levels`_ for details). | |
389 | |
390 6. The formatter is added to the handler and the handler is added to the | |
391 logger. | |
392 | |
393 .. _#16: https://github.com/xolox/python-coloredlogs/issues/16 | |
394 """ | |
395 logger = kw.get('logger') or logging.getLogger() | |
396 reconfigure = kw.get('reconfigure', True) | |
397 stream = kw.get('stream', sys.stderr) | |
398 style = check_style(kw.get('style') or DEFAULT_FORMAT_STYLE) | |
399 # Get the log level from an argument, environment variable or default and | |
400 # convert the names of log levels to numbers to enable numeric comparison. | |
401 if level is None: | |
402 level = os.environ.get('COLOREDLOGS_LOG_LEVEL', DEFAULT_LOG_LEVEL) | |
403 level = level_to_number(level) | |
404 # Remove any existing stream handler that writes to stdout or stderr, even | |
405 # if the stream handler wasn't created by coloredlogs because multiple | |
406 # stream handlers (in the same hierarchy) writing to stdout or stderr would | |
407 # create duplicate output. `None' is a synonym for the possibly dynamic | |
408 # value of the stderr attribute of the sys module. | |
409 match_streams = ([sys.stdout, sys.stderr] | |
410 if stream in [sys.stdout, sys.stderr, None] | |
411 else [stream]) | |
412 match_handler = lambda handler: match_stream_handler(handler, match_streams) | |
413 handler, logger = replace_handler(logger, match_handler, reconfigure) | |
414 # Make sure reconfiguration is allowed or not relevant. | |
415 if not (handler and not reconfigure): | |
416 # Make it easy to enable system logging. | |
417 syslog_enabled = kw.get('syslog') | |
418 # We ignore the value `None' because it means the caller didn't opt in | |
419 # to system logging and `False' because it means the caller explicitly | |
420 # opted out of system logging. | |
421 if syslog_enabled not in (None, False): | |
422 from coloredlogs.syslog import enable_system_logging | |
423 if syslog_enabled is True: | |
424 # If the caller passed syslog=True then we leave the choice of | |
425 # default log level up to the coloredlogs.syslog module. | |
426 enable_system_logging() | |
427 else: | |
428 # Values other than (None, True, False) are assumed to | |
429 # represent a logging level for system logging. | |
430 enable_system_logging(level=syslog_enabled) | |
431 # Figure out whether we can use ANSI escape sequences. | |
432 use_colors = kw.get('isatty', None) | |
433 # In the following indented block the expression (use_colors is None) | |
434 # can be read as "auto detect is enabled and no reason has yet been | |
435 # found to automatically disable color support". | |
436 if use_colors or (use_colors is None): | |
437 # Respect the user's choice not to have colors. | |
438 if use_colors is None and 'NO_COLOR' in os.environ: | |
439 # For details on this see https://no-color.org/. | |
440 use_colors = False | |
441 # Try to enable Windows native ANSI support or Colorama? | |
442 if (use_colors or use_colors is None) and on_windows(): | |
443 # This can fail, in which case ANSI escape sequences would end | |
444 # up being printed to the terminal in raw form. This is very | |
445 # user hostile, so to avoid this happening we disable color | |
446 # support on failure. | |
447 use_colors = enable_ansi_support() | |
448 # When auto detection is enabled, and so far we encountered no | |
449 # reason to disable color support, then we will enable color | |
450 # support if 'stream' is connected to a terminal. | |
451 if use_colors is None: | |
452 use_colors = terminal_supports_colors(stream) | |
453 # Create a stream handler and make sure to preserve any filters | |
454 # the current handler may have (if an existing handler is found). | |
455 filters = handler.filters if handler else None | |
456 handler = logging.StreamHandler(stream) if stream else StandardErrorHandler() | |
457 handler.setLevel(level) | |
458 if filters: | |
459 handler.filters = filters | |
460 # Prepare the arguments to the formatter, allowing the caller to | |
461 # customize the values of `fmt', `datefmt' and `style' as desired. | |
462 formatter_options = dict(fmt=kw.get('fmt'), datefmt=kw.get('datefmt')) | |
463 # Only pass the `style' argument to the formatter when the caller | |
464 # provided an alternative logging format style. This prevents | |
465 # TypeError exceptions on Python versions before 3.2. | |
466 if style != DEFAULT_FORMAT_STYLE: | |
467 formatter_options['style'] = style | |
468 # Come up with a default log format? | |
469 if not formatter_options['fmt']: | |
470 # Use the log format defined by the environment variable | |
471 # $COLOREDLOGS_LOG_FORMAT or fall back to the default. | |
472 formatter_options['fmt'] = os.environ.get('COLOREDLOGS_LOG_FORMAT') or DEFAULT_LOG_FORMAT | |
473 # If the caller didn't specify a date/time format we'll use the format | |
474 # defined by the environment variable $COLOREDLOGS_DATE_FORMAT (or fall | |
475 # back to the default). | |
476 if not formatter_options['datefmt']: | |
477 formatter_options['datefmt'] = os.environ.get('COLOREDLOGS_DATE_FORMAT') or DEFAULT_DATE_FORMAT | |
478 # Python's logging module shows milliseconds by default through special | |
479 # handling in the logging.Formatter.formatTime() method [1]. Because | |
480 # coloredlogs always defines a `datefmt' it bypasses this special | |
481 # handling, which is fine because ever since publishing coloredlogs | |
482 # I've never needed millisecond precision ;-). However there are users | |
483 # of coloredlogs that do want milliseconds to be shown [2] so we | |
484 # provide a shortcut to make it easy. | |
485 # | |
486 # [1] https://stackoverflow.com/questions/6290739/python-logging-use-milliseconds-in-time-format | |
487 # [2] https://github.com/xolox/python-coloredlogs/issues/16 | |
488 if kw.get('milliseconds'): | |
489 parser = FormatStringParser(style=style) | |
490 if not (parser.contains_field(formatter_options['fmt'], 'msecs') | |
491 or '%f' in formatter_options['datefmt']): | |
492 pattern = parser.get_pattern('asctime') | |
493 replacements = {'%': '%(msecs)03d', '{': '{msecs:03}', '$': '${msecs}'} | |
494 formatter_options['fmt'] = pattern.sub( | |
495 r'\g<0>,' + replacements[style], | |
496 formatter_options['fmt'], | |
497 ) | |
498 # Do we need to make %(hostname) available to the formatter? | |
499 HostNameFilter.install( | |
500 fmt=formatter_options['fmt'], | |
501 handler=handler, | |
502 style=style, | |
503 use_chroot=kw.get('use_chroot', True), | |
504 ) | |
505 # Do we need to make %(programname) available to the formatter? | |
506 ProgramNameFilter.install( | |
507 fmt=formatter_options['fmt'], | |
508 handler=handler, | |
509 programname=kw.get('programname'), | |
510 style=style, | |
511 ) | |
512 # Do we need to make %(username) available to the formatter? | |
513 UserNameFilter.install( | |
514 fmt=formatter_options['fmt'], | |
515 handler=handler, | |
516 username=kw.get('username'), | |
517 style=style, | |
518 ) | |
519 # Inject additional formatter arguments specific to ColoredFormatter? | |
520 if use_colors: | |
521 for name, environment_name in (('field_styles', 'COLOREDLOGS_FIELD_STYLES'), | |
522 ('level_styles', 'COLOREDLOGS_LEVEL_STYLES')): | |
523 value = kw.get(name) | |
524 if value is None: | |
525 # If no styles have been specified we'll fall back | |
526 # to the styles defined by the environment variable. | |
527 environment_value = os.environ.get(environment_name) | |
528 if environment_value is not None: | |
529 value = parse_encoded_styles(environment_value) | |
530 if value is not None: | |
531 formatter_options[name] = value | |
532 # Create a (possibly colored) formatter. | |
533 formatter_type = ColoredFormatter if use_colors else BasicFormatter | |
534 handler.setFormatter(formatter_type(**formatter_options)) | |
535 # Adjust the level of the selected logger. | |
536 adjust_level(logger, level) | |
537 # Install the stream handler. | |
538 logger.addHandler(handler) | |
539 | |
540 | |
541 def check_style(value): | |
542 """ | |
543 Validate a logging format style. | |
544 | |
545 :param value: The logging format style to validate (any value). | |
546 :returns: The logging format character (a string of one character). | |
547 :raises: :exc:`~exceptions.ValueError` when the given style isn't supported. | |
548 | |
549 On Python 3.2+ this function accepts the logging format styles ``%``, ``{`` | |
550 and ``$`` while on older versions only ``%`` is accepted (because older | |
551 Python versions don't support alternative logging format styles). | |
552 """ | |
553 if sys.version_info[:2] >= (3, 2): | |
554 if value not in FORMAT_STYLE_PATTERNS: | |
555 msg = "Unsupported logging format style! (%r)" | |
556 raise ValueError(format(msg, value)) | |
557 elif value != DEFAULT_FORMAT_STYLE: | |
558 msg = "Format string styles other than %r require Python 3.2+!" | |
559 raise ValueError(msg, DEFAULT_FORMAT_STYLE) | |
560 return value | |
561 | |
562 | |
563 def increase_verbosity(): | |
564 """ | |
565 Increase the verbosity of the root handler by one defined level. | |
566 | |
567 Understands custom logging levels like defined by my ``verboselogs`` | |
568 module. | |
569 """ | |
570 defined_levels = sorted(set(find_defined_levels().values())) | |
571 current_index = defined_levels.index(get_level()) | |
572 selected_index = max(0, current_index - 1) | |
573 set_level(defined_levels[selected_index]) | |
574 | |
575 | |
576 def decrease_verbosity(): | |
577 """ | |
578 Decrease the verbosity of the root handler by one defined level. | |
579 | |
580 Understands custom logging levels like defined by my ``verboselogs`` | |
581 module. | |
582 """ | |
583 defined_levels = sorted(set(find_defined_levels().values())) | |
584 current_index = defined_levels.index(get_level()) | |
585 selected_index = min(current_index + 1, len(defined_levels) - 1) | |
586 set_level(defined_levels[selected_index]) | |
587 | |
588 | |
589 def is_verbose(): | |
590 """ | |
591 Check whether the log level of the root handler is set to a verbose level. | |
592 | |
593 :returns: ``True`` if the root handler is verbose, ``False`` if not. | |
594 """ | |
595 return get_level() < DEFAULT_LOG_LEVEL | |
596 | |
597 | |
598 def get_level(): | |
599 """ | |
600 Get the logging level of the root handler. | |
601 | |
602 :returns: The logging level of the root handler (an integer) or | |
603 :data:`DEFAULT_LOG_LEVEL` (if no root handler exists). | |
604 """ | |
605 handler, logger = find_handler(logging.getLogger(), match_stream_handler) | |
606 return handler.level if handler else DEFAULT_LOG_LEVEL | |
607 | |
608 | |
609 def set_level(level): | |
610 """ | |
611 Set the logging level of the root handler. | |
612 | |
613 :param level: The logging level to filter on (an integer or string). | |
614 | |
615 If no root handler exists yet this automatically calls :func:`install()`. | |
616 """ | |
617 handler, logger = find_handler(logging.getLogger(), match_stream_handler) | |
618 if handler and logger: | |
619 # Change the level of the existing handler. | |
620 handler.setLevel(level_to_number(level)) | |
621 # Adjust the level of the selected logger. | |
622 adjust_level(logger, level) | |
623 else: | |
624 # Create a new handler with the given level. | |
625 install(level=level) | |
626 | |
627 | |
628 def adjust_level(logger, level): | |
629 """ | |
630 Increase a logger's verbosity up to the requested level. | |
631 | |
632 :param logger: The logger to change (a :class:`~logging.Logger` object). | |
633 :param level: The log level to enable (a string or number). | |
634 | |
635 This function is used by functions like :func:`install()`, | |
636 :func:`increase_verbosity()` and :func:`.enable_system_logging()` to adjust | |
637 a logger's level so that log messages up to the requested log level are | |
638 propagated to the configured output handler(s). | |
639 | |
640 It uses :func:`logging.Logger.getEffectiveLevel()` to check whether | |
641 `logger` propagates or swallows log messages of the requested `level` and | |
642 sets the logger's level to the requested level if it would otherwise | |
643 swallow log messages. | |
644 | |
645 Effectively this function will "widen the scope of logging" when asked to | |
646 do so but it will never "narrow the scope of logging". This is because I am | |
647 convinced that filtering of log messages should (primarily) be decided by | |
648 handlers. | |
649 """ | |
650 level = level_to_number(level) | |
651 if logger.getEffectiveLevel() > level: | |
652 logger.setLevel(level) | |
653 | |
654 | |
655 def find_defined_levels(): | |
656 """ | |
657 Find the defined logging levels. | |
658 | |
659 :returns: A dictionary with level names as keys and integers as values. | |
660 | |
661 Here's what the result looks like by default (when | |
662 no custom levels or level names have been defined): | |
663 | |
664 >>> find_defined_levels() | |
665 {'NOTSET': 0, | |
666 'DEBUG': 10, | |
667 'INFO': 20, | |
668 'WARN': 30, | |
669 'WARNING': 30, | |
670 'ERROR': 40, | |
671 'FATAL': 50, | |
672 'CRITICAL': 50} | |
673 """ | |
674 defined_levels = {} | |
675 for name in dir(logging): | |
676 if name.isupper(): | |
677 value = getattr(logging, name) | |
678 if isinstance(value, int): | |
679 defined_levels[name] = value | |
680 return defined_levels | |
681 | |
682 | |
683 def level_to_number(value): | |
684 """ | |
685 Coerce a logging level name to a number. | |
686 | |
687 :param value: A logging level (integer or string). | |
688 :returns: The number of the log level (an integer). | |
689 | |
690 This function translates log level names into their numeric values.. | |
691 """ | |
692 if is_string(value): | |
693 try: | |
694 defined_levels = find_defined_levels() | |
695 value = defined_levels[value.upper()] | |
696 except KeyError: | |
697 # Don't fail on unsupported log levels. | |
698 value = DEFAULT_LOG_LEVEL | |
699 return value | |
700 | |
701 | |
702 def find_level_aliases(): | |
703 """ | |
704 Find log level names which are aliases of each other. | |
705 | |
706 :returns: A dictionary that maps aliases to their canonical name. | |
707 | |
708 .. note:: Canonical names are chosen to be the alias with the longest | |
709 string length so that e.g. ``WARN`` is an alias for ``WARNING`` | |
710 instead of the other way around. | |
711 | |
712 Here's what the result looks like by default (when | |
713 no custom levels or level names have been defined): | |
714 | |
715 >>> from coloredlogs import find_level_aliases | |
716 >>> find_level_aliases() | |
717 {'WARN': 'WARNING', 'FATAL': 'CRITICAL'} | |
718 """ | |
719 mapping = collections.defaultdict(list) | |
720 for name, value in find_defined_levels().items(): | |
721 mapping[value].append(name) | |
722 aliases = {} | |
723 for value, names in mapping.items(): | |
724 if len(names) > 1: | |
725 names = sorted(names, key=lambda n: len(n)) | |
726 canonical_name = names.pop() | |
727 for alias in names: | |
728 aliases[alias] = canonical_name | |
729 return aliases | |
730 | |
731 | |
732 def parse_encoded_styles(text, normalize_key=None): | |
733 """ | |
734 Parse text styles encoded in a string into a nested data structure. | |
735 | |
736 :param text: The encoded styles (a string). | |
737 :returns: A dictionary in the structure of the :data:`DEFAULT_FIELD_STYLES` | |
738 and :data:`DEFAULT_LEVEL_STYLES` dictionaries. | |
739 | |
740 Here's an example of how this function works: | |
741 | |
742 >>> from coloredlogs import parse_encoded_styles | |
743 >>> from pprint import pprint | |
744 >>> encoded_styles = 'debug=green;warning=yellow;error=red;critical=red,bold' | |
745 >>> pprint(parse_encoded_styles(encoded_styles)) | |
746 {'debug': {'color': 'green'}, | |
747 'warning': {'color': 'yellow'}, | |
748 'error': {'color': 'red'}, | |
749 'critical': {'bold': True, 'color': 'red'}} | |
750 """ | |
751 parsed_styles = {} | |
752 for assignment in split(text, ';'): | |
753 name, _, styles = assignment.partition('=') | |
754 target = parsed_styles.setdefault(name, {}) | |
755 for token in split(styles, ','): | |
756 # When this code was originally written, setting background colors | |
757 # wasn't supported yet, so there was no need to disambiguate | |
758 # between the text color and background color. This explains why | |
759 # a color name or number implies setting the text color (for | |
760 # backwards compatibility). | |
761 if token.isdigit(): | |
762 target['color'] = int(token) | |
763 elif token in ANSI_COLOR_CODES: | |
764 target['color'] = token | |
765 elif '=' in token: | |
766 name, _, value = token.partition('=') | |
767 if name in ('color', 'background'): | |
768 if value.isdigit(): | |
769 target[name] = int(value) | |
770 elif value in ANSI_COLOR_CODES: | |
771 target[name] = value | |
772 else: | |
773 target[token] = True | |
774 return parsed_styles | |
775 | |
776 | |
777 def find_hostname(use_chroot=True): | |
778 """ | |
779 Find the host name to include in log messages. | |
780 | |
781 :param use_chroot: Use the name of the chroot when inside a chroot? | |
782 (boolean, defaults to :data:`True`) | |
783 :returns: A suitable host name (a string). | |
784 | |
785 Looks for :data:`CHROOT_FILES` that have a nonempty first line (taken to be | |
786 the chroot name). If none are found then :func:`socket.gethostname()` is | |
787 used as a fall back. | |
788 """ | |
789 for chroot_file in CHROOT_FILES: | |
790 try: | |
791 with open(chroot_file) as handle: | |
792 first_line = next(handle) | |
793 name = first_line.strip() | |
794 if name: | |
795 return name | |
796 except Exception: | |
797 pass | |
798 return socket.gethostname() | |
799 | |
800 | |
801 def find_program_name(): | |
802 """ | |
803 Select a suitable program name to embed in log messages. | |
804 | |
805 :returns: One of the following strings (in decreasing order of preference): | |
806 | |
807 1. The base name of the currently running Python program or | |
808 script (based on the value at index zero of :data:`sys.argv`). | |
809 2. The base name of the Python executable (based on | |
810 :data:`sys.executable`). | |
811 3. The string 'python'. | |
812 """ | |
813 # Gotcha: sys.argv[0] is '-c' if Python is started with the -c option. | |
814 return ((os.path.basename(sys.argv[0]) if sys.argv and sys.argv[0] != '-c' else '') | |
815 or (os.path.basename(sys.executable) if sys.executable else '') | |
816 or 'python') | |
817 | |
818 | |
819 def find_username(): | |
820 """ | |
821 Find the username to include in log messages. | |
822 | |
823 :returns: A suitable username (a string). | |
824 | |
825 On UNIX systems this uses the :mod:`pwd` module which means ``root`` will | |
826 be reported when :man:`sudo` is used (as it should). If this fails (for | |
827 example on Windows) then :func:`getpass.getuser()` is used as a fall back. | |
828 """ | |
829 try: | |
830 import pwd | |
831 uid = os.getuid() | |
832 entry = pwd.getpwuid(uid) | |
833 return entry.pw_name | |
834 except Exception: | |
835 import getpass | |
836 return getpass.getuser() | |
837 | |
838 | |
839 def replace_handler(logger, match_handler, reconfigure): | |
840 """ | |
841 Prepare to replace a handler. | |
842 | |
843 :param logger: Refer to :func:`find_handler()`. | |
844 :param match_handler: Refer to :func:`find_handler()`. | |
845 :param reconfigure: :data:`True` if an existing handler should be replaced, | |
846 :data:`False` otherwise. | |
847 :returns: A tuple of two values: | |
848 | |
849 1. The matched :class:`~logging.Handler` object or :data:`None` | |
850 if no handler was matched. | |
851 2. The :class:`~logging.Logger` to which the matched handler was | |
852 attached or the logger given to :func:`replace_handler()`. | |
853 """ | |
854 handler, other_logger = find_handler(logger, match_handler) | |
855 if handler and other_logger and reconfigure: | |
856 # Remove the existing handler from the logger that its attached to | |
857 # so that we can install a new handler that behaves differently. | |
858 other_logger.removeHandler(handler) | |
859 # Switch to the logger that the existing handler was attached to so | |
860 # that reconfiguration doesn't narrow the scope of logging. | |
861 logger = other_logger | |
862 return handler, logger | |
863 | |
864 | |
865 def find_handler(logger, match_handler): | |
866 """ | |
867 Find a (specific type of) handler in the propagation tree of a logger. | |
868 | |
869 :param logger: The logger to check (a :class:`~logging.Logger` object). | |
870 :param match_handler: A callable that receives a :class:`~logging.Handler` | |
871 object and returns :data:`True` to match a handler or | |
872 :data:`False` to skip that handler and continue | |
873 searching for a match. | |
874 :returns: A tuple of two values: | |
875 | |
876 1. The matched :class:`~logging.Handler` object or :data:`None` | |
877 if no handler was matched. | |
878 2. The :class:`~logging.Logger` object to which the handler is | |
879 attached or :data:`None` if no handler was matched. | |
880 | |
881 This function finds a logging handler (of the given type) attached to a | |
882 logger or one of its parents (see :func:`walk_propagation_tree()`). It uses | |
883 the undocumented :class:`~logging.Logger.handlers` attribute to find | |
884 handlers attached to a logger, however it won't raise an exception if the | |
885 attribute isn't available. The advantages of this approach are: | |
886 | |
887 - This works regardless of whether :mod:`coloredlogs` attached the handler | |
888 or other Python code attached the handler. | |
889 | |
890 - This will correctly recognize the situation where the given logger has no | |
891 handlers but :attr:`~logging.Logger.propagate` is enabled and the logger | |
892 has a parent logger that does have a handler attached. | |
893 """ | |
894 for logger in walk_propagation_tree(logger): | |
895 for handler in getattr(logger, 'handlers', []): | |
896 if match_handler(handler): | |
897 return handler, logger | |
898 return None, None | |
899 | |
900 | |
901 def match_stream_handler(handler, streams=[]): | |
902 """ | |
903 Identify stream handlers writing to the given streams(s). | |
904 | |
905 :param handler: The :class:`~logging.Handler` class to check. | |
906 :param streams: A sequence of streams to match (defaults to matching | |
907 :data:`~sys.stdout` and :data:`~sys.stderr`). | |
908 :returns: :data:`True` if the handler is a :class:`~logging.StreamHandler` | |
909 logging to the given stream(s), :data:`False` otherwise. | |
910 | |
911 This function can be used as a callback for :func:`find_handler()`. | |
912 """ | |
913 return (isinstance(handler, logging.StreamHandler) | |
914 and getattr(handler, 'stream') in (streams or (sys.stdout, sys.stderr))) | |
915 | |
916 | |
917 def walk_propagation_tree(logger): | |
918 """ | |
919 Walk through the propagation hierarchy of the given logger. | |
920 | |
921 :param logger: The logger whose hierarchy to walk (a | |
922 :class:`~logging.Logger` object). | |
923 :returns: A generator of :class:`~logging.Logger` objects. | |
924 | |
925 .. note:: This uses the undocumented :class:`logging.Logger.parent` | |
926 attribute to find higher level loggers, however it won't | |
927 raise an exception if the attribute isn't available. | |
928 """ | |
929 while isinstance(logger, logging.Logger): | |
930 # Yield the logger to our caller. | |
931 yield logger | |
932 # Check if the logger has propagation enabled. | |
933 if logger.propagate: | |
934 # Continue with the parent logger. We use getattr() because the | |
935 # `parent' attribute isn't documented so properly speaking we | |
936 # shouldn't break if it's not available. | |
937 logger = getattr(logger, 'parent', None) | |
938 else: | |
939 # The propagation chain stops here. | |
940 logger = None | |
941 | |
942 | |
943 class BasicFormatter(logging.Formatter): | |
944 | |
945 """ | |
946 Log :class:`~logging.Formatter` that supports ``%f`` for millisecond formatting. | |
947 | |
948 This class extends :class:`~logging.Formatter` to enable the use of ``%f`` | |
949 for millisecond formatting in date/time strings, to allow for the type of | |
950 flexibility requested in issue `#45`_. | |
951 | |
952 .. _#45: https://github.com/xolox/python-coloredlogs/issues/45 | |
953 """ | |
954 | |
955 def formatTime(self, record, datefmt=None): | |
956 """ | |
957 Format the date/time of a log record. | |
958 | |
959 :param record: A :class:`~logging.LogRecord` object. | |
960 :param datefmt: A date/time format string (defaults to :data:`DEFAULT_DATE_FORMAT`). | |
961 :returns: The formatted date/time (a string). | |
962 | |
963 This method overrides :func:`~logging.Formatter.formatTime()` to set | |
964 `datefmt` to :data:`DEFAULT_DATE_FORMAT` when the caller hasn't | |
965 specified a date format. | |
966 | |
967 When `datefmt` contains the token ``%f`` it will be replaced by the | |
968 value of ``%(msecs)03d`` (refer to issue `#45`_ for use cases). | |
969 """ | |
970 # The default value of the following argument is defined here so | |
971 # that Sphinx doesn't embed the default value in the generated | |
972 # documentation (because the result is awkward to read). | |
973 datefmt = datefmt or DEFAULT_DATE_FORMAT | |
974 # Replace %f with the value of %(msecs)03d. | |
975 if '%f' in datefmt: | |
976 datefmt = datefmt.replace('%f', '%03d' % record.msecs) | |
977 # Delegate the actual date/time formatting to the base formatter. | |
978 return logging.Formatter.formatTime(self, record, datefmt) | |
979 | |
980 | |
981 class ColoredFormatter(BasicFormatter): | |
982 | |
983 """ | |
984 Log :class:`~logging.Formatter` that uses `ANSI escape sequences`_ to create colored logs. | |
985 | |
986 :class:`ColoredFormatter` inherits from :class:`BasicFormatter` to enable | |
987 the use of ``%f`` for millisecond formatting in date/time strings. | |
988 | |
989 .. note:: If you want to use :class:`ColoredFormatter` on Windows then you | |
990 need to call :func:`~humanfriendly.terminal.enable_ansi_support()`. | |
991 This is done for you when you call :func:`coloredlogs.install()`. | |
992 """ | |
993 | |
994 def __init__(self, fmt=None, datefmt=None, style=DEFAULT_FORMAT_STYLE, level_styles=None, field_styles=None): | |
995 """ | |
996 Initialize a :class:`ColoredFormatter` object. | |
997 | |
998 :param fmt: A log format string (defaults to :data:`DEFAULT_LOG_FORMAT`). | |
999 :param datefmt: A date/time format string (defaults to :data:`None`, | |
1000 but see the documentation of | |
1001 :func:`BasicFormatter.formatTime()`). | |
1002 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to | |
1003 :data:`DEFAULT_FORMAT_STYLE`) | |
1004 :param level_styles: A dictionary with custom level styles | |
1005 (defaults to :data:`DEFAULT_LEVEL_STYLES`). | |
1006 :param field_styles: A dictionary with custom field styles | |
1007 (defaults to :data:`DEFAULT_FIELD_STYLES`). | |
1008 :raises: Refer to :func:`check_style()`. | |
1009 | |
1010 This initializer uses :func:`colorize_format()` to inject ANSI escape | |
1011 sequences in the log format string before it is passed to the | |
1012 initializer of the base class. | |
1013 """ | |
1014 self.nn = NameNormalizer() | |
1015 # The default values of the following arguments are defined here so | |
1016 # that Sphinx doesn't embed the default values in the generated | |
1017 # documentation (because the result is awkward to read). | |
1018 fmt = fmt or DEFAULT_LOG_FORMAT | |
1019 self.level_styles = self.nn.normalize_keys(DEFAULT_LEVEL_STYLES if level_styles is None else level_styles) | |
1020 self.field_styles = self.nn.normalize_keys(DEFAULT_FIELD_STYLES if field_styles is None else field_styles) | |
1021 # Rewrite the format string to inject ANSI escape sequences. | |
1022 kw = dict(fmt=self.colorize_format(fmt, style), datefmt=datefmt) | |
1023 # If we were given a non-default logging format style we pass it on | |
1024 # to our superclass. At this point check_style() will have already | |
1025 # complained that the use of alternative logging format styles | |
1026 # requires Python 3.2 or newer. | |
1027 if style != DEFAULT_FORMAT_STYLE: | |
1028 kw['style'] = style | |
1029 # Initialize the superclass with the rewritten format string. | |
1030 logging.Formatter.__init__(self, **kw) | |
1031 | |
1032 def colorize_format(self, fmt, style=DEFAULT_FORMAT_STYLE): | |
1033 """ | |
1034 Rewrite a logging format string to inject ANSI escape sequences. | |
1035 | |
1036 :param fmt: The log format string. | |
1037 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to | |
1038 :data:`DEFAULT_FORMAT_STYLE`). | |
1039 :returns: The logging format string with ANSI escape sequences. | |
1040 | |
1041 This method takes a logging format string like the ones you give to | |
1042 :class:`logging.Formatter` and processes it as follows: | |
1043 | |
1044 1. First the logging format string is separated into formatting | |
1045 directives versus surrounding text (according to the given `style`). | |
1046 | |
1047 2. Then formatting directives and surrounding text are grouped | |
1048 based on whitespace delimiters (in the surrounding text). | |
1049 | |
1050 3. For each group styling is selected as follows: | |
1051 | |
1052 1. If the group contains a single formatting directive that has | |
1053 a style defined then the whole group is styled accordingly. | |
1054 | |
1055 2. If the group contains multiple formatting directives that | |
1056 have styles defined then each formatting directive is styled | |
1057 individually and surrounding text isn't styled. | |
1058 | |
1059 As an example consider the default log format (:data:`DEFAULT_LOG_FORMAT`):: | |
1060 | |
1061 %(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s | |
1062 | |
1063 The default field styles (:data:`DEFAULT_FIELD_STYLES`) define a style for the | |
1064 `name` field but not for the `process` field, however because both fields | |
1065 are part of the same whitespace delimited token they'll be highlighted | |
1066 together in the style defined for the `name` field. | |
1067 """ | |
1068 result = [] | |
1069 parser = FormatStringParser(style=style) | |
1070 for group in parser.get_grouped_pairs(fmt): | |
1071 applicable_styles = [self.nn.get(self.field_styles, token.name) for token in group if token.name] | |
1072 if sum(map(bool, applicable_styles)) == 1: | |
1073 # If exactly one (1) field style is available for the group of | |
1074 # tokens then all of the tokens will be styled the same way. | |
1075 # This provides a limited form of backwards compatibility with | |
1076 # the (intended) behavior of coloredlogs before the release of | |
1077 # version 10. | |
1078 result.append(ansi_wrap( | |
1079 ''.join(token.text for token in group), | |
1080 **next(s for s in applicable_styles if s) | |
1081 )) | |
1082 else: | |
1083 for token in group: | |
1084 text = token.text | |
1085 if token.name: | |
1086 field_styles = self.nn.get(self.field_styles, token.name) | |
1087 if field_styles: | |
1088 text = ansi_wrap(text, **field_styles) | |
1089 result.append(text) | |
1090 return ''.join(result) | |
1091 | |
1092 def format(self, record): | |
1093 """ | |
1094 Apply level-specific styling to log records. | |
1095 | |
1096 :param record: A :class:`~logging.LogRecord` object. | |
1097 :returns: The result of :func:`logging.Formatter.format()`. | |
1098 | |
1099 This method injects ANSI escape sequences that are specific to the | |
1100 level of each log record (because such logic cannot be expressed in the | |
1101 syntax of a log format string). It works by making a copy of the log | |
1102 record, changing the `msg` field inside the copy and passing the copy | |
1103 into the :func:`~logging.Formatter.format()` method of the base | |
1104 class. | |
1105 """ | |
1106 style = self.nn.get(self.level_styles, record.levelname) | |
1107 # After the introduction of the `Empty' class it was reported in issue | |
1108 # 33 that format() can be called when `Empty' has already been garbage | |
1109 # collected. This explains the (otherwise rather out of place) `Empty | |
1110 # is not None' check in the following `if' statement. The reasoning | |
1111 # here is that it's much better to log a message without formatting | |
1112 # then to raise an exception ;-). | |
1113 # | |
1114 # For more details refer to issue 33 on GitHub: | |
1115 # https://github.com/xolox/python-coloredlogs/issues/33 | |
1116 if style and Empty is not None: | |
1117 # Due to the way that Python's logging module is structured and | |
1118 # documented the only (IMHO) clean way to customize its behavior is | |
1119 # to change incoming LogRecord objects before they get to the base | |
1120 # formatter. However we don't want to break other formatters and | |
1121 # handlers, so we copy the log record. | |
1122 # | |
1123 # In the past this used copy.copy() but as reported in issue 29 | |
1124 # (which is reproducible) this can cause deadlocks. The following | |
1125 # Python voodoo is intended to accomplish the same thing as | |
1126 # copy.copy() without all of the generalization and overhead that | |
1127 # we don't need for our -very limited- use case. | |
1128 # | |
1129 # For more details refer to issue 29 on GitHub: | |
1130 # https://github.com/xolox/python-coloredlogs/issues/29 | |
1131 copy = Empty() | |
1132 copy.__class__ = record.__class__ | |
1133 copy.__dict__.update(record.__dict__) | |
1134 copy.msg = ansi_wrap(coerce_string(record.msg), **style) | |
1135 record = copy | |
1136 # Delegate the remaining formatting to the base formatter. | |
1137 return logging.Formatter.format(self, record) | |
1138 | |
1139 | |
1140 class Empty(object): | |
1141 """An empty class used to copy :class:`~logging.LogRecord` objects without reinitializing them.""" | |
1142 | |
1143 | |
1144 class HostNameFilter(logging.Filter): | |
1145 | |
1146 """ | |
1147 Log filter to enable the ``%(hostname)s`` format. | |
1148 | |
1149 Python's :mod:`logging` module doesn't expose the system's host name while | |
1150 I consider this to be a valuable addition. Fortunately it's very easy to | |
1151 expose additional fields in format strings: :func:`filter()` simply sets | |
1152 the ``hostname`` attribute of each :class:`~logging.LogRecord` object it | |
1153 receives and this is enough to enable the use of the ``%(hostname)s`` | |
1154 expression in format strings. | |
1155 | |
1156 You can install this log filter as follows:: | |
1157 | |
1158 >>> import coloredlogs, logging | |
1159 >>> handler = logging.StreamHandler() | |
1160 >>> handler.addFilter(coloredlogs.HostNameFilter()) | |
1161 >>> handler.setFormatter(logging.Formatter('[%(hostname)s] %(message)s')) | |
1162 >>> logger = logging.getLogger() | |
1163 >>> logger.addHandler(handler) | |
1164 >>> logger.setLevel(logging.INFO) | |
1165 >>> logger.info("Does it work?") | |
1166 [peter-macbook] Does it work? | |
1167 | |
1168 Of course :func:`coloredlogs.install()` does all of this for you :-). | |
1169 """ | |
1170 | |
1171 @classmethod | |
1172 def install(cls, handler, fmt=None, use_chroot=True, style=DEFAULT_FORMAT_STYLE): | |
1173 """ | |
1174 Install the :class:`HostNameFilter` on a log handler (only if needed). | |
1175 | |
1176 :param fmt: The log format string to check for ``%(hostname)``. | |
1177 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to | |
1178 :data:`DEFAULT_FORMAT_STYLE`). | |
1179 :param handler: The logging handler on which to install the filter. | |
1180 :param use_chroot: Refer to :func:`find_hostname()`. | |
1181 | |
1182 If `fmt` is given the filter will only be installed if `fmt` uses the | |
1183 ``hostname`` field. If `fmt` is not given the filter is installed | |
1184 unconditionally. | |
1185 """ | |
1186 if fmt: | |
1187 parser = FormatStringParser(style=style) | |
1188 if not parser.contains_field(fmt, 'hostname'): | |
1189 return | |
1190 handler.addFilter(cls(use_chroot)) | |
1191 | |
1192 def __init__(self, use_chroot=True): | |
1193 """ | |
1194 Initialize a :class:`HostNameFilter` object. | |
1195 | |
1196 :param use_chroot: Refer to :func:`find_hostname()`. | |
1197 """ | |
1198 self.hostname = find_hostname(use_chroot) | |
1199 | |
1200 def filter(self, record): | |
1201 """Set each :class:`~logging.LogRecord`'s `hostname` field.""" | |
1202 # Modify the record. | |
1203 record.hostname = self.hostname | |
1204 # Don't filter the record. | |
1205 return 1 | |
1206 | |
1207 | |
1208 class ProgramNameFilter(logging.Filter): | |
1209 | |
1210 """ | |
1211 Log filter to enable the ``%(programname)s`` format. | |
1212 | |
1213 Python's :mod:`logging` module doesn't expose the name of the currently | |
1214 running program while I consider this to be a useful addition. Fortunately | |
1215 it's very easy to expose additional fields in format strings: | |
1216 :func:`filter()` simply sets the ``programname`` attribute of each | |
1217 :class:`~logging.LogRecord` object it receives and this is enough to enable | |
1218 the use of the ``%(programname)s`` expression in format strings. | |
1219 | |
1220 Refer to :class:`HostNameFilter` for an example of how to manually install | |
1221 these log filters. | |
1222 """ | |
1223 | |
1224 @classmethod | |
1225 def install(cls, handler, fmt, programname=None, style=DEFAULT_FORMAT_STYLE): | |
1226 """ | |
1227 Install the :class:`ProgramNameFilter` (only if needed). | |
1228 | |
1229 :param fmt: The log format string to check for ``%(programname)``. | |
1230 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to | |
1231 :data:`DEFAULT_FORMAT_STYLE`). | |
1232 :param handler: The logging handler on which to install the filter. | |
1233 :param programname: Refer to :func:`__init__()`. | |
1234 | |
1235 If `fmt` is given the filter will only be installed if `fmt` uses the | |
1236 ``programname`` field. If `fmt` is not given the filter is installed | |
1237 unconditionally. | |
1238 """ | |
1239 if fmt: | |
1240 parser = FormatStringParser(style=style) | |
1241 if not parser.contains_field(fmt, 'programname'): | |
1242 return | |
1243 handler.addFilter(cls(programname)) | |
1244 | |
1245 def __init__(self, programname=None): | |
1246 """ | |
1247 Initialize a :class:`ProgramNameFilter` object. | |
1248 | |
1249 :param programname: The program name to use (defaults to the result of | |
1250 :func:`find_program_name()`). | |
1251 """ | |
1252 self.programname = programname or find_program_name() | |
1253 | |
1254 def filter(self, record): | |
1255 """Set each :class:`~logging.LogRecord`'s `programname` field.""" | |
1256 # Modify the record. | |
1257 record.programname = self.programname | |
1258 # Don't filter the record. | |
1259 return 1 | |
1260 | |
1261 | |
1262 class UserNameFilter(logging.Filter): | |
1263 | |
1264 """ | |
1265 Log filter to enable the ``%(username)s`` format. | |
1266 | |
1267 Python's :mod:`logging` module doesn't expose the username of the currently | |
1268 logged in user as requested in `#76`_. Given that :class:`HostNameFilter` | |
1269 and :class:`ProgramNameFilter` are already provided by `coloredlogs` it | |
1270 made sense to provide :class:`UserNameFilter` as well. | |
1271 | |
1272 Refer to :class:`HostNameFilter` for an example of how to manually install | |
1273 these log filters. | |
1274 | |
1275 .. _#76: https://github.com/xolox/python-coloredlogs/issues/76 | |
1276 """ | |
1277 | |
1278 @classmethod | |
1279 def install(cls, handler, fmt, username=None, style=DEFAULT_FORMAT_STYLE): | |
1280 """ | |
1281 Install the :class:`UserNameFilter` (only if needed). | |
1282 | |
1283 :param fmt: The log format string to check for ``%(username)``. | |
1284 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to | |
1285 :data:`DEFAULT_FORMAT_STYLE`). | |
1286 :param handler: The logging handler on which to install the filter. | |
1287 :param username: Refer to :func:`__init__()`. | |
1288 | |
1289 If `fmt` is given the filter will only be installed if `fmt` uses the | |
1290 ``username`` field. If `fmt` is not given the filter is installed | |
1291 unconditionally. | |
1292 """ | |
1293 if fmt: | |
1294 parser = FormatStringParser(style=style) | |
1295 if not parser.contains_field(fmt, 'username'): | |
1296 return | |
1297 handler.addFilter(cls(username)) | |
1298 | |
1299 def __init__(self, username=None): | |
1300 """ | |
1301 Initialize a :class:`UserNameFilter` object. | |
1302 | |
1303 :param username: The username to use (defaults to the | |
1304 result of :func:`find_username()`). | |
1305 """ | |
1306 self.username = username or find_username() | |
1307 | |
1308 def filter(self, record): | |
1309 """Set each :class:`~logging.LogRecord`'s `username` field.""" | |
1310 # Modify the record. | |
1311 record.username = self.username | |
1312 # Don't filter the record. | |
1313 return 1 | |
1314 | |
1315 | |
1316 class StandardErrorHandler(logging.StreamHandler): | |
1317 | |
1318 """ | |
1319 A :class:`~logging.StreamHandler` that gets the value of :data:`sys.stderr` for each log message. | |
1320 | |
1321 The :class:`StandardErrorHandler` class enables `monkey patching of | |
1322 sys.stderr <https://github.com/xolox/python-coloredlogs/pull/31>`_. It's | |
1323 basically the same as the ``logging._StderrHandler`` class present in | |
1324 Python 3 but it will be available regardless of Python version. This | |
1325 handler is used by :func:`coloredlogs.install()` to improve compatibility | |
1326 with the Python standard library. | |
1327 """ | |
1328 | |
1329 def __init__(self, level=logging.NOTSET): | |
1330 """Initialize a :class:`StandardErrorHandler` object.""" | |
1331 logging.Handler.__init__(self, level) | |
1332 | |
1333 @property | |
1334 def stream(self): | |
1335 """Get the value of :data:`sys.stderr` (a file-like object).""" | |
1336 return sys.stderr | |
1337 | |
1338 | |
1339 class FormatStringParser(object): | |
1340 | |
1341 """ | |
1342 Shallow logging format string parser. | |
1343 | |
1344 This class enables introspection and manipulation of logging format strings | |
1345 in the three styles supported by the :mod:`logging` module starting from | |
1346 Python 3.2 (``%``, ``{`` and ``$``). | |
1347 """ | |
1348 | |
1349 def __init__(self, style=DEFAULT_FORMAT_STYLE): | |
1350 """ | |
1351 Initialize a :class:`FormatStringParser` object. | |
1352 | |
1353 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to | |
1354 :data:`DEFAULT_FORMAT_STYLE`). | |
1355 :raises: Refer to :func:`check_style()`. | |
1356 """ | |
1357 self.style = check_style(style) | |
1358 self.capturing_pattern = FORMAT_STYLE_PATTERNS[style] | |
1359 # Remove the capture group around the mapping key / field name. | |
1360 self.raw_pattern = self.capturing_pattern.replace(r'(\w+)', r'\w+') | |
1361 # After removing the inner capture group we add an outer capture group | |
1362 # to make the pattern suitable for simple tokenization using re.split(). | |
1363 self.tokenize_pattern = re.compile('(%s)' % self.raw_pattern, re.VERBOSE) | |
1364 # Compile a regular expression for finding field names. | |
1365 self.name_pattern = re.compile(self.capturing_pattern, re.VERBOSE) | |
1366 | |
1367 def contains_field(self, format_string, field_name): | |
1368 """ | |
1369 Get the field names referenced by a format string. | |
1370 | |
1371 :param format_string: The logging format string. | |
1372 :returns: A list of strings with field names. | |
1373 """ | |
1374 return field_name in self.get_field_names(format_string) | |
1375 | |
1376 def get_field_names(self, format_string): | |
1377 """ | |
1378 Get the field names referenced by a format string. | |
1379 | |
1380 :param format_string: The logging format string. | |
1381 :returns: A list of strings with field names. | |
1382 """ | |
1383 return self.name_pattern.findall(format_string) | |
1384 | |
1385 def get_grouped_pairs(self, format_string): | |
1386 """ | |
1387 Group the results of :func:`get_pairs()` separated by whitespace. | |
1388 | |
1389 :param format_string: The logging format string. | |
1390 :returns: A list of lists of :class:`FormatStringToken` objects. | |
1391 """ | |
1392 # Step 1: Split simple tokens (without a name) into | |
1393 # their whitespace parts and non-whitespace parts. | |
1394 separated = [] | |
1395 pattern = re.compile(r'(\s+)') | |
1396 for token in self.get_pairs(format_string): | |
1397 if token.name: | |
1398 separated.append(token) | |
1399 else: | |
1400 separated.extend( | |
1401 FormatStringToken(name=None, text=text) | |
1402 for text in pattern.split(token.text) if text | |
1403 ) | |
1404 # Step 2: Group tokens together based on whitespace. | |
1405 current_group = [] | |
1406 grouped_pairs = [] | |
1407 for token in separated: | |
1408 if token.text.isspace(): | |
1409 if current_group: | |
1410 grouped_pairs.append(current_group) | |
1411 grouped_pairs.append([token]) | |
1412 current_group = [] | |
1413 else: | |
1414 current_group.append(token) | |
1415 if current_group: | |
1416 grouped_pairs.append(current_group) | |
1417 return grouped_pairs | |
1418 | |
1419 def get_pairs(self, format_string): | |
1420 """ | |
1421 Tokenize a logging format string and extract field names from tokens. | |
1422 | |
1423 :param format_string: The logging format string. | |
1424 :returns: A generator of :class:`FormatStringToken` objects. | |
1425 """ | |
1426 for token in self.get_tokens(format_string): | |
1427 match = self.name_pattern.search(token) | |
1428 name = match.group(1) if match else None | |
1429 yield FormatStringToken(name=name, text=token) | |
1430 | |
1431 def get_pattern(self, field_name): | |
1432 """ | |
1433 Get a regular expression to match a formatting directive that references the given field name. | |
1434 | |
1435 :param field_name: The name of the field to match (a string). | |
1436 :returns: A compiled regular expression object. | |
1437 """ | |
1438 return re.compile(self.raw_pattern.replace(r'\w+', field_name), re.VERBOSE) | |
1439 | |
1440 def get_tokens(self, format_string): | |
1441 """ | |
1442 Tokenize a logging format string. | |
1443 | |
1444 :param format_string: The logging format string. | |
1445 :returns: A list of strings with formatting directives separated from surrounding text. | |
1446 """ | |
1447 return [t for t in self.tokenize_pattern.split(format_string) if t] | |
1448 | |
1449 | |
1450 class FormatStringToken(collections.namedtuple('FormatStringToken', 'text, name')): | |
1451 | |
1452 """ | |
1453 A named tuple for the results of :func:`FormatStringParser.get_pairs()`. | |
1454 | |
1455 .. attribute:: name | |
1456 | |
1457 The field name referenced in `text` (a string). If `text` doesn't | |
1458 contain a formatting directive this will be :data:`None`. | |
1459 | |
1460 .. attribute:: text | |
1461 | |
1462 The text extracted from the logging format string (a string). | |
1463 """ | |
1464 | |
1465 | |
1466 class NameNormalizer(object): | |
1467 | |
1468 """Responsible for normalizing field and level names.""" | |
1469 | |
1470 def __init__(self): | |
1471 """Initialize a :class:`NameNormalizer` object.""" | |
1472 self.aliases = {k.lower(): v.lower() for k, v in find_level_aliases().items()} | |
1473 | |
1474 def normalize_name(self, name): | |
1475 """ | |
1476 Normalize a field or level name. | |
1477 | |
1478 :param name: The field or level name (a string). | |
1479 :returns: The normalized name (a string). | |
1480 | |
1481 Transforms all strings to lowercase and resolves level name aliases | |
1482 (refer to :func:`find_level_aliases()`) to their canonical name: | |
1483 | |
1484 >>> from coloredlogs import NameNormalizer | |
1485 >>> from humanfriendly import format_table | |
1486 >>> nn = NameNormalizer() | |
1487 >>> sample_names = ['DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL', 'CRITICAL'] | |
1488 >>> print(format_table([(n, nn.normalize_name(n)) for n in sample_names])) | |
1489 ----------------------- | |
1490 | DEBUG | debug | | |
1491 | INFO | info | | |
1492 | WARN | warning | | |
1493 | WARNING | warning | | |
1494 | ERROR | error | | |
1495 | FATAL | critical | | |
1496 | CRITICAL | critical | | |
1497 ----------------------- | |
1498 """ | |
1499 name = name.lower() | |
1500 if name in self.aliases: | |
1501 name = self.aliases[name] | |
1502 return name | |
1503 | |
1504 def normalize_keys(self, value): | |
1505 """ | |
1506 Normalize the keys of a dictionary using :func:`normalize_name()`. | |
1507 | |
1508 :param value: The dictionary to normalize. | |
1509 :returns: A dictionary with normalized keys. | |
1510 """ | |
1511 return {self.normalize_name(k): v for k, v in value.items()} | |
1512 | |
1513 def get(self, normalized_dict, name): | |
1514 """ | |
1515 Get a value from a dictionary after normalizing the key. | |
1516 | |
1517 :param normalized_dict: A dictionary produced by :func:`normalize_keys()`. | |
1518 :param name: A key to normalize and get from the dictionary. | |
1519 :returns: The value of the normalized key (if any). | |
1520 """ | |
1521 return normalized_dict.get(self.normalize_name(name)) |