# -*- coding: utf-8 -*- """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. """ from __future__ import absolute_import, division, unicode_literals import logging import sys from os.path import normpath from pprint import PrettyPrinter from string import Formatter from sys import exc_info as sys_exc_info from traceback import extract_stack, format_list from .compatibility import StringIO, string_type, to_str, xbmc from .constants import ADDON_ID from .utils.convert_format import to_unicode from .utils.system_version import current_system_version # noinspection PyUnresolvedReferences __all__ = ( 'check_frame', 'critical', 'debug', 'debugging', 'error', 'exception', 'info', 'log', 'warning', 'CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'WARNING', ) class RecordFormatter(logging.Formatter): def formatMessage(self, record): record.__dict__['__sep__'] = '\n' if '\n' in record.message else ' - ' try: return self._style.format(record) except AttributeError: try: return self._fmt % record.__dict__ except UnicodeDecodeError as e: record.__dict__ = { key: to_unicode(value) for key, value in record.__dict__.items() } try: return self._fmt % record.__dict__ except UnicodeDecodeError: raise e def formatStack(self, stack_info): return stack_info def format(self, record): record.message = to_unicode(record.getMessage()) if self.usesTime(): record.asctime = self.formatTime(record, self.datefmt) s = self.formatMessage(record) if record.exc_info: if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: if record.stack_info: if s[-1:] != '\n': s += '\n\n' s += self.formatStack(record.stack_info) if s[-1:] != '\n': s += '\n\n' s += record.exc_text elif record.stack_info: if s[-1:] != '\n': s += '\n\n' s += self.formatStack(record.stack_info) return s class StreamWrapper(object): OPEN = frozenset(('(', '[', '{')) CLOSE = frozenset((')', ']', '}')) def __init__(self, stream, indent_per_level, level, indent): self.stream = stream self.indent_per_level = indent_per_level self.level = level self.indent = indent self.previous_indent = 0 self.previous_out = '' def update_level(self, level, indent): self.level = level self.indent = indent def write(self, out): write = self.stream.write indent = self.indent out = to_unicode(out) if '\n' in out: write(to_str(out)) elif out in self.OPEN: write(to_str(out)) write('\n' + (1 + indent) * ' ') elif out in self.CLOSE: if self.previous_out not in self.CLOSE: if indent == self.previous_indent: indent = (self.level - 1) * self.indent_per_level write('\n' + indent * ' ') write(to_str(out)) else: write(to_str(out)) self.previous_indent = indent self.previous_out = out class VariableWidthPrettyPrinter(PrettyPrinter, object): def _format(self, object, stream, indent, allowance, context, level): if not isinstance(object, string_type): indent = level * self._indent_per_level if level: stream.update_level(level, indent) else: stream = StreamWrapper( stream, self._indent_per_level, level, indent, ) super(VariableWidthPrettyPrinter, self)._format( object=object, stream=stream, indent=indent, allowance=allowance, context=context, level=level, ) class PrettyPrintFormatter(Formatter): _pretty_printer = VariableWidthPrettyPrinter(indent=4, width=160) def convert_field(self, value, conversion): if conversion == 'r': return self._pretty_printer.pformat(value) if conversion in {'d', 'e', 't', 'w'}: _sort_dicts = sort_dicts = getattr(self._pretty_printer, '_sort_dicts', None) width = self._pretty_printer._width # __dict__ if conversion == 'd': if sort_dicts: _sort_dicts = False try: value = getattr(value, '__repr_data__')() except AttributeError: if not isinstance(value, dict): value = { attr: getattr(value, attr, None) for attr in dir(value) } # eval iterators elif conversion == 'e': if (getattr(value, '__iter__', None) and not getattr(value, '__len__', None)): value = tuple(value) if sort_dicts: _sort_dicts = False # text representation elif conversion == 't': try: value = getattr(value, '__str_parts__')(as_dict=True) if sort_dicts: _sort_dicts = False except AttributeError: pass # wide output elif conversion == 'w': self._pretty_printer._width = 2 * width if _sort_dicts != sort_dicts: self._pretty_printer._sort_dicts = _sort_dicts out = self._pretty_printer.pformat(value) if sort_dicts: self._pretty_printer._sort_dicts = sort_dicts self._pretty_printer._width = width return out return super(PrettyPrintFormatter, self).convert_field( value, conversion, ) if not current_system_version.compatible(19): def parse(self, *args, **kwargs): output = super(PrettyPrintFormatter, self).parse(*args, **kwargs) return ( (to_str(literal_text), field_name, format_spec, conversion) for literal_text, field_name, format_spec, conversion in output ) def format_field(self, *args, **kwargs): return to_str( super(PrettyPrintFormatter, self).format_field(*args, **kwargs) ) class MessageFormatter(object): _formatter = PrettyPrintFormatter() __slots__ = ( 'args', 'kwargs', 'msg', ) def __init__(self, msg, *args, **kwargs): self.msg = msg self.args = args self.kwargs = kwargs def __str__(self): return self._formatter.vformat(self.msg, self.args, self.kwargs) class Handler(logging.Handler): LEVELS = { logging.NOTSET: xbmc.LOGNONE, logging.DEBUG: xbmc.LOGDEBUG, # logging.INFO: xbmc.LOGINFO, logging.INFO: xbmc.LOGNOTICE, logging.WARN: xbmc.LOGWARNING, logging.WARNING: xbmc.LOGWARNING, logging.ERROR: xbmc.LOGERROR, logging.CRITICAL: xbmc.LOGFATAL, } STANDARD_FORMATTER = RecordFormatter( fmt='[%(addon_id)s] %(module)s:%(lineno)d(%(funcName)s)' '%(__sep__)s%(message)s', ) DEBUG_FORMATTER = RecordFormatter( fmt='[%(addon_id)s] %(module)s, line %(lineno)d, in %(funcName)s' '\n%(message)s', ) _stack_info = False def __init__(self, level): super(Handler, self).__init__(level=level) self.setFormatter(self.STANDARD_FORMATTER) def emit(self, record): record.addon_id = ADDON_ID xbmc.log( msg=self.format(record), level=self.LEVELS.get(record.levelno, xbmc.LOGDEBUG), ) def format(self, record): if self.stack_info: fmt = self.DEBUG_FORMATTER else: fmt = self.STANDARD_FORMATTER return fmt.format(record) @property def stack_info(self): return self._stack_info @stack_info.setter def stack_info(self, value): type(self)._stack_info = value class LogRecord(logging.LogRecord): def __init__(self, name, level, pathname, lineno, msg, args, exc_info, func=None, **kwargs): stack_info = kwargs.pop('sinfo', None) super(LogRecord, self).__init__(name, level, pathname, lineno, msg, args, exc_info, func=func, **kwargs) self.stack_info = stack_info if not current_system_version.compatible(19): def getMessage(self): msg = self.msg if isinstance(msg, MessageFormatter): msg = msg.__str__() else: msg = to_str(msg) if self.args: msg = msg % self.args return msg class KodiLogger(logging.Logger): _verbose_logging = False _stack_info = False def __init__(self, name, level=logging.DEBUG): super(KodiLogger, self).__init__(name=name, level=level) self.propagate = False self.addHandler(Handler(level=logging.DEBUG)) def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, stacklevel=1, **kwargs): if isinstance(msg, (list, tuple)): msg = '\n'.join(map(to_str, msg)) if kwargs: msg = MessageFormatter(msg, *args, **kwargs) args = () elif args and args[0] == '*(' and args[-1] == ')': msg = MessageFormatter(msg, *args[1:-1], **kwargs) args = () stack_info = stack_info and (exc_info or self.stack_info) sinfo = None if _srcfiles: try: fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel) except ValueError: fn, lno, func = '(unknown file)', 0, '(unknown function)' else: fn, lno, func = '(unknown file)', 0, '(unknown function)' if exc_info: if isinstance(exc_info, BaseException): exc_info = (type(exc_info), exc_info, exc_info.__traceback__) elif not isinstance(exc_info, tuple): exc_info = sys_exc_info() record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo) self.handle(record) def findCaller(self, stack_info=False, stacklevel=1): target_frame = logging.currentframe() if target_frame is None: return '(unknown file)', 0, '(unknown function)', None last_frame = None while stacklevel > 0: next_frame = target_frame.f_back if next_frame is None: break target_frame = next_frame stacklevel, is_internal = check_frame(target_frame, stacklevel) if is_internal: continue if last_frame is None: last_frame = target_frame stacklevel -= 1 if stack_info: with StringIO() as output: output.write('Stack (most recent call last):\n') for item in format_list(extract_stack(last_frame)): output.write(item) stack_info = output.getvalue() if stack_info[-1] == '\n': stack_info = stack_info[:-1] else: stack_info = None target_frame_code = target_frame.f_code return (target_frame_code.co_filename, target_frame.f_lineno, target_frame_code.co_name, stack_info) def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None): rv = LogRecord(name, level, fn, lno, msg, args, exc_info, func=func, sinfo=sinfo) if extra is not None: for key in extra: if (key in ["message", "asctime"]) or (key in rv.__dict__): raise KeyError("Attempt to overwrite %r in LogRecord" % key) rv.__dict__[key] = extra[key] return rv def exception(self, msg, *args, **kwargs): if self.isEnabledFor(ERROR): self._log( ERROR, msg, args, exc_info=kwargs.pop('exc_info', True), stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs ) def error_trace(self, msg, *args, **kwargs): if self.isEnabledFor(ERROR): self._log( ERROR, msg, args, stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs ) def warning_trace(self, msg, *args, **kwargs): if self.isEnabledFor(WARNING): self._log( WARNING, msg, args, stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs ) def debug_trace(self, msg, *args, **kwargs): if self.isEnabledFor(DEBUG): self._log( DEBUG, msg, args, stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs ) @property def debugging(self): return self.isEnabledFor(logging.DEBUG) @debugging.setter def debugging(self, value): if value: Handler.LEVELS[logging.DEBUG] = xbmc.LOGNOTICE self.setLevel(logging.DEBUG) root.setLevel(logging.DEBUG) else: Handler.LEVELS[logging.DEBUG] = xbmc.LOGDEBUG self.setLevel(logging.INFO) root.setLevel(logging.INFO) @property def stack_info(self): return self._stack_info @stack_info.setter def stack_info(self, value): if value: type(self)._stack_info = True Handler.stack_info = True else: type(self)._stack_info = False Handler.stack_info = False @property def verbose_logging(self): return self._verbose_logging @verbose_logging.setter def verbose_logging(self, value): cls = type(self) if value: cls._verbose_logging = True logging.root = root logging.Logger.root = root logging.Logger.manager = manager logging.Logger.manager.setLoggerClass(KodiLogger) logging.setLoggerClass(KodiLogger) else: if cls._verbose_logging: logging.root = logging.RootLogger(logging.WARNING) logging.Logger.root = logging.root logging.Logger.manager = logging.Manager(logging.root) logging.Logger.manager.setLoggerClass(logging.Logger) logging.setLoggerClass(logging.Logger) cls._verbose_logging = False class RootLogger(KodiLogger): def __init__(self, level): super(RootLogger, self).__init__('root', level) def __reduce__(self): return getLogger, () root = RootLogger(logging.INFO) KodiLogger.root = root manager = logging.Manager(root) KodiLogger.manager = manager KodiLogger.manager.setLoggerClass(KodiLogger) critical = root.critical error = root.error warning = root.warning info = root.info debug = root.debug log = root.log CRITICAL = logging.CRITICAL ERROR = logging.ERROR WARNING = logging.WARNING INFO = logging.INFO DEBUG = logging.DEBUG def exception(msg, *args, **kwargs): root.error(msg, *args, exc_info=kwargs.pop('exc_info', True), stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs) def error_trace(msg, *args, **kwargs): root.error(msg, *args, stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs) def warning_trace(msg, *args, **kwargs): root.warning(msg, *args, stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs) def debug_trace(msg, *args, **kwargs): root.debug(msg, *args, stack_info=kwargs.pop('stack_info', True), stacklevel=kwargs.pop('stacklevel', 1), **kwargs) def getLogger(name=None): if not name or isinstance(name, string_type) and name == root.name: return root return KodiLogger.manager.getLogger(name) _srcfiles = { normpath(getLogger.__code__.co_filename).lower(), normpath(logging.getLogger.__code__.co_filename).lower(), } def check_frame(frame, stacklevel=None, skip_paths=None): filename = normpath(frame.f_code.co_filename).lower() is_internal = ( filename in _srcfiles or ('importlib' in filename and '_bootstrap' in filename) or (skip_paths and any(skip_path in filename for skip_path in skip_paths)) ) if stacklevel is None: return is_internal if (ADDON_ID in filename and filename.endswith(( 'function_cache.py', 'abstract_settings.py', 'xbmc_items.py', ))): stacklevel += 1 return stacklevel, is_internal __original_module__ = sys.modules[__name__] class ModuleProperties(__original_module__.__class__, object): __name__ = __original_module__.__name__ __file__ = __original_module__.__file__ __getattribute__ = __original_module__.__getattribute__ def __getattr__(self, item): if item == 'debugging': return root.isEnabledFor(logging.DEBUG) raise AttributeError( 'module \'{}\' has no attribute \'{}\''.format(__name__, item) ) sys.modules[__name__] = ModuleProperties(__name__, __doc__)