3 # Thomas Nagy, 2005-2018 (ita)
6 logging, colors, terminal width and pretty-print
9 import os
, re
, traceback
, sys
10 from waflib
import Utils
, ansiterm
12 if not os
.environ
.get('NOSYNC', False):
13 # synchronized output is nearly mandatory to prevent garbled output
14 if sys
.stdout
.isatty() and id(sys
.stdout
) == id(sys
.__stdout
__):
15 sys
.stdout
= ansiterm
.AnsiTerm(sys
.stdout
)
16 if sys
.stderr
.isatty() and id(sys
.stderr
) == id(sys
.__stderr
__):
17 sys
.stderr
= ansiterm
.AnsiTerm(sys
.stderr
)
19 # import the logging module after since it holds a reference on sys.stderr
20 # in case someone uses the root logger
23 LOG_FORMAT
= os
.environ
.get('WAF_LOG_FORMAT', '%(asctime)s %(c1)s%(zone)s%(c2)s %(message)s')
24 HOUR_FORMAT
= os
.environ
.get('WAF_HOUR_FORMAT', '%H:%M:%S')
28 See :py:class:`waflib.Logs.log_filter`
33 Global verbosity level, see :py:func:`waflib.Logs.debug` and :py:func:`waflib.Logs.error`
43 'BLUE' :'\x1b[01;34m',
47 'cursor_on' :'\x1b[?25h',
48 'cursor_off' :'\x1b[?25l',
51 indicator
= '\r\x1b[K%s%s%s'
58 def enable_colors(use
):
60 If *1* is given, then the system will perform a few verifications
61 before enabling colors, such as checking whether the interpreter
62 is running in a terminal. A value of zero will disable colors,
63 and a value above *1* will force colors.
65 :param use: whether to enable colors or not
69 if not (sys
.stderr
.isatty() or sys
.stdout
.isatty()):
71 if Utils
.is_win32
and os
.name
!= 'java':
72 term
= os
.environ
.get('TERM', '') # has ansiterm
74 term
= os
.environ
.get('TERM', 'dumb')
76 if term
in ('dumb', 'emacs'):
80 os
.environ
['TERM'] = 'vt100'
82 colors_lst
['USE'] = use
84 # If console packages are available, replace the dummy function with a real
87 get_term_cols
= ansiterm
.get_term_cols
88 except AttributeError:
92 get_term_cols
.__doc
__ = """
93 Returns the console width in characters.
95 :return: the number of characters per line
101 Returns the ansi sequence corresponding to the given color name.
102 An empty string is returned when coloring is globally disabled.
104 :param cl: color name in capital letters
107 if colors_lst
['USE']:
108 return colors_lst
.get(cl
, '')
111 class color_dict(object):
112 """attribute-based color access, eg: colors.PINK"""
113 def __getattr__(self
, a
):
115 def __call__(self
, a
):
118 colors
= color_dict()
120 re_log
= re
.compile(r
'(\w+): (.*)', re
.M
)
121 class log_filter(logging
.Filter
):
123 Waf logs are of the form 'name: message', and can be filtered by 'waf --zones=name'.
124 For example, the following::
126 from waflib import Logs
127 Logs.debug('test: here is a message')
129 Will be displayed only when executing::
133 def __init__(self
, name
=''):
134 logging
.Filter
.__init
__(self
, name
)
136 def filter(self
, rec
):
138 Filters log records by zone and by logging level
140 :param rec: log entry
142 rec
.zone
= rec
.module
143 if rec
.levelno
>= logging
.INFO
:
146 m
= re_log
.match(rec
.msg
)
148 rec
.zone
= m
.group(1)
152 return getattr(rec
, 'zone', '') in zones
or '*' in zones
153 elif not verbose
> 2:
157 class log_handler(logging
.StreamHandler
):
158 """Dispatches messages to stderr/stdout depending on the severity level"""
159 def emit(self
, record
):
161 Delegates the functionality to :py:meth:`waflib.Log.log_handler.emit_override`
163 # default implementation
166 self
.stream
= record
.stream
167 except AttributeError:
168 if record
.levelno
>= logging
.WARNING
:
169 record
.stream
= self
.stream
= sys
.stderr
171 record
.stream
= self
.stream
= sys
.stdout
172 self
.emit_override(record
)
174 except (KeyboardInterrupt, SystemExit):
176 except: # from the python library -_-
177 self
.handleError(record
)
179 def emit_override(self
, record
, **kw
):
181 Writes the log record to the desired stream (stderr/stdout)
183 self
.terminator
= getattr(record
, 'terminator', '\n')
187 msg
= self
.formatter
.format(record
)
188 fs
= '%s' + self
.terminator
190 if (isinstance(msg
, unicode) and getattr(stream
, 'encoding', None)):
191 fs
= fs
.decode(stream
.encoding
)
193 stream
.write(fs
% msg
)
194 except UnicodeEncodeError:
195 stream
.write((fs
% msg
).encode(stream
.encoding
))
197 stream
.write(fs
% msg
)
199 stream
.write((fs
% msg
).encode('utf-8'))
201 logging
.StreamHandler
.emit(self
, record
)
203 class formatter(logging
.Formatter
):
204 """Simple log formatter which handles colors"""
206 logging
.Formatter
.__init
__(self
, LOG_FORMAT
, HOUR_FORMAT
)
208 def format(self
, rec
):
210 Formats records and adds colors as needed. The records do not get
211 a leading hour format if the logging level is above *INFO*.
214 msg
= rec
.msg
.decode('utf-8')
218 use
= colors_lst
['USE']
219 if (use
== 1 and rec
.stream
.isatty()) or use
== 2:
221 c1
= getattr(rec
, 'c1', None)
224 if rec
.levelno
>= logging
.ERROR
:
226 elif rec
.levelno
>= logging
.WARNING
:
228 elif rec
.levelno
>= logging
.INFO
:
230 c2
= getattr(rec
, 'c2', colors
.NORMAL
)
231 msg
= '%s%s%s' % (c1
, msg
, c2
)
233 # remove single \r that make long lines in text files
234 # and other terminal commands
235 msg
= re
.sub(r
'\r(?!\n)|\x1B\[(K|.*?(m|h|l))', '', msg
)
237 if rec
.levelno
>= logging
.INFO
:
238 # the goal of this is to format without the leading "Logs, hour" prefix
241 return msg
% rec
.args
242 except UnicodeDecodeError:
243 return msg
.encode('utf-8') % rec
.args
248 rec
.c2
= colors
.NORMAL
249 return logging
.Formatter
.format(self
, rec
)
252 """global logger for Logs.debug, Logs.error, etc"""
256 Wraps logging.debug and discards messages if the verbosity level :py:attr:`waflib.Logs.verbose` ≤ 0
260 k
[0] = k
[0].replace('\n', ' ')
265 Wrap logging.errors, adds the stack trace when the verbosity level :py:attr:`waflib.Logs.verbose` ≥ 2
269 st
= traceback
.extract_stack()
273 for filename
, lineno
, name
, line
in st
:
274 buf
.append(' File %r, line %d, in %s' % (filename
, lineno
, name
))
276 buf
.append(' %s' % line
.strip())
278 log
.error('\n'.join(buf
))
282 Wraps logging.warning
284 log
.warning(*k
, **kw
)
294 Initializes the logger :py:attr:`waflib.Logs.log`
297 log
= logging
.getLogger('waflib')
301 hdlr
.setFormatter(formatter())
303 log
.addFilter(log_filter())
304 log
.setLevel(logging
.DEBUG
)
306 def make_logger(path
, name
):
308 Creates a simple logger, which is often used to redirect the context command output::
310 from waflib import Logs
311 bld.logger = Logs.make_logger('test.log', 'build')
312 bld.check(header_name='sadlib.h', features='cxx cprogram', mandatory=False)
314 # have the file closed immediately
315 Logs.free_logger(bld.logger)
320 The method finalize() of the command will try to free the logger, if any
322 :param path: file name to write the log output to
324 :param name: logger name (loggers are reused)
327 logger
= logging
.getLogger(name
)
328 if sys
.hexversion
> 0x3000000:
329 encoding
= sys
.stdout
.encoding
332 hdlr
= logging
.FileHandler(path
, 'w', encoding
=encoding
)
333 formatter
= logging
.Formatter('%(message)s')
334 hdlr
.setFormatter(formatter
)
335 logger
.addHandler(hdlr
)
336 logger
.setLevel(logging
.DEBUG
)
339 def make_mem_logger(name
, to_log
, size
=8192):
341 Creates a memory logger to avoid writing concurrently to the main logger
343 from logging
.handlers
import MemoryHandler
344 logger
= logging
.getLogger(name
)
345 hdlr
= MemoryHandler(size
, target
=to_log
)
346 formatter
= logging
.Formatter('%(message)s')
347 hdlr
.setFormatter(formatter
)
348 logger
.addHandler(hdlr
)
349 logger
.memhandler
= hdlr
350 logger
.setLevel(logging
.DEBUG
)
353 def free_logger(logger
):
355 Frees the resources held by the loggers created through make_logger or make_mem_logger.
356 This is used for file cleanup and for handler removal (logger objects are re-used).
359 for x
in logger
.handlers
:
361 logger
.removeHandler(x
)
365 def pprint(col
, msg
, label
='', sep
='\n'):
367 Prints messages in color immediately on stderr::
369 from waflib import Logs
370 Logs.pprint('RED', 'Something bad just happened')
372 :param col: color name to use in :py:const:`Logs.colors_lst`
374 :param msg: message to display
375 :type msg: string or a value that can be printed by %s
376 :param label: a message to add after the colored output
378 :param sep: a string to append at the end (line separator)
381 info('%s%s%s %s', colors(col
), msg
, colors
.NORMAL
, label
, extra
={'terminator':sep
})