From 04166a184ed8507aff117d8d40a3db5b00757d55 Mon Sep 17 00:00:00 2001 From: Tobias Gruetzmacher Date: Sun, 12 May 2024 12:20:12 +0200 Subject: [PATCH] WIP --- dosagelib/__init__.py | 4 +-- dosagelib/cmd.py | 12 ++++---- dosagelib/comic.py | 3 +- dosagelib/director.py | 9 ++---- dosagelib/logging.py | 65 +++++++++++++++++++++++++++++++++++++++++++ dosagelib/output.py | 15 +++++++--- dosagelib/scraper.py | 19 ++++--------- pyproject.toml | 1 + 8 files changed, 93 insertions(+), 35 deletions(-) create mode 100644 dosagelib/logging.py diff --git a/dosagelib/__init__.py b/dosagelib/__init__.py index 39ba360332..f5b8090442 100644 --- a/dosagelib/__init__.py +++ b/dosagelib/__init__.py @@ -23,7 +23,5 @@ __version__ = version(AppName) # PEP 396 except PackageNotFoundError: # package is not installed - out.warn('{} is not installed, no version available.' - ' Use at least {!r} or {!r} to fix this.'.format( - AppName, 'pip install -e .', 'setup.py egg_info')) + out.warn('{} is not installed, no version available.' ' Use at least {!r} or {!r} to fix this.'.format( AppName, 'pip install -e .', 'setup.py egg_info')) __version__ = 'ERR.NOT.INSTALLED' diff --git a/dosagelib/cmd.py b/dosagelib/cmd.py index 86a16d7124..19cbca4dbf 100644 --- a/dosagelib/cmd.py +++ b/dosagelib/cmd.py @@ -14,7 +14,7 @@ from platformdirs import PlatformDirs -from . import events, configuration, singleton, director +from . import configuration, director, events, singleton, logging from . import AppName, __version__ from .output import out from .scraper import scrapers as scrapercache @@ -230,6 +230,7 @@ def vote_comic(scraperobj): def run(options): """Execute comic commands.""" set_output_info(options) + logging.setup_console(options.verbose, options.timestamps) scrapercache.adddir(user_plugin_path) # ensure only one instance of dosage is running if not options.allow_multiple: @@ -256,8 +257,7 @@ def do_list(column_list=True, verbose=False, listall=False): """List available comics.""" with out.pager(): out.info(u'Available comic scrapers:') - out.info(u'Comics tagged with [{}] require age confirmation' - ' with the --adult option.'.format(TAG_ADULT)) + out.info(u'Comics tagged with [{}] require age confirmation' ' with the --adult option.'.format(TAG_ADULT)) out.info(u'Non-english comics are tagged with [%s].' % TAG_LANG) scrapers = sorted(scrapercache.all(listall), key=lambda s: s.name.lower()) @@ -268,8 +268,7 @@ def do_list(column_list=True, verbose=False, listall=False): out.info(u'%d supported comics.' % num) if disabled: out.info('') - out.info(u'Some comics are disabled, they are tagged with' - ' [{}:REASON], where REASON is one of:'.format(TAG_DISABLED)) + out.info(u'Some comics are disabled, they are tagged with' ' [{}:REASON], where REASON is one of:'.format(TAG_DISABLED)) for k in disabled: out.info(u' %-10s %s' % (k, disabled[k])) return 0 @@ -298,8 +297,7 @@ def do_column_list(scrapers): maxlen = max(len(name) for name in names) names_per_line = max(width // (maxlen + 1), 1) while names: - out.info(u''.join(name.ljust(maxlen) for name in - names[:names_per_line])) + out.info(u''.join(name.ljust(maxlen) for name in names[:names_per_line])) del names[:names_per_line] return num, disabled diff --git a/dosagelib/comic.py b/dosagelib/comic.py index 222549e14c..832ad08916 100644 --- a/dosagelib/comic.py +++ b/dosagelib/comic.py @@ -87,8 +87,7 @@ def connect(self, lastchange=None): if maintype == 'image': self.ext = '.' + subtype.replace('jpeg', 'jpg') self.contentLength = int(self.urlobj.headers.get('content-length', 0)) - out.debug(u'... filename = %r, ext = %r, contentLength = %d' % ( - self.filename, self.ext, self.contentLength)) + out.debug(u'... filename = %r, ext = %r, contentLength = %d' % (self.filename, self.ext, self.contentLength)) def save(self, basepath): """Save comic URL to filename on disk.""" diff --git a/dosagelib/director.py b/dosagelib/director.py index 225e8de3c1..5167ad86d0 100644 --- a/dosagelib/director.py +++ b/dosagelib/director.py @@ -139,8 +139,7 @@ def saveComicStrip(self, strip): if self.stopped: break except Exception as msg: - out.exception('Could not save image at {} to {}: {!r}'.format( - image.referrer, image.filename, msg)) + out.exception('Could not save image at {} to {}: {!r}'.format(image.referrer, image.filename, msg)) self.errors += 1 return allskipped @@ -245,11 +244,9 @@ def shouldRunScraper(scraperobj, adult=True, listing=False): def warn_adult(scraperobj): """Print warning about adult content.""" - out.warn(u"skipping adult comic {};" - " use the --adult option to confirm your age".format(scraperobj.name)) + out.warn(u"skipping adult comic {};" " use the --adult option to confirm your age".format(scraperobj.name)) def warn_disabled(scraperobj, reasons): """Print warning about disabled comic modules.""" - out.warn(u"Skipping comic {}: {}".format( - scraperobj.name, ' '.join(reasons.values()))) + out.warn(u"Skipping comic {}: {}".format( scraperobj.name, ' '.join(reasons.values()))) diff --git a/dosagelib/logging.py b/dosagelib/logging.py new file mode 100644 index 0000000000..f40f0809c6 --- /dev/null +++ b/dosagelib/logging.py @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: © 2024 Tobias Gruetzmacher +""" +Helpers for logging. +""" +import logging + +from rich.console import ConsoleRenderable +from rich.logging import RichHandler +from rich.text import Text + + +def setup_console(level: int, timestamps: bool) -> None: + """ + Configure Python logging for simple Dosage console output. This tries to + emulate Dosage's legacy output style as much as possible. + + Levels work roughly like this: + 0 - The default, with shortened exception logging + 1 - Enables verbose exception logging + 2 - Enables DEBUG logging + 3 - Enables TRACE logging + """ + logging.basicConfig( + level=translate_level(level), + format="%(threadName)s> %(message)s", + datefmt="%X", + handlers=[DosageRichHandler(show_time=timestamps)] + ) + +def translate_level(level: int) -> int: + if level < 2: + return logging.INFO + if level == 2: + return logging.DEBUG + return logging.NOTSET + + +class DosageRichHandler(RichHandler): + def __init__(self, + show_time: bool = True) -> None: + super().__init__(show_level=False, show_time=show_time, show_path=False) + + def render_message(self, record: logging.LogRecord, message: str) -> ConsoleRenderable: + if record.levelno > logging.INFO: + message = f"{record.levelname.upper()}: {message}" + style = f"logging.level.{record.levelname.lower()}" + else: + style = '' + + use_markup = getattr(record, "markup", self.markup) + message_text = Text.from_markup(message, style=style) if use_markup else Text(message, + style=style) + + highlighter = getattr(record, "highlighter", self.highlighter) + if highlighter: + message_text = highlighter(message_text) + + if self.keywords is None: + self.keywords = self.KEYWORDS + + if self.keywords: + message_text.highlight_words(self.keywords, "logging.keyword") + + return message_text diff --git a/dosagelib/output.py b/dosagelib/output.py index 4e14218ee8..71b01e777f 100644 --- a/dosagelib/output.py +++ b/dosagelib/output.py @@ -1,10 +1,11 @@ # SPDX-License-Identifier: MIT -# Copyright (C) 2004-2008 Tristan Seligmann and Jonathan Jacobs -# Copyright (C) 2012-2014 Bastian Kleineidam -# Copyright (C) 2015-2020 Tobias Gruetzmacher +# SPDX-FileCopyrightText: © 2004 Tristan Seligmann and Jonathan Jacobs +# SPDX-FileCopyrightText: © 2012 Bastian Kleineidam +# SPDX-FileCopyrightText: © 2015 Tobias Gruetzmacher import codecs import contextlib import io +import logging import os import pydoc import sys @@ -18,6 +19,7 @@ from colorama import Fore, Style +logger = logging.getLogger(__name__) lock = threading.Lock() @@ -26,7 +28,7 @@ def get_threadname(): return threading.current_thread().name -class Output(object): +class Output: """Print output with context, indentation and optional timestamps.""" DEFAULT_WIDTH = 80 @@ -55,20 +57,24 @@ def __init__(self, stream=None): def info(self, s, level=0): """Write an informational message.""" self.write(s, level=level) + logger.info(s) def debug(self, s, level=2): """Write a debug message.""" # "white" is the default color for most terminals... self.write(s, level=level, color=Fore.WHITE) + logger.debug(s) def warn(self, s, level=0): """Write a warning message.""" self.write(u"WARN: %s" % s, level=level, color=Style.BRIGHT + Fore.YELLOW) + logger.warning(s) def error(self, s, level=0): """Write an error message.""" self.write(u"ERROR: %s" % s, level=level, color=Style.DIM + Fore.RED) + logger.error(s) def exception(self, s): """Write error message with traceback info.""" @@ -77,6 +83,7 @@ def exception(self, s): self.writelines(traceback.format_stack(), 1) self.writelines(traceback.format_tb(tb)[1:], 1) self.writelines(traceback.format_exception_only(type, value), 1) + logger.exeception(s) def write(self, s, level=0, color=None): """Write message with indentation, context and optional timestamp.""" diff --git a/dosagelib/scraper.py b/dosagelib/scraper.py index b0f436744e..f6916ae915 100644 --- a/dosagelib/scraper.py +++ b/dosagelib/scraper.py @@ -143,15 +143,12 @@ def getComicStrip(self, url, data) -> ComicStrip: # remove duplicate URLs urls = uniq(urls) if len(urls) > 1 and not self.multipleImagesPerStrip: - out.warn( - u"Found %d images instead of 1 at %s with expressions %s" % - (len(urls), url, prettyMatcherList(self.imageSearch))) + out.warn( u"Found %d images instead of 1 at %s with expressions %s" % (len(urls), url, prettyMatcherList(self.imageSearch))) image = urls[0] out.warn("Choosing image %s" % image) urls = (image,) elif not urls: - out.warn("Found no images at %s with expressions %s" % (url, - prettyMatcherList(self.imageSearch))) + out.warn("Found no images at %s with expressions %s" % (url, prettyMatcherList(self.imageSearch))) if self.textSearch: text = self.fetchText(url, data, self.textSearch, optional=self.textOptional) @@ -407,8 +404,7 @@ def fetchUrls(self, url, data, urlSearch): if not searchUrl: raise ValueError("Pattern %s matched empty URL at %s." % (search.pattern, url)) - out.debug(u'matched URL %r with pattern %s' % - (searchUrl, search.pattern)) + out.debug(u'matched URL %r with pattern %s' % (searchUrl, search.pattern)) searchUrls.append(normaliseURL(urljoin(data[1], searchUrl))) if searchUrls: # do not search other links if one pattern matched @@ -425,8 +421,7 @@ def fetchText(self, url, data, textSearch, optional): match = textSearch.search(data[0]) if match: text = match.group(1) - out.debug(u'matched text %r with pattern %s' % - (text, textSearch.pattern)) + out.debug(u'matched text %r with pattern %s' % (text, textSearch.pattern)) return html.unescape(text).strip() if optional: return None @@ -593,8 +588,7 @@ def load(self) -> None: modules += 1 classes += self.addmodule(module) self.validate() - out.debug("... %d scrapers loaded from %d classes in %d modules." % ( - len(self.data), classes, modules)) + out.debug("... %d scrapers loaded from %d classes in %d modules." % ( len(self.data), classes, modules)) def adddir(self, path) -> None: """Add an additional directory with python modules to the scraper list. @@ -613,8 +607,7 @@ def adddir(self, path) -> None: self.validate() self.userdirs.add(path) if classes > 0: - out.debug("Added %d user classes from %d modules." % ( - classes, modules)) + out.debug("Added %d user classes from %d modules." % ( classes, modules)) def addmodule(self, module) -> int: """Adds all valid plugin classes from the specified module to the cache. diff --git a/pyproject.toml b/pyproject.toml index 10c294f4ad..e2bdbf06dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "lxml>=4.0.0", "platformdirs", "requests>=2.0", + "rich", "importlib_resources>=5.0.0;python_version<'3.9'", ] dynamic = ["version"]