# This file is part of beets. # Copyright 2016, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. """A drop-in replacement for the standard-library `logging` module that allows {}-style log formatting on Python 2 and 3. Provides everything the "logging" module does. The only difference is that when getLogger(name) instantiates a logger that logger uses {}-style formatting. """ from copy import copy from logging import * # noqa import subprocess import threading def logsafe(val): """Coerce a potentially "problematic" value so it can be formatted in a Unicode log string. This works around a number of pitfalls when logging objects in Python 2: - Logging path names, which must be byte strings, requires conversion for output. - Some objects, including some exceptions, will crash when you call `unicode(v)` while `str(v)` works fine. CalledProcessError is an example. """ # Already Unicode. if isinstance(val, str): return val # Bytestring: needs decoding. elif isinstance(val, bytes): # Blindly convert with UTF-8. Eventually, it would be nice to # (a) only do this for paths, if they can be given a distinct # type, and (b) warn the developer if they do this for other # bytestrings. return val.decode('utf-8', 'replace') # A "problem" object: needs a workaround. elif isinstance(val, subprocess.CalledProcessError): try: return str(val) except UnicodeDecodeError: # An object with a broken __unicode__ formatter. Use __str__ # instead. return str(val).decode('utf-8', 'replace') # Other objects are used as-is so field access, etc., still works in # the format string. else: return val class StrFormatLogger(Logger): """A version of `Logger` that uses `str.format`-style formatting instead of %-style formatting. """ class _LogMessage: def __init__(self, msg, args, kwargs): self.msg = msg self.args = args self.kwargs = kwargs def __str__(self): args = [logsafe(a) for a in self.args] kwargs = {k: logsafe(v) for (k, v) in self.kwargs.items()} return self.msg.format(*args, **kwargs) def _log(self, level, msg, args, exc_info=None, extra=None, **kwargs): """Log msg.format(*args, **kwargs)""" m = self._LogMessage(msg, args, kwargs) return super()._log(level, m, (), exc_info, extra) class ThreadLocalLevelLogger(Logger): """A version of `Logger` whose level is thread-local instead of shared. """ def __init__(self, name, level=NOTSET): self._thread_level = threading.local() self.default_level = NOTSET super().__init__(name, level) @property def level(self): try: return self._thread_level.level except AttributeError: self._thread_level.level = self.default_level return self.level @level.setter def level(self, value): self._thread_level.level = value def set_global_level(self, level): """Set the level on the current thread + the default value for all threads. """ self.default_level = level self.setLevel(level) class BeetsLogger(ThreadLocalLevelLogger, StrFormatLogger): pass my_manager = copy(Logger.manager) my_manager.loggerClass = BeetsLogger def getLogger(name=None): # noqa if name: return my_manager.getLogger(name) else: return Logger.root