├── .circleci └── config.yml ├── .gitignore ├── .screenshots ├── nicelog-150408.png ├── nicelog.png ├── nicelog2.png └── nicelog3.png ├── CHANGELOG.rst ├── MANIFEST.in ├── Makefile ├── README.rst ├── nicelog ├── __init__.py ├── colorers │ ├── __init__.py │ ├── base.py │ └── terminal.py ├── formatters │ └── __init__.py ├── styles │ ├── __init__.py │ └── base.py └── utils.py ├── scripts └── manual_test.py ├── setup.cfg ├── setup.py ├── tests ├── requirements.txt └── test_formatters │ └── test_colorful.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | 6 | version: 2 7 | 8 | workflows: 9 | version: 2 10 | tests: 11 | jobs: 12 | - test-python-3.6 13 | - test-python-3.5 14 | - test-python-2.7 15 | 16 | jobs: 17 | 18 | test-python-3.6: &test-template 19 | 20 | docker: 21 | - image: circleci/python:3.6 22 | 23 | working_directory: ~/repo 24 | 25 | steps: 26 | - checkout 27 | 28 | # Download and cache dependencies 29 | - restore_cache: 30 | keys: 31 | - v1-dependencies-{{ checksum "setup.py" }} 32 | # fallback to using the latest cache if no exact match is found 33 | - v1-dependencies- 34 | 35 | - run: 36 | name: install dependencies 37 | command: | 38 | python -m venv venv || virtualenv venv 39 | . venv/bin/activate 40 | python setup.py install 41 | pip install pytest flake8 freezegun 42 | 43 | - save_cache: 44 | paths: 45 | - ./venv 46 | key: v1-dependencies-{{ checksum "setup.py" }} 47 | 48 | - run: 49 | name: run tests 50 | command: | 51 | . venv/bin/activate 52 | py.test ./tests/ -s -vvv 53 | 54 | - store_artifacts: 55 | path: test-reports 56 | destination: test-reports 57 | 58 | test-python-3.5: 59 | <<: *test-template 60 | docker: 61 | - image: circleci/python:3.5 62 | 63 | test-python-2.7: 64 | <<: *test-template 65 | docker: 66 | - image: circleci/python:2.7 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.egg 3 | *.egg-info 4 | *.pyc 5 | *~ 6 | .cache 7 | .coverage 8 | .tox 9 | /.venv* 10 | /build 11 | /dist 12 | __pycache__ 13 | old-stuff 14 | .pytest_cache 15 | -------------------------------------------------------------------------------- /.screenshots/nicelog-150408.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/nicelog/be25f46144bab67c15bf7aad2fe9c2f713173664/.screenshots/nicelog-150408.png -------------------------------------------------------------------------------- /.screenshots/nicelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/nicelog/be25f46144bab67c15bf7aad2fe9c2f713173664/.screenshots/nicelog.png -------------------------------------------------------------------------------- /.screenshots/nicelog2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/nicelog/be25f46144bab67c15bf7aad2fe9c2f713173664/.screenshots/nicelog2.png -------------------------------------------------------------------------------- /.screenshots/nicelog3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/nicelog/be25f46144bab67c15bf7aad2fe9c2f713173664/.screenshots/nicelog3.png -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.2 5 | ---- 6 | 7 | - More decoupling between "colorer" and "style" 8 | - Support for pretty tracebacks (colorful + code context + locals) 9 | - Added some tests 10 | - Python3 support via six 11 | 12 | 13 | v0.1.9 14 | ------ 15 | 16 | - Replaced ``strftime(3)`` conversion specifiers ``%F`` and ``%T`` 17 | aren't available on all platforms: replaced with long versions 18 | ``%Y-%m-%d`` and ``%H:%M:%S``. 19 | 20 | 21 | v0.1.8 22 | ------ 23 | 24 | - Prevent failure in case the ``TERM`` environment variable is not set (PR #1) 25 | 26 | 27 | v0.1.7 28 | ------ 29 | 30 | - Added support for ``message_inline`` argument. If set to ``False``, 31 | messages will be displayed on their own line (useful when enabling a lot of 32 | information) 33 | 34 | 35 | v0.1.6 36 | ------ 37 | 38 | - Added support for showing more information: 39 | 40 | - record date 41 | - file name / line number 42 | - module / function 43 | 44 | 45 | v0.1.5 46 | ------ 47 | 48 | - Added support for nicer colors in 256-color mode 49 | - Removed dependency from termcolor (now shipping better implementation) 50 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## Standard makefile for Python tests 2 | 3 | BASE_PACKAGE = nicelog 4 | 5 | .PHONY: all upload package install install_dev test docs publish_docs 6 | 7 | all: help 8 | 9 | help: 10 | @echo "AVAILABLE TARGETS" 11 | @echo "----------------------------------------" 12 | @echo "pypi_upload - build source distribution and upload to pypi" 13 | @echo "pypi_register - register proejct on pypi" 14 | @echo "package - build sdist and py2/py3 wheels" 15 | @echo "twine_upload - upload via twine" 16 | @echo 17 | @echo "install - install project in production mode" 18 | @echo "install_dev - install project in development mode" 19 | @echo 20 | @echo "test - run tests" 21 | @echo "setup_tests - install dependencies for tests" 22 | @echo 23 | @echo "docs - build documentation (HTML)" 24 | @echo "publish_docs - publish documentation to GitHub pages" 25 | 26 | pypi_register: 27 | python setup.py register -r https://pypi.python.org/pypi 28 | 29 | pypi_upload: 30 | python setup.py sdist upload -r https://pypi.python.org/pypi 31 | 32 | package: 33 | python3 setup.py sdist bdist_wheel 34 | python2 setup.py bdist_wheel 35 | 36 | clean_package: 37 | rm -f dist/* 38 | 39 | twine_upload: 40 | twine upload dist/* 41 | 42 | install: 43 | python setup.py install 44 | 45 | install_dev: 46 | python setup.py develop 47 | 48 | test: 49 | py.test -vvv --pep8 --cov=$(BASE_PACKAGE) --cov-report=term-missing ./tests 50 | 51 | manual_test: 52 | PYTHONPATH=scripts python -m manual_test 53 | 54 | setup_tests: 55 | pip install -U -r ./tests/requirements.txt 56 | 57 | docs: 58 | $(MAKE) -C docs html 59 | 60 | publish_docs: docs 61 | ghp-import -n -p ./docs/build/html 62 | @echo 63 | @echo "HTML output published on github-pages" 64 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Nice Log 2 | ######## 3 | 4 | .. image:: https://circleci.com/gh/rshk/nicelog.svg?&style=shield 5 | :target: https://circleci.com/gh/rshk/nicelog 6 | :alt: CircleCI build status 7 | 8 | 9 | Provide formatters to nicely display colorful logging output on the console. 10 | 11 | `Fork this project on GitHub `_ 12 | 13 | Right now, it contains only one formatter, coloring log lines 14 | depending on the log level and adding nice line prefixes containing 15 | logger name, but future plans are to add more formatters and allow 16 | better ways to customize them. 17 | 18 | 19 | Installation 20 | ============ 21 | 22 | :: 23 | 24 | pip install nicelog 25 | 26 | 27 | Quick usage 28 | =========== 29 | 30 | Since version ``0.3``, nicelog comes with a helper function to quickly 31 | set up logging for basic needs. 32 | 33 | .. code-block:: python 34 | 35 | from nicelog import setup_logging 36 | 37 | setup_logging() 38 | 39 | Or, if you want to include debug messages too: 40 | 41 | .. code-block:: python 42 | 43 | setup_logging(debug=True) 44 | 45 | 46 | Advanced usage 47 | ============== 48 | 49 | .. code-block:: python 50 | 51 | import logging 52 | import sys 53 | 54 | from nicelog.formatters import Colorful 55 | 56 | # Setup a logger 57 | logger = logging.getLogger('foo') 58 | logger.setLevel(logging.DEBUG) 59 | 60 | # Setup a handler, writing colorful output 61 | # to the console 62 | handler = logging.StreamHandler(sys.stderr) 63 | handler.setFormatter(Colorful()) 64 | handler.setLevel(logging.DEBUG) 65 | logger.addHandler(handler) 66 | 67 | # Now log some messages.. 68 | logger.debug('Debug message') 69 | logger.info('Info message') 70 | logger.warning('Warning message') 71 | logger.error('Error message') 72 | logger.critical('Critical message') 73 | try: 74 | raise ValueError('This is an exception') 75 | except: 76 | logger.exception("An error occurred") 77 | 78 | 79 | Example output 80 | ============== 81 | 82 | Here it is, in all its glory: 83 | 84 | .. image:: .screenshots/nicelog-150408.png 85 | :alt: Screenshot 86 | 87 | 88 | The output format can be further customized, eg. if you want to reduce 89 | colorfulness or verbosity. 90 | 91 | 92 | Integrations 93 | ============ 94 | 95 | Django 96 | ------ 97 | 98 | I usually put something like this in my (local) settings: 99 | 100 | .. code-block:: python 101 | 102 | LOGGING['formatters']['standard'] = { 103 | '()': 'nicelog.formatters.Colorful', 104 | 'show_date': True, 105 | 'show_function': True, 106 | 'show_filename': True, 107 | 'message_inline': False, 108 | } 109 | -------------------------------------------------------------------------------- /nicelog/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | def setup_logging(debug=False): 6 | """Helper function to quickly setup everything needed. 7 | 8 | Configures the logging system to print colorful messages to the 9 | standard error. 10 | 11 | Args: 12 | 13 | debug: 14 | If set to True, will use DEBUG as log level for the "root" 15 | logger. Otherwise, it will default to INFO. 16 | """ 17 | 18 | from nicelog.formatters import Colorful 19 | handler = logging.StreamHandler(sys.stderr) 20 | handler.setFormatter(Colorful( 21 | show_date=True, 22 | show_function=True, 23 | show_filename=True, 24 | message_inline=False)) 25 | handler.setLevel(logging.DEBUG) 26 | 27 | root_logger = logging.getLogger() 28 | root_logger.setLevel(logging.DEBUG if debug else logging.INFO) 29 | root_logger.addHandler(handler) 30 | -------------------------------------------------------------------------------- /nicelog/colorers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/nicelog/be25f46144bab67c15bf7aad2fe9c2f713173664/nicelog/colorers/__init__.py -------------------------------------------------------------------------------- /nicelog/colorers/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import six 6 | 7 | 8 | class BaseColorer(object): 9 | def __init__(self, style): 10 | self.style = style 11 | 12 | def render(self, style, text): 13 | item_style = self._get_style(style) 14 | return self.colorize(text, **item_style) 15 | 16 | def _get_style(self, style_name): 17 | if isinstance(style_name, six.string_types): 18 | style_name = [style_name] 19 | for name in style_name: 20 | try: 21 | return getattr(self.style, name) 22 | except: 23 | pass 24 | return {} 25 | 26 | def colorize(self, text, fg=None, bg=None, attrs=None): 27 | return text 28 | -------------------------------------------------------------------------------- /nicelog/colorers/terminal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import os 6 | 7 | from .base import BaseColorer 8 | 9 | 10 | def get_term_colorer(*a, **kw): 11 | if os.environ.get('ANSI_COLORS_DISABLED') is not None: 12 | return None 13 | term = os.environ.get('TERM') 14 | if term and '256color' in term: 15 | return Xterm256Colorer(*a, **kw) 16 | return Xterm16Colorer(*a, **kw) 17 | 18 | 19 | class Xterm16Colorer(BaseColorer): 20 | _colors = dict( 21 | grey='0', red='1', green='2', yellow='3', 22 | blue='4', magenta='5', cyan='6', white='7', 23 | hi_grey='0', hi_red='1', hi_green='2', hi_yellow='3', 24 | hi_blue='4', hi_magenta='5', hi_cyan='6', hi_white='7') 25 | _reset = '0' 26 | _attrs = { 27 | 'bold': '1', 'dark': '2', 'underline': '4', 'blink': '5', 28 | 'reverse': '7', 'concealed': '8', 29 | } 30 | 31 | def _get_fg_color(self, color): 32 | fg_color = self._colors.get(color) 33 | if fg_color is None: 34 | return 35 | return '3{0}'.format(fg_color) 36 | 37 | def _get_bg_color(self, color): 38 | bg_color = self._colors.get(color) 39 | if bg_color is None: 40 | return 41 | return '4{0}'.format(bg_color) 42 | 43 | def colorize(self, text, fg=None, bg=None, attrs=None): 44 | _attrs = filter(None, (self._attrs.get(x) for x in (attrs or []))) 45 | _open_parts = list(_attrs) 46 | _open_parts.append(self._get_fg_color(fg)) 47 | _open_parts.append(self._get_bg_color(bg)) 48 | _open_parts = [x for x in _open_parts if x] 49 | 50 | if len(_open_parts) == 0: 51 | _open_parts.append(self._reset) 52 | t_open = '\033[{0}m'.format(';'.join(str(x) for x in _open_parts)) 53 | t_close = '\033[{0}m'.format(self._reset) 54 | return u'{0}{1}{2}'.format(t_open, text, t_close) 55 | 56 | 57 | def _256c(num): # color 58 | return str(16 + int(num, 6)) 59 | 60 | 61 | def _256g(num): # gray 62 | return str(231 + num) 63 | 64 | 65 | class Xterm256Colorer(Xterm16Colorer): 66 | 67 | _colors = dict( 68 | grey=_256g(9), 69 | red=_256c('500'), 70 | green=_256c('130'), 71 | yellow=_256c('510'), 72 | blue=_256c('025'), 73 | magenta=_256c('515'), 74 | cyan=_256c('033'), 75 | white=_256g(21), 76 | hi_grey=_256g(14), 77 | hi_red=_256c('511'), 78 | hi_green=_256c('450'), 79 | hi_yellow=_256c('530'), 80 | hi_blue=_256c('135'), 81 | hi_magenta=_256c('503'), 82 | hi_cyan=_256c('244'), 83 | hi_white=_256g(24)) 84 | 85 | def _get_fg_color(self, color): 86 | fg_color = self._colors.get(color) 87 | if fg_color is None: 88 | return 89 | return '38;5;{0}'.format(fg_color) 90 | 91 | def _get_bg_color(self, color): 92 | bg_color = self._colors.get(color) 93 | if bg_color is None: 94 | return 95 | return '48;5;{0}'.format(bg_color) 96 | -------------------------------------------------------------------------------- /nicelog/formatters/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import datetime 6 | import logging 7 | import os 8 | import sys 9 | import six 10 | 11 | from nicelog.colorers.terminal import get_term_colorer 12 | from nicelog.styles.base import BaseStyle as DefaultStyle 13 | 14 | 15 | POWERLINE_STYLE = bool(os.environ.get('HAS_POWERLINE_FONT', False)) 16 | DEFAULT = object() 17 | 18 | 19 | class Colorful(logging.Formatter, object): 20 | 21 | def __init__(self, show_date=True, show_function=True, 22 | show_filename=True, message_inline=False, 23 | beautiful_tracebacks=True, 24 | colorer=DEFAULT, style=DEFAULT, *a, **kw): 25 | 26 | """Log formatter for beautiful colored output 27 | 28 | Args 29 | show_date: whether to include date in log output 30 | show_function: whether to include function name 31 | show_filename: whether to include file name 32 | message_inline: whether to print messages inline 33 | beautiful_tracebacks: whether to nicely format tracebacks 34 | colorer (BaseColorer): used to create color output 35 | style (BaseStyle): color style to use 36 | """ 37 | 38 | super(Colorful, self).__init__(*a, **kw) 39 | if style is DEFAULT: 40 | style = DefaultStyle() 41 | self.style = style 42 | if colorer is DEFAULT: 43 | colorer = get_term_colorer(style=style) 44 | self.colorer = colorer 45 | self._show_date = show_date 46 | self._show_function = show_function 47 | self._show_filename = show_filename 48 | self._message_inline = message_inline 49 | self._beautiful_tracebacks = beautiful_tracebacks 50 | 51 | def format(self, record): 52 | 53 | parts = [] 54 | 55 | parts.append(self._format_level(record)) 56 | 57 | if self._show_date: 58 | parts.append(self._format_date(record)) 59 | 60 | parts.append(self._format_name(record)) 61 | 62 | if self._show_filename: 63 | parts.append(self._format_filename(record)) 64 | 65 | if self._show_function: 66 | parts.append(self._format_function(record)) 67 | 68 | if self._message_inline: 69 | parts.append(self._format_message_inline(record).rstrip()) 70 | else: 71 | parts.append(self._format_message_block(record).rstrip()) 72 | 73 | # todo: beautiful exceptions if required to 74 | exc_info = self._format_traceback(record) 75 | if exc_info is not None: 76 | parts.append("\n\n" + self._indent(exc_info)) 77 | 78 | return ' '.join(parts) 79 | 80 | def _render(self, style_name, text): 81 | if self.colorer is None: 82 | return text 83 | return self.colorer.render(style_name, text) 84 | 85 | def _format_date(self, record): 86 | fmtdate = datetime.datetime.fromtimestamp( 87 | record.created).strftime("%Y-%m-%d %H:%M:%S") 88 | return self._render('date', fmtdate) 89 | 90 | def _format_level_and_name(self, record): 91 | return ''.join((self._format_level(record), self._format_name(record))) 92 | 93 | def _format_level(self, record): 94 | return self._render( 95 | ('level_name_{0}'.format(record.levelname), 'level_name_DEFAULT'), 96 | ' {0:^8} '.format(record.levelname)) 97 | 98 | def _format_name(self, record): 99 | return self._render('logger_name', ' {0} '.format(record.name)) 100 | 101 | def _format_filename(self, record): 102 | return ':'.join(( 103 | self._render('file_name', record.filename), 104 | self._render('line_number', str(record.lineno)), 105 | )) 106 | 107 | def _format_function(self, record): 108 | return '.'.join(( 109 | self._render('module_name', record.module), 110 | self._render('function_name', str(record.funcName)), 111 | )) 112 | 113 | def _format_message_inline(self, record): 114 | return self._render( 115 | ('message_{0}'.format(record.levelname), 'message_DEFAULT'), 116 | record.getMessage().rstrip()) 117 | 118 | def _format_message_block(self, record): 119 | return "\n" + self._indent(record.getMessage()) 120 | 121 | def _format_traceback(self, record): 122 | if record.exc_info: 123 | if self._beautiful_tracebacks: 124 | return self._format_beautiful_traceback(record) 125 | text = self._format_plain_traceback(record) 126 | return self._render('exception', text) 127 | return None 128 | 129 | def _format_plain_traceback(self, record): 130 | if record.exc_info: 131 | # Cache the traceback text to avoid converting it multiple times 132 | # (it's constant anyway) 133 | if not record.exc_text: 134 | record.exc_text = self.formatException(record.exc_info) 135 | if record.exc_text: 136 | try: 137 | return six.u(record.exc_text) 138 | except UnicodeError: 139 | # Sometimes filenames have non-ASCII chars, which can lead 140 | # to errors when s is Unicode and record.exc_text is str 141 | # See issue 8924. 142 | # We also use replace for when there are multiple 143 | # encodings, e.g. UTF-8 for the filesystem and latin-1 144 | # for a script. See issue 13232. 145 | return record.exc_text.decode( 146 | sys.getfilesystemencoding(), 'replace') 147 | return None 148 | 149 | def _format_beautiful_traceback(self, record): 150 | if not record.exc_info: 151 | return 152 | from nicelog.utils import TracebackInfo 153 | # todo: use colorer to render traceback! 154 | # todo: use suitable pygments formatter for the colorer 155 | trace = TracebackInfo.from_tb(record.exc_info[2]).format_color() 156 | trace += "\n" + repr(record.exc_info[1]) 157 | return trace 158 | 159 | def _indent(self, text, tab=" ", level=1): 160 | _indent = tab * level 161 | lines = text.splitlines() 162 | indented = ["{0}{1}".format(_indent, line) for line in lines] 163 | return "\n".join(indented) 164 | 165 | 166 | ColorLineFormatter = Colorful 167 | -------------------------------------------------------------------------------- /nicelog/styles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/nicelog/be25f46144bab67c15bf7aad2fe9c2f713173664/nicelog/styles/__init__.py -------------------------------------------------------------------------------- /nicelog/styles/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division, unicode_literals 4 | 5 | 6 | class BaseStyle(object): 7 | 8 | date = dict(fg='cyan') 9 | 10 | level_name_DEBUG = dict(fg='cyan', attrs=['reverse']) 11 | level_name_INFO = dict(fg='green', attrs=['reverse']) 12 | level_name_WARNING = dict(fg='yellow', attrs=['reverse']) 13 | level_name_ERROR = dict(fg='red', attrs=['reverse']) 14 | level_name_CRITICAL = dict(fg='red', attrs=['reverse']) 15 | level_name_DEFAULT = dict(fg='white', attrs=['reverse']) 16 | 17 | logger_name = dict(fg='red', bg='white') 18 | file_name = dict(fg='green') 19 | line_number = dict(fg='hi_green') 20 | module_name = dict(fg='yellow') 21 | function_name = dict(fg='hi_yellow') 22 | 23 | message_DEBUG = dict(fg='cyan') 24 | message_INFO = dict(fg='green') 25 | message_WARNING = dict(fg='yellow') 26 | message_ERROR = dict(fg='red') 27 | message_CRITICAL = dict(fg='red') 28 | message_DEFAULT = dict(fg='white') 29 | 30 | exception = dict(fg='hi_grey') 31 | -------------------------------------------------------------------------------- /nicelog/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import io 6 | import linecache 7 | import sys 8 | 9 | import six 10 | from six.moves import zip, range 11 | 12 | 13 | def trim_string(s, maxlen=1024, ellps='...'): 14 | """ 15 | Trim a string to a maximum length, adding an "ellipsis" 16 | indicator if the string was trimmed 17 | """ 18 | 19 | # todo: allow cutting in the middle of the string, 20 | # instead of just on the right end..? 21 | 22 | if len(s) > maxlen: 23 | return s[:maxlen - len(ellps)] + ellps 24 | return s 25 | 26 | 27 | class FrameInfo(object): 28 | def __init__(self, filename, lineno, name, line, locs): 29 | self.filename = filename 30 | self.lineno = lineno 31 | self.name = name 32 | self.line = line 33 | self.locs = self._format_locals(locs) 34 | self.context = self._get_context() 35 | 36 | def _get_context(self, size=3): 37 | """Return some "context" lines from a file""" 38 | _start = max(0, self.lineno - size - 1) 39 | _end = self.lineno + size 40 | _lines = linecache.getlines(self.filename)[_start:_end] 41 | _lines = [x.rstrip() for x in _lines] 42 | _lines = list(zip(range(_start + 1, _end + 1), _lines)) 43 | return _lines 44 | 45 | def _format_locals(self, locs): 46 | formatted = {} 47 | for k, v in six.iteritems(locs): 48 | try: 49 | fmtval = trim_string(repr(v), maxlen=1024) 50 | except Exception as e: 51 | fmtval = ''.format(repr(e)) 52 | formatted[k] = fmtval 53 | return formatted 54 | 55 | 56 | class TracebackInfo(object): 57 | """ 58 | Class used to hold information about an error traceback. 59 | 60 | This is meant to be serialized & stored in the database, instead 61 | of a full traceback object, which is *not* serializable. 62 | 63 | It holds information about: 64 | 65 | - the exception that caused the thing to fail 66 | - the stack frames (with file / line number, function and exact code 67 | around the point in which the exception occurred) 68 | - a representation of the local variables for each frame. 69 | 70 | A textual representation of the traceback information may be 71 | retrieved by using ``str()`` or ``unicode()`` on the object 72 | instance. 73 | """ 74 | 75 | def __init__(self): 76 | self.frames = [] 77 | 78 | @classmethod 79 | def from_current_exc(cls): 80 | """ 81 | Instantiate with traceback from ``sys.exc_info()``. 82 | """ 83 | return cls.from_tb(sys.exc_info()[2]) 84 | 85 | @classmethod 86 | def from_tb(cls, tb): 87 | """ 88 | Instantiate from a traceback object. 89 | """ 90 | obj = cls() 91 | obj.frames = cls._extract_tb(tb) 92 | return obj 93 | 94 | def format(self): 95 | """Format traceback for printing""" 96 | 97 | output = io.StringIO() 98 | output.write('------------------ ' 99 | 'Traceback (most recent call last) ' 100 | '-----------------\n\n') 101 | output.write('\n'.join( 102 | self._format_frame(f) 103 | for f in self.frames)) 104 | return output.getvalue() 105 | 106 | def format_color(self): # todo: accept a colorizer + style 107 | """Format traceback for printing on 256-color terminal""" 108 | 109 | output = io.StringIO() 110 | output.write('\033[0m------------------ ' 111 | 'Traceback (most recent call last) ' 112 | '-----------------\n\n') 113 | output.write(u'\n'.join( 114 | self._format_frame_color(f) 115 | for f in self.frames)) 116 | return output.getvalue() 117 | 118 | def _format_frame(self, frame): 119 | output = io.StringIO() 120 | output.write( 121 | u' File "{0}", line {1}, in {2}\n'.format( 122 | frame.filename, frame.lineno, frame.name)) 123 | 124 | if frame.context: 125 | for line in frame.context: 126 | fmtstring = u'{0:4d}: {1}\n' 127 | if line[0] == frame.lineno: 128 | fmtstring = u' > ' + fmtstring 129 | else: 130 | fmtstring = u' ' + fmtstring 131 | output.write(fmtstring.format(line[0], line[1])) 132 | 133 | if len(frame.locs): 134 | output.write(u'\n Local variables:\n') 135 | 136 | for key, val in sorted(six.iteritems(frame.locs)): 137 | output.write(u' {0} = {1}\n'.format(key, val)) 138 | 139 | return output.getvalue() 140 | 141 | def _format_frame_color(self, frame): 142 | from pygments import highlight 143 | from pygments.lexers import get_lexer_by_name 144 | from pygments.formatters import Terminal256Formatter 145 | 146 | _code_lexer = get_lexer_by_name('python') 147 | _code_formatter = Terminal256Formatter(style='monokai') 148 | 149 | def _highlight(code): 150 | return highlight(code, _code_lexer, _code_formatter) 151 | 152 | output = io.StringIO() 153 | output.write( 154 | u'\033[0m' 155 | u'\033[1mFile\033[0m \033[38;5;70m"{0}"\033[39m, ' 156 | u'\033[1mline\033[0m \033[38;5;190m{1}\033[39m, ' 157 | u'\033[1min\033[0m \033[38;5;214m{2}\033[0m\n\n' 158 | .format(frame.filename, frame.lineno, frame.name)) 159 | 160 | if frame.context: 161 | for line in frame.context: 162 | fmtstring = u'{0:4d}: {1}\n' 163 | if line[0] == frame.lineno: 164 | fmtstring = (u' \033[48;5;250m\033[38;5;232m' 165 | u'{0:4d}\033[0m {1}\n') 166 | else: 167 | fmtstring = (u' \033[48;5;237m\033[38;5;250m' 168 | u'{0:4d}\033[0m {1}\n') 169 | 170 | color_line = _highlight(line[1]) 171 | output.write(fmtstring.format(line[0], color_line.rstrip())) 172 | 173 | if len(frame.locs): 174 | output.write(u'\n \033[1mLocal variables:\033[0m\n') 175 | 176 | for key, val in sorted(six.iteritems(frame.locs)): 177 | code_line = _highlight(u'{0} = {1}'.format(key, val)).rstrip() 178 | output.write(u' {0}\n'.format(code_line)) 179 | 180 | return output.getvalue() 181 | 182 | @classmethod 183 | def _extract_tb(cls, tb, limit=None): 184 | if limit is None: 185 | if hasattr(sys, 'tracebacklimit'): 186 | limit = sys.tracebacklimit 187 | frames = [] 188 | n = 0 189 | while tb is not None and (limit is None or n < limit): 190 | f = tb.tb_frame 191 | lineno = tb.tb_lineno 192 | co = f.f_code 193 | filename = co.co_filename 194 | name = co.co_name 195 | linecache.checkcache(filename) 196 | line = linecache.getline(filename, lineno, f.f_globals) 197 | locs = f.f_locals # Will be converted to repr() by FrameInfo 198 | if line: 199 | line = line.strip() 200 | else: 201 | line = None 202 | frames.append(FrameInfo(filename, lineno, name, line, locs)) 203 | tb = tb.tb_next 204 | n = n+1 205 | return frames 206 | 207 | def __str__(self): 208 | return self.format().encode('utf-8') 209 | 210 | def __unicode__(self): 211 | return self.format() 212 | -------------------------------------------------------------------------------- /scripts/manual_test.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from nicelog.formatters import Colorful 5 | 6 | logger = logging.getLogger('foo') 7 | logger.setLevel(logging.DEBUG) 8 | 9 | handler = logging.StreamHandler(sys.stderr) 10 | handler.setFormatter(Colorful()) 11 | handler.setLevel(logging.DEBUG) 12 | 13 | logger.addHandler(handler) 14 | 15 | 16 | def first_wrapper(): 17 | local_1st_1 = 'Value of first local var' # noqa 18 | local_1st_2 = 1234 # noqa 19 | second_wrapper() 20 | 21 | 22 | def second_wrapper(): 23 | local_2nd_1 = True # noqa 24 | local_2nd_2 = u'Something different' # noqa 25 | some_function() 26 | 27 | 28 | def some_function(): 29 | func_local = {'Hello': 'World'} # noqa 30 | raise ValueError('This is an exception') 31 | 32 | 33 | def do_stuff(): 34 | logger.debug('Debug message') 35 | logger.info('Info message') 36 | logger.warning('Warning message') 37 | logger.error('Error message') 38 | logger.critical('Critical message') 39 | try: 40 | first_wrapper() 41 | except: 42 | logger.exception("An error occurred") 43 | 44 | 45 | do_stuff() 46 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .svn,CVS,.bzr,.hg,.git,__pycache__ 3 | 4 | [isort] 5 | # see: https://github.com/timothycrosley/isort/wiki/isort-Settings 6 | # forced_separate=tests 7 | multi_line_output=4 8 | known_first_party=nicelog,tests 9 | # known_third_party= 10 | default_section=THIRDPARTY 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | version = '0.3' 5 | 6 | here = os.path.dirname(__file__) 7 | 8 | with open(os.path.join(here, 'README.rst')) as fp: 9 | longdesc = [fp.read()] 10 | 11 | with open(os.path.join(here, 'CHANGELOG.rst')) as fp: 12 | longdesc.append(fp.read()) 13 | 14 | longdesc = "\n\n".join(longdesc) 15 | 16 | setup( 17 | name='nicelog', 18 | version=version, 19 | packages=find_packages(), 20 | url='http://github.com/rshk/nicelog', 21 | license='BSD', 22 | author='Samuele Santi', 23 | author_email='samuele@samuelesanti.com', 24 | description='Nice colorful formatters for Python logging.', 25 | long_description=longdesc, 26 | install_requires=['pygments', 'six'], 27 | # tests_require=tests_require, 28 | test_suite='tests', 29 | classifiers=[ 30 | "License :: OSI Approved :: BSD License", 31 | # "Development Status :: 1 - Planning", 32 | # "Development Status :: 2 - Pre-Alpha", 33 | # "Development Status :: 3 - Alpha", 34 | # "Development Status :: 4 - Beta", 35 | "Development Status :: 5 - Production/Stable", 36 | # "Development Status :: 6 - Mature", 37 | # "Development Status :: 7 - Inactive", 38 | 39 | "Programming Language :: Python :: 2.6", 40 | "Programming Language :: Python :: 2.7", 41 | ], 42 | package_data={'': ['README.rst']}, 43 | include_package_data=True, 44 | zip_safe=False) 45 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | freezegun 4 | flake8 5 | -------------------------------------------------------------------------------- /tests/test_formatters/test_colorful.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division, unicode_literals 4 | 5 | import io 6 | import re 7 | from logging import DEBUG, StreamHandler, getLogger 8 | 9 | import pytest 10 | from freezegun import freeze_time 11 | 12 | from nicelog.formatters import Colorful 13 | 14 | 15 | @freeze_time('2015-11-30 16:30') 16 | class TestColorfulFormatter(object): 17 | 18 | @pytest.fixture 19 | def logger_output(self): 20 | return io.StringIO() 21 | 22 | @pytest.fixture 23 | def logger(self, logger_output): 24 | handler = StreamHandler(logger_output) 25 | handler.setLevel(DEBUG) 26 | formatter = Colorful() 27 | handler.setFormatter(formatter) 28 | logger = getLogger('test_logger') 29 | logger.setLevel(DEBUG) 30 | logger.addHandler(handler) 31 | return logger 32 | 33 | def test_formatter_works_end_to_end(self, logger, logger_output): 34 | 35 | logger.info('Hello, world') 36 | 37 | data = logger_output.getvalue() 38 | 39 | # Make sure we have colors, but don't bother matching them 40 | assert '\x1b[' in data 41 | 42 | RE_EXPECTED = re.compile( 43 | r'^ INFO ' 44 | r'2015-11-30 16:30:00 ' 45 | r'test_logger ' 46 | r'test_colorful.py:[0-9]+ ' 47 | r'test_colorful.test_formatter_works_end_to_end \n' 48 | r' Hello, world\n$') 49 | 50 | clean = re.sub('\x1b\[.*?m', '', data) 51 | assert RE_EXPECTED.match(clean) 52 | 53 | def test_debug_message_can_be_logged(self, logger, logger_output): 54 | 55 | logger.debug('Hello, world') 56 | 57 | data = logger_output.getvalue() 58 | 59 | assert '\x1b[' in data 60 | assert 'DEBUG' in data 61 | assert 'Hello, world' in data 62 | 63 | def test_warning_message_can_be_logged(self, logger, logger_output): 64 | 65 | logger.warning('Hello, world') 66 | 67 | data = logger_output.getvalue() 68 | 69 | assert '\x1b[' in data 70 | assert 'WARNING' in data 71 | assert 'Hello, world' in data 72 | 73 | def test_error_message_can_be_logged(self, logger, logger_output): 74 | 75 | logger.error('Hello, world') 76 | 77 | data = logger_output.getvalue() 78 | 79 | assert '\x1b[' in data 80 | assert 'ERROR' in data 81 | assert 'Hello, world' in data 82 | 83 | def test_exception_can_be_logged(self, logger, logger_output): 84 | 85 | try: 86 | raise ValueError('EXCEPTION_MESSAGE') 87 | except: 88 | logger.exception('Hello, world') 89 | 90 | data = logger_output.getvalue() 91 | 92 | assert '\x1b[' in data 93 | assert 'EXCEPTION' in data 94 | assert 'Hello, world' in data 95 | assert 'EXCEPTION_MESSAGE' in data 96 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = flake8,py27,py34,py35,py36,py37 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | flake8 8 | freezegun 9 | commands = 10 | py.test ./tests/ -s -vvv 11 | 12 | [testenv:flake8] 13 | deps = flake8 14 | commands = flake8 nicelog tests 15 | basepython = python3.5 16 | 17 | [testenv:pylint] 18 | deps = pylint 19 | commands = pylint nicelog tests 20 | basepython = python3.4 21 | --------------------------------------------------------------------------------