Merge branch 'develop' into feature/mixers
Conflicts: mopidy/backend/__init__.py
This commit is contained in:
commit
7e6d3de81a
@ -71,8 +71,15 @@ Feature release.
|
||||
|
||||
**Configuration**
|
||||
|
||||
- Add ``optional=True`` support to :class:`mopidy.config.Boolean`.
|
||||
|
||||
**Logging**
|
||||
|
||||
- Fix proper decoding of exception messages that depends on the user's locale.
|
||||
|
||||
- Colorize logs depending on log level. This can be turned off with the new
|
||||
:confval:`logging/color` configuration. (Fixes: :issue:`772`)
|
||||
|
||||
**Extension support**
|
||||
|
||||
- Removed the :class:`~mopidy.ext.Extension` methods that were deprecated in
|
||||
|
||||
@ -107,6 +107,11 @@ Audio configuration
|
||||
Logging configuration
|
||||
---------------------
|
||||
|
||||
.. confval:: logging/color
|
||||
|
||||
Whether or not to colorize the console log based on log level. Defaults to
|
||||
``true``.
|
||||
|
||||
.. confval:: logging/console_format
|
||||
|
||||
The log format used for informational logging.
|
||||
|
||||
@ -15,6 +15,7 @@ from mopidy.utils import path, versioning
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_logging_schema = ConfigSchema('logging')
|
||||
_logging_schema['color'] = Boolean()
|
||||
_logging_schema['console_format'] = String()
|
||||
_logging_schema['debug_format'] = String()
|
||||
_logging_schema['debug_file'] = Path()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
[logging]
|
||||
color = true
|
||||
console_format = %(levelname)-8s %(message)s
|
||||
debug_format = %(levelname)-8s %(asctime)s [%(process)d:%(threadName)s] %(name)s\n %(message)s
|
||||
debug_file = mopidy.log
|
||||
|
||||
@ -151,7 +151,13 @@ class Boolean(ConfigValue):
|
||||
true_values = ('1', 'yes', 'true', 'on')
|
||||
false_values = ('0', 'no', 'false', 'off')
|
||||
|
||||
def __init__(self, optional=False):
|
||||
self._required = not optional
|
||||
|
||||
def deserialize(self, value):
|
||||
validators.validate_required(value, self._required)
|
||||
if not value:
|
||||
return None
|
||||
if value.lower() in self.true_values:
|
||||
return True
|
||||
elif value.lower() in self.false_values:
|
||||
|
||||
@ -14,6 +14,7 @@ import tornado.websocket
|
||||
from mopidy import models, zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.http import handlers
|
||||
from mopidy.utils import formatting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -88,7 +89,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
|
||||
logger.debug(
|
||||
'HTTP routes from extensions: %s',
|
||||
list((l[0], l[1]) for l in request_handlers))
|
||||
formatting.indent('\n'.join(
|
||||
'%r: %r' % (r[0], r[1]) for r in request_handlers)))
|
||||
return request_handlers
|
||||
|
||||
def _get_app_request_handlers(self):
|
||||
|
||||
@ -152,6 +152,9 @@ class ClientListHandler(tornado.web.RequestHandler):
|
||||
self.apps = apps
|
||||
self.statics = statics
|
||||
|
||||
def get_template_path(self):
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
def get(self):
|
||||
set_mopidy_headers(self)
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import zeroconf
|
||||
from mopidy import exceptions, zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.mpd import session
|
||||
from mopidy.utils import encoding, network, process
|
||||
@ -34,10 +33,9 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
||||
max_connections=config['mpd']['max_connections'],
|
||||
timeout=config['mpd']['connection_timeout'])
|
||||
except IOError as error:
|
||||
logger.error(
|
||||
'MPD server startup failed: %s',
|
||||
raise exceptions.FrontendError(
|
||||
'MPD server startup failed: %s' %
|
||||
encoding.locale_decode(error))
|
||||
sys.exit(1)
|
||||
|
||||
logger.info('MPD server running at [%s]:%s', self.hostname, self.port)
|
||||
|
||||
|
||||
@ -3,6 +3,16 @@ from __future__ import unicode_literals
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import platform
|
||||
|
||||
|
||||
LOG_LEVELS = {
|
||||
-1: dict(root=logging.ERROR, mopidy=logging.WARNING),
|
||||
0: dict(root=logging.ERROR, mopidy=logging.INFO),
|
||||
1: dict(root=logging.WARNING, mopidy=logging.DEBUG),
|
||||
2: dict(root=logging.INFO, mopidy=logging.DEBUG),
|
||||
3: dict(root=logging.DEBUG, mopidy=logging.DEBUG),
|
||||
}
|
||||
|
||||
|
||||
class DelayedHandler(logging.Handler):
|
||||
@ -46,13 +56,41 @@ def setup_logging(config, verbosity_level, save_debug_log):
|
||||
_delayed_handler.release()
|
||||
|
||||
|
||||
LOG_LEVELS = {
|
||||
-1: dict(root=logging.ERROR, mopidy=logging.WARNING),
|
||||
0: dict(root=logging.ERROR, mopidy=logging.INFO),
|
||||
1: dict(root=logging.WARNING, mopidy=logging.DEBUG),
|
||||
2: dict(root=logging.INFO, mopidy=logging.DEBUG),
|
||||
3: dict(root=logging.DEBUG, mopidy=logging.DEBUG),
|
||||
}
|
||||
def setup_console_logging(config, verbosity_level):
|
||||
if verbosity_level < min(LOG_LEVELS.keys()):
|
||||
verbosity_level = min(LOG_LEVELS.keys())
|
||||
if verbosity_level > max(LOG_LEVELS.keys()):
|
||||
verbosity_level = max(LOG_LEVELS.keys())
|
||||
|
||||
loglevels = config.get('loglevels', {})
|
||||
has_debug_loglevels = any([
|
||||
level < logging.INFO for level in loglevels.values()])
|
||||
|
||||
verbosity_filter = VerbosityFilter(verbosity_level, loglevels)
|
||||
|
||||
if verbosity_level < 1 and not has_debug_loglevels:
|
||||
log_format = config['logging']['console_format']
|
||||
else:
|
||||
log_format = config['logging']['debug_format']
|
||||
formatter = logging.Formatter(log_format)
|
||||
|
||||
if config['logging']['color']:
|
||||
handler = ColorizingStreamHandler()
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
handler.addFilter(verbosity_filter)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
logging.getLogger('').addHandler(handler)
|
||||
|
||||
|
||||
def setup_debug_logging_to_file(config):
|
||||
formatter = logging.Formatter(config['logging']['debug_format'])
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
config['logging']['debug_file'], maxBytes=10485760, backupCount=3)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
logging.getLogger('').addHandler(handler)
|
||||
|
||||
|
||||
class VerbosityFilter(logging.Filter):
|
||||
@ -72,35 +110,74 @@ class VerbosityFilter(logging.Filter):
|
||||
return record.levelno >= required_log_level
|
||||
|
||||
|
||||
def setup_console_logging(config, verbosity_level):
|
||||
if verbosity_level < min(LOG_LEVELS.keys()):
|
||||
verbosity_level = min(LOG_LEVELS.keys())
|
||||
if verbosity_level > max(LOG_LEVELS.keys()):
|
||||
verbosity_level = max(LOG_LEVELS.keys())
|
||||
class ColorizingStreamHandler(logging.StreamHandler):
|
||||
"""
|
||||
Stream handler which colorizes the log using ANSI escape sequences.
|
||||
|
||||
loglevels = config.get('loglevels', {})
|
||||
has_debug_loglevels = any([
|
||||
level < logging.INFO for level in loglevels.values()])
|
||||
Does nothing on Windows, which doesn't support ANSI escape sequences.
|
||||
|
||||
verbosity_filter = VerbosityFilter(verbosity_level, loglevels)
|
||||
This implementation is based upon https://gist.github.com/vsajip/758430,
|
||||
which is:
|
||||
|
||||
if verbosity_level < 1 and not has_debug_loglevels:
|
||||
log_format = config['logging']['console_format']
|
||||
else:
|
||||
log_format = config['logging']['debug_format']
|
||||
formatter = logging.Formatter(log_format)
|
||||
Copyright (C) 2010-2012 Vinay Sajip. All rights reserved.
|
||||
Licensed under the new BSD license.
|
||||
"""
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.addFilter(verbosity_filter)
|
||||
handler.setFormatter(formatter)
|
||||
color_map = {
|
||||
'black': 0,
|
||||
'red': 1,
|
||||
'green': 2,
|
||||
'yellow': 3,
|
||||
'blue': 4,
|
||||
'magenta': 5,
|
||||
'cyan': 6,
|
||||
'white': 7,
|
||||
}
|
||||
|
||||
logging.getLogger('').addHandler(handler)
|
||||
# Map logging levels to (background, foreground, bold/intense)
|
||||
level_map = {
|
||||
logging.DEBUG: (None, 'blue', False),
|
||||
logging.INFO: (None, 'white', False),
|
||||
logging.WARNING: (None, 'yellow', False),
|
||||
logging.ERROR: (None, 'red', False),
|
||||
logging.CRITICAL: ('red', 'white', True),
|
||||
}
|
||||
csi = '\x1b['
|
||||
reset = '\x1b[0m'
|
||||
|
||||
is_windows = platform.system() == 'Windows'
|
||||
|
||||
def setup_debug_logging_to_file(config):
|
||||
formatter = logging.Formatter(config['logging']['debug_format'])
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
config['logging']['debug_file'], maxBytes=10485760, backupCount=3)
|
||||
handler.setFormatter(formatter)
|
||||
@property
|
||||
def is_tty(self):
|
||||
isatty = getattr(self.stream, 'isatty', None)
|
||||
return isatty and isatty()
|
||||
|
||||
logging.getLogger('').addHandler(handler)
|
||||
def emit(self, record):
|
||||
try:
|
||||
message = self.format(record)
|
||||
self.stream.write(message)
|
||||
self.stream.write(getattr(self, 'terminator', '\n'))
|
||||
self.flush()
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
def format(self, record):
|
||||
message = logging.StreamHandler.format(self, record)
|
||||
if not self.is_tty or self.is_windows:
|
||||
return message
|
||||
return self.colorize(message, record)
|
||||
|
||||
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
|
||||
|
||||
@ -214,6 +214,10 @@ class BooleanTest(unittest.TestCase):
|
||||
self.assertEqual(b'false', result)
|
||||
self.assertIsInstance(result, bytes)
|
||||
|
||||
def test_deserialize_respects_optional(self):
|
||||
value = types.Boolean(optional=True)
|
||||
self.assertEqual(None, value.deserialize(''))
|
||||
|
||||
# TODO: test None or other invalid values into serialize?
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user