out 0.79__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- out/__init__.py +253 -0
- out/demos.py +76 -0
- out/detection.py +20 -0
- out/format.py +249 -0
- out/highlight.py +92 -0
- out/test_mod.py +14 -0
- out/themes.py +264 -0
- out-0.79.dist-info/LICENSE +165 -0
- out-0.79.dist-info/METADATA +544 -0
- out-0.79.dist-info/RECORD +12 -0
- out-0.79.dist-info/WHEEL +5 -0
- out-0.79.dist-info/top_level.txt +1 -0
out/__init__.py
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
'''
|
2
|
+
out - Simple logging with a few fun features.
|
3
|
+
© 2018-25, Mike Miller - Released under the LGPL, version 3+.
|
4
|
+
'''
|
5
|
+
import os
|
6
|
+
import sys
|
7
|
+
import logging
|
8
|
+
import traceback
|
9
|
+
|
10
|
+
from .detection import _find_palettes, is_fbterm
|
11
|
+
|
12
|
+
|
13
|
+
# detect environment before loading formatters and themes
|
14
|
+
_out_file = sys.stderr
|
15
|
+
fg, bg, fx, _level, _is_a_tty = _find_palettes(_out_file)
|
16
|
+
|
17
|
+
|
18
|
+
# now we're ready to import these:
|
19
|
+
from .format import (ColorFormatter as _ColorFormatter,
|
20
|
+
JSONFormatter as _JSONFormatter)
|
21
|
+
from .themes import (render_themes as _render_themes,
|
22
|
+
render_styles as _render_styles,
|
23
|
+
icons as _icons)
|
24
|
+
|
25
|
+
__version__ = '0.79'
|
26
|
+
|
27
|
+
# Allow string as well as constant access. Levels will be added below:
|
28
|
+
level_map = {
|
29
|
+
'debug': logging.DEBUG,
|
30
|
+
'info': logging.INFO,
|
31
|
+
'warn': logging.WARN,
|
32
|
+
'warning': logging.WARN,
|
33
|
+
'err': logging.ERROR,
|
34
|
+
'error': logging.ERROR,
|
35
|
+
'critical': logging.FATAL,
|
36
|
+
'fatal': logging.FATAL,
|
37
|
+
}
|
38
|
+
|
39
|
+
|
40
|
+
class Logger(logging.Logger):
|
41
|
+
'''
|
42
|
+
A singleton logger with centralized configuration.
|
43
|
+
'''
|
44
|
+
default_level = logging.INFO
|
45
|
+
__path__ = __path__ # allows python3 -m out.demos to work
|
46
|
+
__version__ = __version__
|
47
|
+
__name__ = __name__
|
48
|
+
__spec__ = None # needed to make -m happy in recent pythons :-/
|
49
|
+
|
50
|
+
def configure(self, **kwargs):
|
51
|
+
''' Convenience function to set a number of parameters on this logger
|
52
|
+
and associated handlers and formatters.
|
53
|
+
'''
|
54
|
+
for kwarg in kwargs:
|
55
|
+
value = kwargs[kwarg]
|
56
|
+
|
57
|
+
if kwarg == 'level':
|
58
|
+
self.set_level(value)
|
59
|
+
|
60
|
+
elif kwarg == 'default_level':
|
61
|
+
self.default_level = level_map.get(value, value)
|
62
|
+
|
63
|
+
elif kwarg == 'datefmt':
|
64
|
+
self.handlers[0].formatter.datefmt = value
|
65
|
+
|
66
|
+
elif kwarg == 'msgfmt':
|
67
|
+
self.handlers[0].formatter._style._fmt = value
|
68
|
+
|
69
|
+
elif kwarg == 'stream':
|
70
|
+
self.handlers[0].stream = value
|
71
|
+
_, _, _, tlevel, is_a_tty = _find_palettes(value)
|
72
|
+
# probably shouldn't auto configure theme, but it does,
|
73
|
+
# skipping currently
|
74
|
+
_add_handler(value, is_a_tty, tlevel, theme=None)
|
75
|
+
|
76
|
+
elif kwarg == 'theme':
|
77
|
+
if type(value) is str:
|
78
|
+
# this section should be reconciled with _add_handler
|
79
|
+
theme = _render_themes(self.handlers[0].stream)[value]
|
80
|
+
if value == 'plain':
|
81
|
+
fmtr = logging.Formatter(style='{', **theme)
|
82
|
+
elif value == 'json':
|
83
|
+
tlvl = self.handlers[0]._term_level
|
84
|
+
if is_fbterm: hl = False # doesn't work well
|
85
|
+
else: hl = bool(tlvl) # highlighting
|
86
|
+
fmtr = _JSONFormatter(term_level=tlvl, hl=hl, **theme)
|
87
|
+
else:
|
88
|
+
fmtr = _ColorFormatter(**theme)
|
89
|
+
elif type(value) is dict:
|
90
|
+
if 'style' in value or 'icons' in value:
|
91
|
+
fmtr = _ColorFormatter(**theme)
|
92
|
+
else:
|
93
|
+
fmtr = logging.Formatter(style='{', **theme)
|
94
|
+
self.handlers[0].setFormatter(fmtr)
|
95
|
+
|
96
|
+
elif kwarg == 'highlight':
|
97
|
+
value = bool(value)
|
98
|
+
if value is False: # True value is a highlighter
|
99
|
+
self.handlers[0].formatter._highlight = value
|
100
|
+
|
101
|
+
elif kwarg == 'icons':
|
102
|
+
if type(value) is str:
|
103
|
+
value = _icons[value]
|
104
|
+
self.handlers[0].formatter._theme_icons = value
|
105
|
+
|
106
|
+
elif kwarg == 'style':
|
107
|
+
if type(value) is str:
|
108
|
+
value = _render_styles(self.handlers[0].stream)[value]
|
109
|
+
self.handlers[0].formatter._theme_style = value
|
110
|
+
|
111
|
+
elif kwarg == 'lexer':
|
112
|
+
try:
|
113
|
+
self.handlers[0].formatter.set_lexer(value)
|
114
|
+
except AttributeError:
|
115
|
+
self.error('lexer: ColorFormatter not available.')
|
116
|
+
else:
|
117
|
+
raise NameError('unknown keyword argument: %s' % kwarg)
|
118
|
+
|
119
|
+
def log_config(self):
|
120
|
+
''' Log the current logging configuration. '''
|
121
|
+
level = self.level
|
122
|
+
debug = self.debug
|
123
|
+
debug('out logging config, version: %r', __version__)
|
124
|
+
debug(' .name: {}, id: {}', self.name, hex(id(self)))
|
125
|
+
debug(' .level: %s (%s)', level_map_int[level], level)
|
126
|
+
debug(' .propagate: %s', self.propagate)
|
127
|
+
debug(' .default_level: %s (%s)',
|
128
|
+
level_map_int[self.default_level], self.default_level)
|
129
|
+
|
130
|
+
for i, handler in enumerate(self.handlers):
|
131
|
+
fmtr = handler.formatter
|
132
|
+
debug(' + Handler: %s %r', i, handler)
|
133
|
+
debug(' + Formatter: %r', fmtr)
|
134
|
+
debug(' .datefmt: %r', fmtr.datefmt)
|
135
|
+
debug(' .msgfmt: %r', fmtr._fmt)
|
136
|
+
debug(' fmt_style: %s', fmtr._style)
|
137
|
+
debug(' theme.styles: %r', fmtr._theme_style)
|
138
|
+
debug(' theme.icons: %r', fmtr._theme_icons)
|
139
|
+
try:
|
140
|
+
debug(' highlighting: %r, %r',
|
141
|
+
fmtr._lexer.__class__.__name__,
|
142
|
+
fmtr._hl_fmtr.__class__.__name__)
|
143
|
+
except AttributeError:
|
144
|
+
pass
|
145
|
+
|
146
|
+
def setLevel(self, level):
|
147
|
+
if type(level) is int:
|
148
|
+
super().setLevel(level)
|
149
|
+
else:
|
150
|
+
super().setLevel(level_map.get(level.lower(), level))
|
151
|
+
set_level = setLevel
|
152
|
+
|
153
|
+
def __call__(self, message, *args):
|
154
|
+
''' Call logger directly, without function. '''
|
155
|
+
if self.isEnabledFor(self.default_level):
|
156
|
+
self._log(self.default_level, message, args)
|
157
|
+
|
158
|
+
|
159
|
+
def add_logging_level(name, value, method_name=None):
|
160
|
+
''' Comprehensively adds a new logging level to the ``logging`` module and
|
161
|
+
the currently configured logging class.
|
162
|
+
|
163
|
+
Derived from: https://stackoverflow.com/a/35804945/450917
|
164
|
+
'''
|
165
|
+
if not method_name:
|
166
|
+
method_name = name.lower()
|
167
|
+
|
168
|
+
# set levels
|
169
|
+
logging.addLevelName(value, name)
|
170
|
+
setattr(logging, name, value)
|
171
|
+
level_map[name.lower()] = value
|
172
|
+
|
173
|
+
if value == getattr(logging, 'EXCEPT', None): # needs traceback added
|
174
|
+
def log_for_level(self, message='', *args, **kwargs):
|
175
|
+
show = kwargs.pop('show', True)
|
176
|
+
if self.isEnabledFor(value):
|
177
|
+
if show:
|
178
|
+
message = message.lstrip() + ' ▾\n'
|
179
|
+
message += traceback.format_exc()
|
180
|
+
else:
|
181
|
+
message = message.lstrip()
|
182
|
+
self._log(value, message, args, stacklevel=2, **kwargs)
|
183
|
+
else:
|
184
|
+
def log_for_level(self, message, *args, **kwargs):
|
185
|
+
if self.isEnabledFor(value):
|
186
|
+
self._log(value, message, args, stacklevel=2, **kwargs)
|
187
|
+
|
188
|
+
#~ def logToRoot(message, *args, **kwargs): # may not need
|
189
|
+
#~ logging.log(value, message, *args, **kwargs)
|
190
|
+
|
191
|
+
# set functions
|
192
|
+
setattr(logging.getLoggerClass(), method_name, log_for_level)
|
193
|
+
#~ setattr(logging, method_name, logToRoot)
|
194
|
+
|
195
|
+
|
196
|
+
def _add_handler(out_file, is_a_tty, level, theme='auto'):
|
197
|
+
''' Repeatable handler config. '''
|
198
|
+
if is_fbterm:
|
199
|
+
hl = False # doesn't work well
|
200
|
+
else:
|
201
|
+
hl = level > 1 # highlighting > ANSI_MONOCHROME
|
202
|
+
_handler = logging.StreamHandler(stream=out_file)
|
203
|
+
|
204
|
+
if theme == 'auto':
|
205
|
+
_theme_name = 'interactive' if is_a_tty else 'production'
|
206
|
+
if os.environ.get('TERM') in ('linux', 'fbterm'):
|
207
|
+
_theme_name = 'linux_' + _theme_name
|
208
|
+
if os.name == 'nt':
|
209
|
+
_theme_name = 'windows_' + _theme_name
|
210
|
+
theme = _render_themes(out_file, fg=fg, bg=bg, fx=fx)[_theme_name]
|
211
|
+
elif theme is None:
|
212
|
+
try:
|
213
|
+
fmtr = out.handlers[0].formatter
|
214
|
+
theme = dict(
|
215
|
+
icons=fmtr._theme_icons, style=fmtr._theme_style,
|
216
|
+
fmt=fmtr._style._fmt, datefmt=fmtr.datefmt,
|
217
|
+
)
|
218
|
+
except AttributeError:
|
219
|
+
theme = {}
|
220
|
+
|
221
|
+
out.handlers = [] # clear any old
|
222
|
+
_handler._term_level = level
|
223
|
+
_formatter = _ColorFormatter(hl=hl, term_level=level, **theme)
|
224
|
+
_handler.setFormatter(_formatter)
|
225
|
+
out.addHandler(_handler)
|
226
|
+
|
227
|
+
|
228
|
+
# re-configure root logger
|
229
|
+
out = logging.getLogger() # root
|
230
|
+
out.name = 'main'
|
231
|
+
out.__class__ = Logger # one way to add call()
|
232
|
+
|
233
|
+
# odd level numbers chosen to avoid commonly configured variations
|
234
|
+
add_logging_level('TRACE', 7)
|
235
|
+
add_logging_level('NOTE', 27)
|
236
|
+
add_logging_level('EXCEPT', logging.ERROR + 3, 'exception')
|
237
|
+
add_logging_level('EXCEPT', logging.ERROR + 3, 'exc')
|
238
|
+
add_logging_level('FATAL', logging.FATAL)
|
239
|
+
level_map_int = {
|
240
|
+
val: key
|
241
|
+
for key, val in level_map.items()
|
242
|
+
}
|
243
|
+
out.warn = out.warning # fix warn
|
244
|
+
out.set_level('note')
|
245
|
+
|
246
|
+
_add_handler(_out_file, _is_a_tty, _level)
|
247
|
+
|
248
|
+
|
249
|
+
# save original module for later, in case it's needed.
|
250
|
+
out._module = sys.modules[__name__]
|
251
|
+
|
252
|
+
# Wrap module with instance for direct access
|
253
|
+
sys.modules[__name__] = out
|
out/demos.py
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
'''
|
2
|
+
out - Simple logging with a few fun features.
|
3
|
+
© 2018-25, Mike Miller - Released under the LGPL, version 3+.
|
4
|
+
'''
|
5
|
+
import sys
|
6
|
+
import out
|
7
|
+
|
8
|
+
|
9
|
+
def test_levels(full=True):
|
10
|
+
|
11
|
+
out('no explicit level, should be info.')
|
12
|
+
out.trace('trace msg: %s', 'Absurdly voluminous details…')
|
13
|
+
out.debug('debug message')
|
14
|
+
out.info('info message - Normal feedback')
|
15
|
+
out.note('note message - Important positive feedback to remember.')
|
16
|
+
out.warn('warn message - Something to worry about.')
|
17
|
+
|
18
|
+
if full:
|
19
|
+
out.critical('critical message - *flatline*')
|
20
|
+
out.fatal('fatal message - *flatline*')
|
21
|
+
try:
|
22
|
+
1/0
|
23
|
+
except Exception:
|
24
|
+
out.error('error message - Pow!')
|
25
|
+
#~ out.exception('exception message - Kerblooey!')
|
26
|
+
out.exc('exc message - Kerblooey!')
|
27
|
+
|
28
|
+
|
29
|
+
out.warn('begin...')
|
30
|
+
print('⏵⏵ print std config:')
|
31
|
+
out.configure(
|
32
|
+
level='trace', # needs to go before to allow console log to be viewed!
|
33
|
+
)
|
34
|
+
out.log_config()
|
35
|
+
print('---------------------------------')
|
36
|
+
|
37
|
+
|
38
|
+
print('⏵⏵ change to stdout:')
|
39
|
+
out.configure(
|
40
|
+
#~ theme='interactive',
|
41
|
+
stream=sys.stdout, # runs console.detection again
|
42
|
+
)
|
43
|
+
out.log_config()
|
44
|
+
print('---------------------------------')
|
45
|
+
|
46
|
+
|
47
|
+
print('⏵⏵ log messages from module:')
|
48
|
+
from out import test_mod; test_mod # pyflakes
|
49
|
+
print()
|
50
|
+
|
51
|
+
print('⏵⏵ test levels:')
|
52
|
+
test_levels()
|
53
|
+
print('---------------------------------')
|
54
|
+
|
55
|
+
|
56
|
+
print('⏵⏵ test different highlighting, lexers:')
|
57
|
+
out.configure(lexer='json')
|
58
|
+
out.debug('debug message: JSON: %s', '{"data": [null, true, false, "hi", 123]}')
|
59
|
+
|
60
|
+
out.configure(lexer='xml')
|
61
|
+
out.trace('trace message: XML: %s', '<xml><tag attr="woot">text</tag></xml><!-- hi -->')
|
62
|
+
|
63
|
+
out.configure(lexer='python3')
|
64
|
+
out.note('debug message: PyON: %s # hi',
|
65
|
+
{'data': [None, True, False, 'hi', 123]})
|
66
|
+
out.note('import foo; [ x for x in y ]')
|
67
|
+
print('---------------------------------')
|
68
|
+
|
69
|
+
|
70
|
+
print('⏵⏵ test json formatter:')
|
71
|
+
out.configure(
|
72
|
+
level='info',
|
73
|
+
theme='json',
|
74
|
+
)
|
75
|
+
out('no explicit level')
|
76
|
+
out.warn('warn: Heavens to Mergatroyd!')
|
out/detection.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
'''
|
2
|
+
out - Simple logging with a few fun features.
|
3
|
+
© 2018-25, Mike Miller - Released under the LGPL, version 3+.
|
4
|
+
'''
|
5
|
+
from console.constants import TermLevel
|
6
|
+
from console.detection import init, is_a_tty, is_fbterm, os_name
|
7
|
+
from console.style import ForegroundPalette, BackgroundPalette, EffectsPalette
|
8
|
+
|
9
|
+
|
10
|
+
def _find_palettes(stream):
|
11
|
+
''' Need to configure palettes manually, since we are checking stderr. '''
|
12
|
+
level = init(_stream=stream)
|
13
|
+
fg = ForegroundPalette(level=level)
|
14
|
+
bg = BackgroundPalette(level=level)
|
15
|
+
fx = EffectsPalette(level=level)
|
16
|
+
|
17
|
+
return fg, bg, fx, level, is_a_tty(stream)
|
18
|
+
|
19
|
+
|
20
|
+
TermLevel, is_fbterm, os_name # quiet pyflakes
|
out/format.py
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
'''
|
2
|
+
out - Simple logging with a few fun features.
|
3
|
+
© 2018-25, Mike Miller - Released under the LGPL, version 3+.
|
4
|
+
|
5
|
+
Message template variables:
|
6
|
+
|
7
|
+
{name} Name of the logger (logging channel)
|
8
|
+
{levelno} Numeric logging level for the message (DEBUG, INFO,
|
9
|
+
WARNING, ERROR, CRITICAL)
|
10
|
+
{levelname} Text logging level for the message ("DEBUG", "INFO",
|
11
|
+
"WARNING", "ERROR", "CRITICAL")
|
12
|
+
{pathname} Full pathname of the source file where the logging
|
13
|
+
call was issued (if available)
|
14
|
+
{filename} Filename portion of pathname
|
15
|
+
{module} Module (name portion of filename)
|
16
|
+
{lineno)d Source line number where the logging call was issued
|
17
|
+
(if available)
|
18
|
+
{funcName} Function name
|
19
|
+
{created} Time when the LogRecord was created (time.time()
|
20
|
+
return value)
|
21
|
+
{asctime} Textual time when the LogRecord was created
|
22
|
+
{msecs} Millisecond portion of the creation time
|
23
|
+
{relativeCreated} Time in milliseconds when the LogRecord was created,
|
24
|
+
relative to the time the logging module was loaded
|
25
|
+
(typically at application startup time)
|
26
|
+
{thread} Thread ID (if available)
|
27
|
+
{threadName} Thread name (if available)
|
28
|
+
{process} Process ID (if available)
|
29
|
+
{message} The result of record.getMessage(), computed just as
|
30
|
+
the record is emitted
|
31
|
+
|
32
|
+
# added:
|
33
|
+
{on}…{off} Toggles level-specific style (colors, etc) support.
|
34
|
+
{icon} Level-specific icon.
|
35
|
+
'''
|
36
|
+
import logging
|
37
|
+
import re
|
38
|
+
from pprint import pformat
|
39
|
+
|
40
|
+
from . import themes
|
41
|
+
from . import fx
|
42
|
+
from . import highlight
|
43
|
+
from .detection import is_fbterm
|
44
|
+
|
45
|
+
DATA_SEARCH_LIMIT = 80
|
46
|
+
_end = str(fx.end)
|
47
|
+
if is_fbterm: # fbterm esc seqs conflict with brace formatting :-/
|
48
|
+
_end = _end.replace('}', '}}')
|
49
|
+
|
50
|
+
# compile searches for data message highlighting
|
51
|
+
json_data_search = re.compile(r'\{|\[|"').search
|
52
|
+
xml_data_search = re.compile('<').search
|
53
|
+
pyt_data_search = re.compile(r'''
|
54
|
+
\{ # literal
|
55
|
+
| # or
|
56
|
+
\[ # left bracket
|
57
|
+
| # or...
|
58
|
+
\(
|
59
|
+
|
|
60
|
+
:
|
61
|
+
''', re.VERBOSE).search
|
62
|
+
|
63
|
+
|
64
|
+
class ColorFormatter(logging.Formatter):
|
65
|
+
''' Colors the level-name of a log message according to the level.
|
66
|
+
|
67
|
+
Arguments:
|
68
|
+
|
69
|
+
datefmt - strftime datetime template
|
70
|
+
fmt - log template
|
71
|
+
icons - dict of level:value for icons
|
72
|
+
style - dict of level:value for terminal style
|
73
|
+
template_style - log template syntax: %, {, $
|
74
|
+
|
75
|
+
# highlighting
|
76
|
+
hl - bool, highlight the message.
|
77
|
+
lexer - None, or Pygment's lexer: python3', 'json', etc.
|
78
|
+
hl_formatter - None, or pass a configured Pygments formatter.
|
79
|
+
code_indent - If highlighting data with newlines, indent N sp.
|
80
|
+
'''
|
81
|
+
default_msec_format = '%s.%03d' # use period decimal point
|
82
|
+
|
83
|
+
def __init__(self,
|
84
|
+
code_indent=12,
|
85
|
+
datefmt=None,
|
86
|
+
fmt=None,
|
87
|
+
hl=True,
|
88
|
+
hl_formatter=None,
|
89
|
+
term_level=None,
|
90
|
+
icons=None,
|
91
|
+
lexer='python3',
|
92
|
+
style=None,
|
93
|
+
template_style='{',
|
94
|
+
):
|
95
|
+
self._theme_style = (
|
96
|
+
style if style else themes.render_styles(term_level)['norm']
|
97
|
+
)
|
98
|
+
self._theme_icons = icons if icons else themes.icons['rounded']
|
99
|
+
self._code_indent = code_indent
|
100
|
+
self._highlight = self._lexer = None
|
101
|
+
if hl:
|
102
|
+
if lexer:
|
103
|
+
self._highlight = highlight.highlight
|
104
|
+
self.set_lexer(lexer)
|
105
|
+
self._hl_fmtr = hl_formatter or highlight.get_term_formatter(term_level)
|
106
|
+
|
107
|
+
super().__init__(fmt=fmt, datefmt=datefmt, style=template_style)
|
108
|
+
|
109
|
+
def set_lexer(self, name):
|
110
|
+
if highlight.get_lexer_by_name:
|
111
|
+
self._lexer = highlight.get_lexer_by_name(name)
|
112
|
+
self._lexer.ensurenl = False
|
113
|
+
if name == 'xml':
|
114
|
+
self.data_search = xml_data_search
|
115
|
+
elif name == 'json':
|
116
|
+
self.data_search = json_data_search
|
117
|
+
else:
|
118
|
+
self.data_search = pyt_data_search
|
119
|
+
|
120
|
+
def format(self, record):
|
121
|
+
''' Log color formatting. '''
|
122
|
+
levelname = record.levelname # len7 limit
|
123
|
+
if levelname == 'CRITICAL':
|
124
|
+
levelname = record.levelname = 'FATAL'
|
125
|
+
if self.usesTime():
|
126
|
+
record.asctime = self.formatTime(record, self.datefmt)
|
127
|
+
if record.funcName == '<module>':
|
128
|
+
record.funcName = ''
|
129
|
+
|
130
|
+
# render the message part with arguments
|
131
|
+
try: # Allow {} style - need a faster way to determine this:
|
132
|
+
message = record.getMessage()
|
133
|
+
except TypeError:
|
134
|
+
message = record.msg.format(*record.args)
|
135
|
+
|
136
|
+
# decide to highlight w/ pygments
|
137
|
+
# TODO: Highlight args directly and drop text scan? - didn't work well.
|
138
|
+
if self._highlight:
|
139
|
+
if message.find('\x1b', 0, DATA_SEARCH_LIMIT) > -1:
|
140
|
+
pass # found escape, avoid ANSI
|
141
|
+
else:
|
142
|
+
match = self.data_search(message, 0, DATA_SEARCH_LIMIT)
|
143
|
+
if match:
|
144
|
+
pos = match.start()
|
145
|
+
front, back = message[:pos], message[pos:] # Spliten-Sie
|
146
|
+
if front.endswith('\n'): # indent data?
|
147
|
+
back = pformat(record.args)
|
148
|
+
back = left_indent(back, self._code_indent)
|
149
|
+
back = self._highlight(back, self._lexer, self._hl_fmtr)
|
150
|
+
message = f'{front}{back}'
|
151
|
+
|
152
|
+
# style the level, icon
|
153
|
+
record.message = message
|
154
|
+
record.on = self._theme_style.get(levelname, '')
|
155
|
+
record.icon = self._theme_icons.get(levelname, '')
|
156
|
+
record.off = _end
|
157
|
+
s = self.formatMessage(record)
|
158
|
+
|
159
|
+
# this needs to be here, Formatter class not very granular.
|
160
|
+
if record.exc_info:
|
161
|
+
# Cache the traceback text to avoid converting it multiple times
|
162
|
+
# (it's constant anyway)
|
163
|
+
if not record.exc_text:
|
164
|
+
record.exc_text = self.formatException(record.exc_info)
|
165
|
+
if record.exc_text:
|
166
|
+
if s[-1:] != "\n":
|
167
|
+
s = s + "\n"
|
168
|
+
s = s + record.exc_text
|
169
|
+
if record.stack_info:
|
170
|
+
if s[-1:] != "\n":
|
171
|
+
s = s + "\n"
|
172
|
+
s = s + self.formatStack(record.stack_info)
|
173
|
+
return s
|
174
|
+
|
175
|
+
|
176
|
+
class JSONFormatter(logging.Formatter):
|
177
|
+
'''
|
178
|
+
Formats a log message into line-oriented JSON.
|
179
|
+
|
180
|
+
The message template format is different.
|
181
|
+
It uses simple CSV (no spaces allowed) to define field order, e.g.:
|
182
|
+
|
183
|
+
fmt='asctime,msecs,levelname,name,funcName,lineno,message'
|
184
|
+
|
185
|
+
(Currently field order requires Python 3.6, but could be backported.)
|
186
|
+
'''
|
187
|
+
def __init__(self, datefmt=None, fmt=None, term_level=None, hl=True,
|
188
|
+
hl_formatter=None):
|
189
|
+
self._fields = fmt.split(',')
|
190
|
+
from json import dumps
|
191
|
+
self.dumps = dumps
|
192
|
+
self._highlight = None
|
193
|
+
if hl:
|
194
|
+
self._highlight = highlight.highlight
|
195
|
+
if self._highlight:
|
196
|
+
self._lexer = highlight.get_lexer_by_name('JSON')
|
197
|
+
self._hl_formatter = (
|
198
|
+
hl_formatter or highlight.get_term_formatter(term_level)
|
199
|
+
)
|
200
|
+
try:
|
201
|
+
super().__init__(fmt=fmt, datefmt=datefmt)
|
202
|
+
except ValueError: # py 3.8 :-/
|
203
|
+
super().__init__(fmt=fmt, datefmt=datefmt, validate=False)
|
204
|
+
|
205
|
+
def format(self, record):
|
206
|
+
''' Log color formatting. '''
|
207
|
+
levelname = record.levelname
|
208
|
+
if levelname == 'CRITICAL':
|
209
|
+
levelname = record.levelname = 'FATAL'
|
210
|
+
record.asctime = self.formatTime(record, self.datefmt)
|
211
|
+
|
212
|
+
# render the message part with arguments
|
213
|
+
try: # Allow {} style - need a faster way to determine this:
|
214
|
+
message = record.getMessage()
|
215
|
+
except TypeError:
|
216
|
+
message = record.msg.format(*record.args)
|
217
|
+
record.message = message
|
218
|
+
|
219
|
+
fields = self._fields
|
220
|
+
data = { name: getattr(record, name) for name in fields }
|
221
|
+
if 'asctime' in fields and 'msecs' in fields: # needs option for this
|
222
|
+
data['asctime'] += '.{:03.0f}'.format(data.pop('msecs'))
|
223
|
+
s = self.dumps(data)
|
224
|
+
if self._highlight:
|
225
|
+
s = self._highlight(s, self._lexer, self._hl_formatter).rstrip()
|
226
|
+
|
227
|
+
# this needs to be here, Formatter class isn't very extensible.
|
228
|
+
if record.exc_info:
|
229
|
+
# Cache the traceback text to avoid converting it multiple times
|
230
|
+
# (it's constant anyway)
|
231
|
+
if not record.exc_text:
|
232
|
+
record.exc_text = self.formatException(record.exc_info)
|
233
|
+
if record.exc_text:
|
234
|
+
if s[-1:] != "\n":
|
235
|
+
s = s + "\n"
|
236
|
+
s = s + record.exc_text
|
237
|
+
if record.stack_info:
|
238
|
+
if s[-1:] != "\n":
|
239
|
+
s = s + "\n"
|
240
|
+
s = s + self.formatStack(record.stack_info)
|
241
|
+
return s
|
242
|
+
|
243
|
+
|
244
|
+
def left_indent(text, indent=12, end='\n'):
|
245
|
+
''' A bit of the ol' ultraviolence :-/ '''
|
246
|
+
indent = ' ' * indent
|
247
|
+
lines = [indent + line for line in text.splitlines(True)]
|
248
|
+
lines.append(end)
|
249
|
+
return ''.join(lines)
|
out/highlight.py
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
'''
|
2
|
+
out - Simple logging with a few fun features.
|
3
|
+
© 2018-25, Mike Miller - Released under the LGPL, version 3+.
|
4
|
+
|
5
|
+
Highlighting with Pygments!
|
6
|
+
'''
|
7
|
+
try:
|
8
|
+
from pygments import highlight
|
9
|
+
from pygments.lexers import get_lexer_by_name
|
10
|
+
from pygments.token import (Keyword, Name, Comment, String, Error,
|
11
|
+
Number, Operator, Punctuation,
|
12
|
+
Token, Generic, Whitespace)
|
13
|
+
except ImportError:
|
14
|
+
highlight = get_lexer_by_name = None
|
15
|
+
|
16
|
+
from .detection import TermLevel
|
17
|
+
|
18
|
+
|
19
|
+
def get_term_formatter(level):
|
20
|
+
''' Build formatter according to environment. '''
|
21
|
+
|
22
|
+
term_formatter = None
|
23
|
+
if level and highlight:
|
24
|
+
|
25
|
+
if level >= TermLevel.ANSI_EXTENDED:
|
26
|
+
|
27
|
+
from pygments.formatters import Terminal256Formatter
|
28
|
+
from pygments.style import Style
|
29
|
+
|
30
|
+
class OutStyle(Style):
|
31
|
+
styles = {
|
32
|
+
Comment: 'italic ansibrightblack',
|
33
|
+
Keyword: 'bold #4ac', # light blue
|
34
|
+
Keyword.Constant: 'nobold #3aa', # ansicyan
|
35
|
+
Number: 'ansigreen',
|
36
|
+
|
37
|
+
Name.Tag: '#4ac', # light blue, xml, json
|
38
|
+
Name.Attribute: '#4ac', # light blue
|
39
|
+
|
40
|
+
Operator: 'nobold #b94',
|
41
|
+
Operator.Word: 'bold #4ac',
|
42
|
+
Punctuation: 'nobold #b94',
|
43
|
+
|
44
|
+
# not sure about string, hard to find a good warm color
|
45
|
+
Generic.String: 'ansired',
|
46
|
+
String: 'ansibrightmagenta',
|
47
|
+
#~ String: '#b00', # brick red
|
48
|
+
#~ String: '#f80', # bright amber
|
49
|
+
#~ String: '#d80', # med amber
|
50
|
+
}
|
51
|
+
term_formatter = Terminal256Formatter(style=OutStyle)
|
52
|
+
|
53
|
+
elif level >= TermLevel.ANSI_BASIC:
|
54
|
+
|
55
|
+
from pygments.formatters import TerminalFormatter
|
56
|
+
|
57
|
+
_default = ('', '')
|
58
|
+
color_scheme = {
|
59
|
+
Comment.Preproc: _default,
|
60
|
+
Name: _default,
|
61
|
+
Token: _default,
|
62
|
+
Whitespace: _default,
|
63
|
+
Generic.Heading: ('**', '**'),
|
64
|
+
|
65
|
+
Comment: ('brightblack', 'brightblack'),
|
66
|
+
Keyword: ('*brightblue*', '*brightblue*'),
|
67
|
+
Keyword.Constant: ('cyan', 'cyan'),
|
68
|
+
Keyword.Type: ('cyan', 'cyan'),
|
69
|
+
Operator: ('yellow', 'yellow'),
|
70
|
+
Operator.Word: ('*brightblue*', '*brightblue*'),
|
71
|
+
|
72
|
+
Name.Builtin: ('cyan', 'cyan'),
|
73
|
+
Name.Decorator: ('magenta', 'magenta'),
|
74
|
+
Name.Tag: ('brightblue', 'brightblue'),
|
75
|
+
Name.Attribute: ('brightblue', 'brightblue'),
|
76
|
+
|
77
|
+
String: ('brightmagenta', 'brightmagenta'),
|
78
|
+
Number: ('green', 'green'),
|
79
|
+
|
80
|
+
Generic.Deleted: ('red', 'brightred'),
|
81
|
+
Generic.Inserted: ('green', 'brightgreen'),
|
82
|
+
Generic.Error: ('brightred', 'brightred'),
|
83
|
+
|
84
|
+
Error: ('_brightred_', '_brightred_'),
|
85
|
+
}
|
86
|
+
|
87
|
+
term_formatter = TerminalFormatter(
|
88
|
+
bg='dark',
|
89
|
+
colorscheme=color_scheme,
|
90
|
+
)
|
91
|
+
|
92
|
+
return term_formatter
|