Get rid of the colorlog dependency

colorlog was problematic for various reasons:

- Not commonly packaged for Linux distributions
- Calling colorama.init() automatically on import
- Not supporting {foo} log formatting
- Not supporting an easy way to turn colors off

Instead we now do the log coloring by hand, which is simpler and means
everyone will have colored logs.
This commit is contained in:
Florian Bruhin 2016-05-13 21:08:50 +02:00
parent a9a853baf0
commit c64e5c9bd5
7 changed files with 70 additions and 64 deletions

View File

@ -46,6 +46,8 @@ Changed
not be. not be.
- Show favicons as window icon with `tabs-are-windows` set. - Show favicons as window icon with `tabs-are-windows` set.
- `:bind <key>` without a command now shows the existing binding - `:bind <key>` without a command now shows the existing binding
- The optional `colorlog` dependency got removed, as qutebrowser now displays
colored logs without it.
Fixed Fixed
----- -----

View File

@ -212,7 +212,6 @@ Documentation of used Python libraries:
* http://pythonhosted.org/setuptools/[setuptools] * http://pythonhosted.org/setuptools/[setuptools]
* http://cx-freeze.readthedocs.org/en/latest/overview.html[cx_Freeze] * http://cx-freeze.readthedocs.org/en/latest/overview.html[cx_Freeze]
* https://pypi.python.org/pypi/colorama[colorama] * https://pypi.python.org/pypi/colorama[colorama]
* https://pypi.python.org/pypi/colorlog[colorlog]
Related RFCs and standards: Related RFCs and standards:

View File

@ -109,11 +109,8 @@ The following libraries are optional and provide a better user experience:
To generate the documentation for the `:help` command, when using the git To generate the documentation for the `:help` command, when using the git
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed. repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
The following libraries are optional and provide colored logging in the On Windows, https://pypi.python.org/pypi/colorama/[colorama] is needed to
console: display colored log output.
* https://pypi.python.org/pypi/colorlog/[colorlog]
* On Windows: https://pypi.python.org/pypi/colorama/[colorama]
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
and its dependencies. and its dependencies.

View File

@ -35,33 +35,16 @@ try:
import colorama import colorama
except ImportError: except ImportError:
colorama = None colorama = None
try:
import colorlog COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white']
except ImportError:
colorlog = None
else:
# WORKAROUND
# colorlog calls colorama.init() which we don't want, also it breaks our
# sys.stdout/sys.stderr if they are None. Bugreports:
# https://code.google.com/p/colorama/issues/detail?id=61
# https://github.com/borntyping/python-colorlog/issues/13
# This should be (partially) fixed in colorama 0.3.2.
# (stream only gets wrapped if it's not None)
if colorama is not None:
colorama.deinit()
# Log formats to use. # Log formats to use.
SIMPLE_FMT = '{asctime:8} {levelname}: {message}' SIMPLE_FMT = ('{green}{asctime:8}{reset} {log_color}{levelname}{reset}: '
EXTENDED_FMT = ('{asctime:8} {levelname:8} {name:10} {module}:{funcName}:' '{message}')
'{lineno} {message}') EXTENDED_FMT = ('{green}{asctime:8}{reset} '
SIMPLE_FMT_COLORED = ('%(green)s%(asctime)-8s%(reset)s ' '{log_color}{levelname:8}{reset} '
'%(log_color)s%(levelname)s%(reset)s: %(message)s') '{cyan}{name:10} {module}:{funcName}:{lineno}{reset} '
EXTENDED_FMT_COLORED = ( '{log_color}{message}{reset}')
'%(green)s%(asctime)-8s%(reset)s '
'%(log_color)s%(levelname)-8s%(reset)s '
'%(cyan)s%(name)-10s %(module)s:%(funcName)s:%(lineno)s%(reset)s '
'%(log_color)s%(message)s%(reset)s'
)
EXTENDED_FMT_HTML = ( EXTENDED_FMT_HTML = (
'<tr>' '<tr>'
'<td><pre>%(green)s%(asctime)-8s%(reset)s</pre></td>' '<td><pre>%(green)s%(asctime)-8s%(reset)s</pre></td>'
@ -223,12 +206,7 @@ def _init_formatters(level, color, force_color):
console_formatter/ram_formatter: logging.Formatter instances. console_formatter/ram_formatter: logging.Formatter instances.
use_colorama: Whether to use colorama. use_colorama: Whether to use colorama.
""" """
if level <= logging.DEBUG: console_fmt = EXTENDED_FMT if level <= logging.DEBUG else SIMPLE_FMT
console_fmt = EXTENDED_FMT
console_fmt_colored = EXTENDED_FMT_COLORED
else:
console_fmt = SIMPLE_FMT
console_fmt_colored = SIMPLE_FMT_COLORED
ram_formatter = logging.Formatter(EXTENDED_FMT, DATEFMT, '{') ram_formatter = logging.Formatter(EXTENDED_FMT, DATEFMT, '{')
html_formatter = HTMLFormatter(EXTENDED_FMT_HTML, DATEFMT, html_formatter = HTMLFormatter(EXTENDED_FMT_HTML, DATEFMT,
log_colors=LOG_COLORS) log_colors=LOG_COLORS)
@ -236,14 +214,17 @@ def _init_formatters(level, color, force_color):
return None, ram_formatter, html_formatter, False return None, ram_formatter, html_formatter, False
use_colorama = False use_colorama = False
color_supported = colorlog is not None and (os.name == 'posix' or colorama) color_supported = os.name == 'posix' or colorama
if color_supported and (sys.stderr.isatty() or force_color) and color: if color_supported and (sys.stderr.isatty() or force_color) and color:
console_formatter = colorlog.ColoredFormatter( use_colors = True
console_fmt_colored, DATEFMT, log_colors=LOG_COLORS)
if colorama and os.name != 'posix': if colorama and os.name != 'posix':
use_colorama = True use_colorama = True
else: else:
console_formatter = logging.Formatter(console_fmt, DATEFMT, '{') use_colors = False
console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{',
use_colors=use_colors)
return console_formatter, ram_formatter, html_formatter, use_colorama return console_formatter, ram_formatter, html_formatter, use_colorama
@ -467,9 +448,43 @@ class RAMHandler(logging.Handler):
return '\n'.join(lines) return '\n'.join(lines)
class ColoredFormatter(logging.Formatter):
"""Logging formatter to output colored logs.
Attributes:
use_colors: Whether to do colored logging or not.
Class attributes:
COLOR_ESCAPES: A dict mapping color names to escape sequences
RESET_ESCAPE: The escape sequence using for resetting colors
"""
COLOR_ESCAPES = {color: '\033[{}m'.format(i)
for i, color in enumerate(COLORS, start=30)}
RESET_ESCAPE = '\033[0m'
def __init__(self, fmt, datefmt, style, *, use_colors):
super().__init__(fmt, datefmt, style)
self._use_colors = use_colors
def format(self, record):
if self._use_colors:
color_dict = dict(self.COLOR_ESCAPES)
color_dict['reset'] = self.RESET_ESCAPE
log_color = LOG_COLORS[record.levelname]
color_dict['log_color'] = self.COLOR_ESCAPES[log_color]
else:
color_dict = {color: '' for color in self.COLOR_ESCAPES}
color_dict['reset'] = ''
color_dict['log_color'] = ''
record.__dict__.update(color_dict)
return super().format(record)
class HTMLFormatter(logging.Formatter): class HTMLFormatter(logging.Formatter):
"""Formatter for HTML-colored log messages, similar to colorlog. """Formatter for HTML-colored log messages.
Attributes: Attributes:
_log_colors: The colors to use for logging levels. _log_colors: The colors to use for logging levels.
@ -489,8 +504,7 @@ class HTMLFormatter(logging.Formatter):
self._colordict = {} self._colordict = {}
# We could solve this nicer by using CSS, but for this simple case this # We could solve this nicer by using CSS, but for this simple case this
# works. # works.
for color in ['black', 'red', 'green', 'yellow', 'blue', 'purple', for color in COLORS:
'cyan', 'white']:
self._colordict[color] = '<font color="{}">'.format(color) self._colordict[color] = '<font color="{}">'.format(color)
self._colordict['reset'] = '</font>' self._colordict['reset'] = '</font>'

View File

@ -129,7 +129,6 @@ def _module_versions():
lines = [] lines = []
modules = collections.OrderedDict([ modules = collections.OrderedDict([
('sip', ['SIP_VERSION_STR']), ('sip', ['SIP_VERSION_STR']),
('colorlog', []),
('colorama', ['VERSION', '__version__']), ('colorama', ['VERSION', '__version__']),
('pypeg2', ['__version__']), ('pypeg2', ['__version__']),
('jinja2', ['__version__']), ('jinja2', ['__version__']),

View File

@ -4,5 +4,4 @@ Pygments==2.1.3
pyPEG2==2.15.2 pyPEG2==2.15.2
PyYAML==3.11 PyYAML==3.11
colorama==0.3.7 colorama==0.3.7
colorlog==2.6.3
cssutils==1.0.1 cssutils==1.0.1

View File

@ -319,7 +319,6 @@ class ImportFake:
def __init__(self): def __init__(self):
self.exists = { self.exists = {
'sip': True, 'sip': True,
'colorlog': True,
'colorama': True, 'colorama': True,
'pypeg2': True, 'pypeg2': True,
'jinja2': True, 'jinja2': True,
@ -383,15 +382,14 @@ class TestModuleVersions:
@pytest.mark.usefixtures('import_fake') @pytest.mark.usefixtures('import_fake')
def test_all_present(self): def test_all_present(self):
"""Test with all modules present in version 1.2.3.""" """Test with all modules present in version 1.2.3."""
expected = ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3', expected = ['sip: yes', 'colorama: 1.2.3', 'pypeg2: 1.2.3',
'pypeg2: 1.2.3', 'jinja2: 1.2.3', 'pygments: 1.2.3', 'jinja2: 1.2.3', 'pygments: 1.2.3', 'yaml: 1.2.3',
'yaml: 1.2.3', 'cssutils: 1.2.3'] 'cssutils: 1.2.3']
assert version._module_versions() == expected assert version._module_versions() == expected
@pytest.mark.parametrize('module, idx, expected', [ @pytest.mark.parametrize('module, idx, expected', [
('colorlog', 1, 'colorlog: no'), ('colorama', 1, 'colorama: no'),
('colorama', 2, 'colorama: no'), ('cssutils', 6, 'cssutils: no'),
('cssutils', 7, 'cssutils: no'),
]) ])
def test_missing_module(self, module, idx, expected, import_fake): def test_missing_module(self, module, idx, expected, import_fake):
"""Test with a module missing. """Test with a module missing.
@ -405,15 +403,14 @@ class TestModuleVersions:
assert version._module_versions()[idx] == expected assert version._module_versions()[idx] == expected
@pytest.mark.parametrize('value, expected', [ @pytest.mark.parametrize('value, expected', [
('VERSION', ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3', ('VERSION', ['sip: yes', 'colorama: 1.2.3', 'pypeg2: yes',
'pypeg2: yes', 'jinja2: yes', 'pygments: yes',
'yaml: yes', 'cssutils: yes']),
('SIP_VERSION_STR', ['sip: 1.2.3', 'colorlog: yes', 'colorama: yes',
'pypeg2: yes', 'jinja2: yes', 'pygments: yes',
'yaml: yes', 'cssutils: yes']),
(None, ['sip: yes', 'colorlog: yes', 'colorama: yes', 'pypeg2: yes',
'jinja2: yes', 'pygments: yes', 'yaml: yes', 'jinja2: yes', 'pygments: yes', 'yaml: yes',
'cssutils: yes']), 'cssutils: yes']),
('SIP_VERSION_STR', ['sip: 1.2.3', 'colorama: yes', 'pypeg2: yes',
'jinja2: yes', 'pygments: yes', 'yaml: yes',
'cssutils: yes']),
(None, ['sip: yes', 'colorama: yes', 'pypeg2: yes', 'jinja2: yes',
'pygments: yes', 'yaml: yes', 'cssutils: yes']),
]) ])
def test_version_attribute(self, value, expected, import_fake): def test_version_attribute(self, value, expected, import_fake):
"""Test with a different version attribute. """Test with a different version attribute.
@ -429,7 +426,6 @@ class TestModuleVersions:
assert version._module_versions() == expected assert version._module_versions() == expected
@pytest.mark.parametrize('name, has_version', [ @pytest.mark.parametrize('name, has_version', [
('colorlog', False),
('sip', False), ('sip', False),
('colorama', True), ('colorama', True),
('pypeg2', True), ('pypeg2', True),
@ -442,7 +438,7 @@ class TestModuleVersions:
"""Check if all dependencies have an expected __version__ attribute. """Check if all dependencies have an expected __version__ attribute.
The aim of this test is to fail if modules suddenly don't have a The aim of this test is to fail if modules suddenly don't have a
__version__ attribute anymore in a newer version, or colorlog has one. __version__ attribute anymore in a newer version.
Args: Args:
name: The name of the module to check. name: The name of the module to check.