diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-01-25 17:08:32 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-01-25 17:08:32 +0100 |
commit | bb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2 (patch) | |
tree | 453fd60b926c37cceff91e8107ddd06867cfcd0e /tagit | |
parent | 41fdbe254cbfc74896080b9f5820f856067da537 (diff) | |
download | tagit-bb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2.tar.gz tagit-bb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2.tar.bz2 tagit-bb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2.zip |
logging and status
Diffstat (limited to 'tagit')
-rw-r--r-- | tagit/__init__.py | 22 | ||||
-rw-r--r-- | tagit/logger/__init__.py | 15 | ||||
-rw-r--r-- | tagit/logger/colors.py | 129 | ||||
-rw-r--r-- | tagit/logger/loader.py | 57 | ||||
-rw-r--r-- | tagit/logger/logger.py | 190 | ||||
-rw-r--r-- | tagit/widgets/status.py | 18 |
6 files changed, 418 insertions, 13 deletions
diff --git a/tagit/__init__.py b/tagit/__init__.py index 1a501d0..ec5f1c2 100644 --- a/tagit/__init__.py +++ b/tagit/__init__.py @@ -45,13 +45,31 @@ resource_add_path(os.path.join(os.path.dirname(__file__), 'assets', 'fonts', 'ki from kivy.core.text import LabelBase LabelBase.register(name='Unifont', fn_regular='Unifont.ttf') +# logging +# the default logger is quite verbose. Should be restricted by the app. +import logging +from . import logger +loghandler = logger.logger_config( + handler=logging.StreamHandler(), + colors=logger.ColorsTerminal, + config=dict( + level='DEBUG', + fmt='[{levelname}] [{module:<12}] {title}{message}', + title='[{title}] ', + filter=['tagit'], + ) + ) + +termlogger = logging.getLogger(__name__) +termlogger.addHandler(loghandler) +termlogger.setLevel(logging.DEBUG) + # console logging fix: # kivy adds an extra whitespace in front of tagit log entries. # Adding a carriage return in front of the log fixes this bug. # This is only needed for the console log handler, not others. # Note that this mechanism is repeated in apps/gui to # achieve the same for user-defined log handlers. -#from tagit import loghandler # FIXME: mb/port -#loghandler.formatter.prefix = '\r' # FIXME: mb/port +loghandler.formatter.prefix = '\r' ## EOF ## diff --git a/tagit/logger/__init__.py b/tagit/logger/__init__.py new file mode 100644 index 0000000..1a2cf15 --- /dev/null +++ b/tagit/logger/__init__.py @@ -0,0 +1,15 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# inner-module imports +from .logger import logger_config, TagitFormatter, Filter_Or, CallbackHandler +from .colors import Colors, ColorsTerminal, ColorsMarkup +from . import loader + +# exports +__all__ = ('logger_config', ) + +## EOF ## diff --git a/tagit/logger/colors.py b/tagit/logger/colors.py new file mode 100644 index 0000000..758807d --- /dev/null +++ b/tagit/logger/colors.py @@ -0,0 +1,129 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import re + +# exports +__all__ = ('Colors', 'ColorsTerminal', 'ColorsMarkup') + + +## code ## + +class Colors(object): + """ + + Status: + * ok + * warn + * error + + Messages: + * title + * info + + """ + + ## status ## + + @classmethod + def ok(obj, text): + return obj.BOLD + obj.OK + text + obj.ENDC + obj.ENDB + + @classmethod + def warn(obj, text): + return obj.BOLD + obj.WARN + text + obj.ENDC + obj.ENDB + + @classmethod + def error(obj, text): + return obj.BOLD + obj.ERROR + text + obj.ENDC + obj.ENDB + + + ## ordinary text for logging ## + + @classmethod + def title(obj, text): + return obj.BOLD + obj.TITLE + text + obj.ENDC + obj.ENDB + + @classmethod + def info(obj, text): + return obj.INFO + text + obj.ENDC + + @classmethod + def debug(obj, text): + return obj.DEBUG + text + obj.ENDC + + + ## ordinary text formatting ## + + @classmethod + def highlight(obj, text): + return obj.BOLD + text + obj.ENDC + + + ## meta functions ## + + @classmethod + def uncolor(obj, text): + return re.sub(obj.UNCOL, '', text) + + @classmethod + def unescape(obj, text): + for v, k in obj.ESCAPE[::-1]: + text = text.replace(k, v) + return text + + @classmethod + def escape(obj, text): + for k, v in obj.ESCAPE: + text = text.replace(k, v) + return text + +class ColorsTerminal(Colors): + """Terminal colors.""" + OK = "\033[38;5;2m" # green + WARN = "\033[33m" # yellow + ERROR = "\033[31m" # red + TITLE = "" # white + INFO = "\033[38;5;5m" # magenta + DEBUG = "\033[1;36m" # light blue + BOLD = "\033[1m" # bold + ENDC = "\033[0m" # end color + ENDB = "\033[0m" # end bold + UNCOL = '\\033\[.*?m' + ESCAPE = [] + +class ColorsMarkup(Colors): + """Console colors. + """ + OK = "[color=#00FF00]" # green + WARN = "[color=#FFFF00]" # yellow + ERROR = "[color=#FF0000]" # red + TITLE = "[color=#FF0000]" # red + INFO = "[color=#A52292]" # magenta + DEBUG = "[color=#4CCBE4]" # light blue + BOLD = "[b]" + ENDC = "[/color]" + ENDB = "[/b]" + UNCOL = '\[/?(color|b)(=.*?)?\]' + #UNCOL = '\[/?(color|b)(=#[\dA-Fa-f]+)?\]' # only permits hex color values + ESCAPE = [ + ('&', '&'), + ('[', '&bl;' ), + (']', '&br;' ), + ] + + # markup removal + # advanced: search for proper tags, i.e. start and ending pairs + # needs to be repeated until no more text is replaced + # FIXME: doesn't work for "[size=<size>]..[/size] style tags + #rx3 = re.compile('\[([^\]]+)\](.*?)\[/\\1\]') # replace with '\\2' + # alt (to be checked) for "[size=<size>]..[/size] style tags + #rx3 = re.compile('\[([^\]]+)(=[^\]+)?\](.*?)\[/\\1\]') # replace with '\\3' + # simple: remove everything that looks like a tag + #rx3 = re.compile('\[[^\]]+\]') # replace with '' + +## EOF ## diff --git a/tagit/logger/loader.py b/tagit/logger/loader.py new file mode 100644 index 0000000..cbf555e --- /dev/null +++ b/tagit/logger/loader.py @@ -0,0 +1,57 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import logging + +# tagit imports +from tagit import config + +# exports +__all__ = ('load_logging', ) + + +## code ## + +def load_logging(cfg): + """Configure the main terminal logger.""" + if ('logging', ) in cfg: + from tagit import loghandler + from tagit.shared import ColorsTerminal + from .logger import logger_config + logger_config(loghandler, ColorsTerminal, cfg('logging').to_tree(defaults=True)) + + if not cfg('session', 'debug'): + # set to info, prevents any DEBUG message to pass through the system. + # the handler config cannot overwrite this. + logging.getLogger('tagit').setLevel(logging.INFO) + + +## config ## + +config.declare_title(('logging', ), __name__, 'Logging', + 'Logging is used in several places (terminal, GUI). Wherever used, logging can be configured through a dictionary. Also see view.status, view.console for default values.') + +config.declare(('logging', 'level'), + config.Enum('debug', 'info', 'warn', 'error', 'critical'), 'debug', + __name__, 'Log level', 'Maximal log level for which messages are shown. The order is: critical > error > warning > info > volatile > debug') + +config.declare(('logging', 'filter'), config.List(config.String()), ['tagit'], + __name__, 'Module filter', 'Module name for which log messages are accepted.') + +config.declare(('logging', 'fmt'), config.String(), '[{levelname}] [{name}] {title}{message}', + __name__, 'Log format', 'Log message formatting. The formatting follows the typical python format string where each item is enclosed in curly braces (e.g. "{message}"). For printable items see the `logging attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_. In addition to those, the item "title" is available, which is a placeholder for the formatted title (if present).') + +config.declare(('logging', 'title'), config.String(), '[{title}] ', + __name__, 'Title format', 'Title formatting.') + +config.declare(('logging', 'maxlen'), config.Unsigned(), 80, + __name__, 'Maximal line length', 'Maximal line length (e.g. console width). Use Infinity to set no line length limit.') + +config.declare(('logging', 'prefix'), config.String(), '', + __name__, 'Log prefix', 'A prefix before every log line (internal use only)') + +## EOF ## diff --git a/tagit/logger/logger.py b/tagit/logger/logger.py new file mode 100644 index 0000000..f705888 --- /dev/null +++ b/tagit/logger/logger.py @@ -0,0 +1,190 @@ +"""tagit logging facility. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import logging +import re + +# exports +__all__ = ( + 'CallbackHandler', + 'Filter_Or', + 'TagitFormatter', + 'logger_config', + ) + + +## code ## + +class CallbackHandler(logging.Handler): + """Adapter for logging.Handler that delegates the message to a callback function.""" + def __init__(self, clbk, *args, **kwargs): + self._clbk = clbk + super(CallbackHandler, self).__init__(*args, **kwargs) + + def emit(self, record): + self.acquire() + try: + self._clbk(self.format, record) + except Exception: + self.handleError(record) + finally: + self.release() + +class Filter_Or(list): + """Lets the record pass if any of the child filters accept it.""" + def filter(self, record): + for itm in iter(self): + if itm.filter(record): + return True + return False + +class TagitFormatter(logging.Formatter): + """Default Formatter for tagit. + + This formatter implements the following features: + * Title awareness + * Colors + * Message truncating + + A message of the form 'Title: Message' will be split into the title and message + part. The title formatting is specified in *fmt_title* and only included if a title + is present. This format string expects a title argument. E.g. fmt_title='[{title}] ' + would produce a title '[Title] ' that would then be included in the formatted record. + + The overal format string (*fmt*) can also include a 'title' argument that specifies + how the title should be positioned. The curly-brace style is assumed. + + If a *maxlen* argument is given, the record's message will be truncated such that the + overall length of the log line does not exceed that limit. + + """ + def __init__(self, *args, fmt_title='{title}: ', colors=None, prefix='', + truncate='front', maxlen=float('inf'), **kwargs): + kwargs['style'] = '{' + super(TagitFormatter, self).__init__(*args, **kwargs) + self.fmt_title = fmt_title + self.cmap = colors + self.maxlen = maxlen + self.prefix = prefix + self.truncate = self.truncate_back if truncate == 'back' else self.truncate_front + + def color_levelname(self, levelname): + if self.cmap is None: + return levelname + elif levelname in ('ERROR', 'CRITICAL'): + return self.cmap.error(levelname) + elif levelname in ('WARNING', ): + return self.cmap.warn(levelname) + elif levelname in ('INFO', ): + return self.cmap.info(levelname) + elif levelname in ('DEBUG', ): + return self.cmap.debug(levelname) + else: + return levelname + + def color_title(self, title): + if self.cmap is None: return title + return self.cmap.title(title) + + def truncate_back(self, msg, maxlen=None): + """Truncate a string.""" + maxlen = maxlen if maxlen is not None else self.maxlen + if len(msg) > maxlen: + return msg[:int(maxlen-3)] + '...' + return msg + + def truncate_front(self, msg, maxlen=None): + """Truncate a string.""" + maxlen = maxlen if maxlen is not None else self.maxlen + if len(msg) > maxlen: + return '...' + msg[-int(maxlen-3):] + return msg + + def format_title(self, record): + # tagit title format "title: content" + rx1 = re.compile('^([^:\s]+(?:\s+[^:\s]+){0,1})\s*:(.*)$') + # kivy title format "[title ] content" + rx2 = re.compile('^\[(.{12})\](.*)$') + + msg = str(record.msg) % record.args + m1 = rx1.search(msg) + m2 = rx2.search(msg) + m = m1 if m1 is not None else m2 + if m is not None: # title + title, msg = m.groups() + return self.fmt_title.format(title=self.color_title(title.strip())), msg.strip() + else: # no title + return '', msg.strip() + + def format(self, record): + # reset level name since it might have been modified + record.levelname = logging.getLevelName(record.levelno) + # title + title, msg = self.format_title(record) + # level name + levelname = self.color_levelname(record.levelname) + + # escape message for coloring + if self.cmap is not None: + msg = self.cmap.escape(msg) + # adjust record + omsg, record.msg = record.msg, msg + olvl, record.levelname = record.levelname, levelname + record.title = title + # compile log line + logline = logging.Formatter(fmt=self._fmt, style='{').format(record) + + # get effective lengths of individual parts + + # truncate message to fixed length + if 0 < self.maxlen and self.maxlen < float('inf'): + if self.cmap is None: + record.msg = self.truncate(record.msg, + self.maxlen - len(logline) + len(record.msg) - 1) + else: + tlen = len(self.cmap.unescape(self.cmap.uncolor(logline))) + mlen = len(self.cmap.unescape(self.cmap.uncolor(record.msg))) + record.msg = self.cmap.escape(self.truncate(self.cmap.unescape(record.msg), + self.maxlen - tlen + mlen - 1)) + logline = logging.Formatter(fmt=self._fmt, style='{').format(record) + + # reset record + record.msg, record.levelname = omsg, olvl + + return self.prefix + logline + + +## logger configuration ## + +def logger_config(handler, colors, config): + """Configure a handler from a user-specified config. Returns the handler. + + The config is a dict with the following keys: + * level : Log level (level name) + * filter : Accept all specified modules (list of module names) + * fmt : Main format string + * title : Format string for the title + * maxlen : Maximum log entry line. No limit if unspecified. + * prefix : Log line prefix + """ + if 'level' in config: + handler.setLevel(config['level'].upper()) + + if 'filter' in config: + handler.addFilter(Filter_Or(map(logging.Filter, config['filter']))) + + handler.setFormatter(TagitFormatter( + fmt = colors.escape(config.get('fmt', '{title}{message}')), + fmt_title = colors.escape(config.get('title', '{title}: ')), + maxlen = float(config.get('maxlen', 0)), + colors = colors, + prefix = config.get('prefix', ''), + )) + + return handler + +## EOF ## diff --git a/tagit/widgets/status.py b/tagit/widgets/status.py index fea52b9..e83b8d8 100644 --- a/tagit/widgets/status.py +++ b/tagit/widgets/status.py @@ -18,10 +18,7 @@ from kivy.uix.boxlayout import BoxLayout import kivy.properties as kp # tagit imports -from tagit import config -from tagit import dialogues -#from tagit.logger import CallbackHandler, logger_config # FIXME: mb/port -#from tagit.uix.kivy.colors import ColorsMarkup # FIXME: mb/port +from tagit import config, dialogues, logger # inner-module imports from .browser import BrowserAwareMixin @@ -94,17 +91,16 @@ class Status(BoxLayout, BrowserAwareMixin, ConfigAwareMixin): logging.getLogger().root.removeHandler(self.handler_history) # status log event - return # FIXME: mb/port - self.handler_status = logger_config( - CallbackHandler(self.status_from_log), - ColorsMarkup, + self.handler_status = logger.logger_config( + logger.CallbackHandler(self.status_from_log), + logger.ColorsMarkup, cfg('ui', 'standalone', 'logging', 'status').to_tree(defaults=True)) logging.getLogger().root.addHandler(self.handler_status) # history (console) - self.handler_history = logger_config( - CallbackHandler(self.update_history), - ColorsMarkup, + self.handler_history = logger.logger_config( + logger.CallbackHandler(self.update_history), + logger.ColorsMarkup, cfg('ui', 'standalone', 'logging', 'console').to_tree(defaults=True)) logging.getLogger().root.addHandler(self.handler_history) |