aboutsummaryrefslogtreecommitdiffstats
path: root/tagit
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-01-25 17:08:32 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-01-25 17:08:32 +0100
commitbb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2 (patch)
tree453fd60b926c37cceff91e8107ddd06867cfcd0e /tagit
parent41fdbe254cbfc74896080b9f5820f856067da537 (diff)
downloadtagit-bb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2.tar.gz
tagit-bb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2.tar.bz2
tagit-bb8f0bfa26da38698fb0c9c04650c5c9a0aa66f2.zip
logging and status
Diffstat (limited to 'tagit')
-rw-r--r--tagit/__init__.py22
-rw-r--r--tagit/logger/__init__.py15
-rw-r--r--tagit/logger/colors.py129
-rw-r--r--tagit/logger/loader.py57
-rw-r--r--tagit/logger/logger.py190
-rw-r--r--tagit/widgets/status.py18
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 = [
+ ('&', '&amp;'),
+ ('[', '&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)