-
-
Save vsajip/758430 to your computer and use it in GitHub Desktop.
| # | |
| # Copyright (C) 2010-2012 Vinay Sajip. All rights reserved. Licensed under the new BSD license. | |
| # | |
| import ctypes | |
| import logging | |
| import os | |
| class ColorizingStreamHandler(logging.StreamHandler): | |
| # color names to indices | |
| color_map = { | |
| 'black': 0, | |
| 'red': 1, | |
| 'green': 2, | |
| 'yellow': 3, | |
| 'blue': 4, | |
| 'magenta': 5, | |
| 'cyan': 6, | |
| 'white': 7, | |
| } | |
| #levels to (background, foreground, bold/intense) | |
| if os.name == 'nt': | |
| level_map = { | |
| logging.DEBUG: (None, 'blue', True), | |
| logging.INFO: (None, 'white', False), | |
| logging.WARNING: (None, 'yellow', True), | |
| logging.ERROR: (None, 'red', True), | |
| logging.CRITICAL: ('red', 'white', True), | |
| } | |
| else: | |
| level_map = { | |
| logging.DEBUG: (None, 'blue', False), | |
| logging.INFO: (None, 'black', False), | |
| logging.WARNING: (None, 'yellow', False), | |
| logging.ERROR: (None, 'red', False), | |
| logging.CRITICAL: ('red', 'white', True), | |
| } | |
| csi = '\x1b[' | |
| reset = '\x1b[0m' | |
| @property | |
| def is_tty(self): | |
| isatty = getattr(self.stream, 'isatty', None) | |
| return isatty and isatty() | |
| def emit(self, record): | |
| try: | |
| message = self.format(record) | |
| stream = self.stream | |
| if not self.is_tty: | |
| stream.write(message) | |
| else: | |
| self.output_colorized(message) | |
| stream.write(getattr(self, 'terminator', '\n')) | |
| self.flush() | |
| except (KeyboardInterrupt, SystemExit): | |
| raise | |
| except: | |
| self.handleError(record) | |
| if os.name != 'nt': | |
| def output_colorized(self, message): | |
| self.stream.write(message) | |
| else: | |
| import re | |
| ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m') | |
| nt_color_map = { | |
| 0: 0x00, # black | |
| 1: 0x04, # red | |
| 2: 0x02, # green | |
| 3: 0x06, # yellow | |
| 4: 0x01, # blue | |
| 5: 0x05, # magenta | |
| 6: 0x03, # cyan | |
| 7: 0x07, # white | |
| } | |
| def output_colorized(self, message): | |
| parts = self.ansi_esc.split(message) | |
| write = self.stream.write | |
| h = None | |
| fd = getattr(self.stream, 'fileno', None) | |
| if fd is not None: | |
| fd = fd() | |
| if fd in (1, 2): # stdout or stderr | |
| h = ctypes.windll.kernel32.GetStdHandle(-10 - fd) | |
| while parts: | |
| text = parts.pop(0) | |
| if text: | |
| write(text) | |
| if parts: | |
| params = parts.pop(0) | |
| if h is not None: | |
| params = [int(p) for p in params.split(';')] | |
| color = 0 | |
| for p in params: | |
| if 40 <= p <= 47: | |
| color |= self.nt_color_map[p - 40] << 4 | |
| elif 30 <= p <= 37: | |
| color |= self.nt_color_map[p - 30] | |
| elif p == 1: | |
| color |= 0x08 # foreground intensity on | |
| elif p == 0: # reset to default color | |
| color = 0x07 | |
| else: | |
| pass # error condition ignored | |
| ctypes.windll.kernel32.SetConsoleTextAttribute(h, color) | |
| def colorize(self, message, record): | |
| if record.levelno in self.level_map: | |
| bg, fg, bold = self.level_map[record.levelno] | |
| params = [] | |
| if bg in self.color_map: | |
| params.append(str(self.color_map[bg] + 40)) | |
| if fg in self.color_map: | |
| params.append(str(self.color_map[fg] + 30)) | |
| if bold: | |
| params.append('1') | |
| if params: | |
| message = ''.join((self.csi, ';'.join(params), | |
| 'm', message, self.reset)) | |
| return message | |
| def format(self, record): | |
| message = logging.StreamHandler.format(self, record) | |
| if self.is_tty: | |
| # Don't colorize any traceback | |
| parts = message.split('\n', 1) | |
| parts[0] = self.colorize(parts[0], record) | |
| message = '\n'.join(parts) | |
| return message | |
| def main(): | |
| root = logging.getLogger() | |
| root.setLevel(logging.DEBUG) | |
| root.addHandler(ColorizingStreamHandler()) | |
| logging.debug('DEBUG') | |
| logging.info('INFO') | |
| logging.warning('WARNING') | |
| logging.error('ERROR') | |
| logging.critical('CRITICAL') | |
| if __name__ == '__main__': | |
| main() |
this also seems to only require ctypes fi you're using NT
@unux: Good point. Gist updated. Thanks.
With python 2.7, this didn't work for me on windows 64bit without "import ctypes" at the top :. Either way, thanks :D
I was looking for this! Thanks for sharing. I'm using your code in a github project I'm developing. Is it enough to keep the copyright notice at the top of the file? Module is here.
Done. Thanks.
Providing an __init__() will facilitate the customization when using dictConfig()
def __init__(self, level_map=None, *args, **kwargs):
if level_map is not None:
self.level_map = level_map
logging.StreamHandler.__init__(self, *args, **kwargs)
According to the code, the logging.INFO level seems to be the terminal default, if you comment it or set all params to None you do not have to make a special case for nt:
--- ansistrm.py.orig 2012-10-24 15:52:45.051056546 +0200
+++ ansistrm.py 2012-10-24 15:57:06.599067746 +0200
@@ -19,25 +19,21 @@
}
#levels to (background, foreground, bold/intense)
- if os.name == 'nt':
- level_map = {
- logging.DEBUG: (None, 'blue', True),
- logging.INFO: (None, 'white', False),
- logging.WARNING: (None, 'yellow', True),
- logging.ERROR: (None, 'red', True),
- logging.CRITICAL: ('red', 'white', True),
- }
- else:
- level_map = {
- logging.DEBUG: (None, 'blue', False),
- logging.INFO: (None, 'black', False),
- logging.WARNING: (None, 'yellow', False),
- logging.ERROR: (None, 'red', False),
- logging.CRITICAL: ('red', 'white', True),
- }
+ level_map = {
+ logging.DEBUG: (None, 'blue', False),
+ logging.INFO: (None, None, False),
+ logging.WARNING: (None, 'yellow', False),
+ logging.ERROR: (None, 'red', False),
+ logging.CRITICAL: ('red', 'white', True),
+ }
csi = '\x1b['
reset = '\x1b[0m'
+ def __init__(self, level_map=None, *args, **kwargs):
+ if level_map is not None:
+ self.level_map = level_map
+ logging.StreamHandler.__init__(self, *args, **kwargs)
+
@property
def is_tty(self):
isatty = getattr(self.stream, 'isatty', None)Thanks for the gist, well done!
On my terminal (gnome 3.4.1.1) white you specified in the color_map is 'normal white' but not the one the terminal actually uses ('bright white'), so I had to change in the color_map to 'white':9 instead of original 7. Value 9 should mean the default text color.
After switching from win7 64bit to win8 64bit the following ctypes declaration was needed:
import ctypes
ctypes.windll.kernel32.SetConsoleTextAttribute.argtypes = [ctypes.c_ulong, ctypes.c_ushort]
for python3 support:
import sys
if os.name != 'nt':
def output_colorized(self, message):
if sys.version[0] == '2':
self.stream.write(message)
else:
self.stream.write(message.decode())For Win10 color support, I found a flush was needed:
diff --git a/ansistrm.py b/ansistrm.py
index a8ef384..e681aeb 100644
--- a/ansistrm.py
+++ b/ansistrm.py
@@ -89,6 +89,7 @@ class ColorizingStreamHandler(logging.StreamHandler):
text = parts.pop(0)
if text:
write(text)
+ self.stream.flush()
if parts:
params = parts.pop(0)
if h is not None:Seems like info doesnt print on python3
Gist updated to use non-capturing groups. Thanks to Marius Gedminas for the suggestion.