Comprehensively log all version information

Manuskript now logs the versions of modules and libraries powering them
for as far those are easily accessible. This includes all the optional
modules, too. None of this is visible on the terminal of course - unless
Manuskript is run with the --verbose flag. This clears up the last bit
of unnecessary console spam, leaving our users blissfully unaware.

Until we (and/or Qt) break something again, that is...
This commit is contained in:
Jan Wester 2019-10-14 20:16:07 +02:00
parent 37becdf80a
commit 239e66e7cb
3 changed files with 118 additions and 12 deletions

View file

@ -4,15 +4,17 @@
# standard python `logging` module, this module will take care of specific
# manuskript needs to keep it separate from the rest of the logic.
from manuskript.functions import writablePath
import os
import logging
from manuskript.functions import writablePath
from importlib import import_module
LOGGER = logging.getLogger(__name__)
LOGFORMAT_CONSOLE = "%(levelname)s> %(message)s"
LOGFORMAT_FILE = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
logger = logging.getLogger(__name__)
def setUp(console_level=logging.WARN):
"""Sets up a convenient environment for logging.
@ -37,7 +39,7 @@ def setUp(console_level=logging.WARN):
ch.setFormatter(logging.Formatter(LOGFORMAT_CONSOLE))
root_logger.addHandler(ch)
logger.debug("Logging to STDERR.")
LOGGER.debug("Logging to STDERR.")
def logToFile(file_level=logging.DEBUG, logfile=None):
@ -65,9 +67,9 @@ def logToFile(file_level=logging.DEBUG, logfile=None):
root_logger.addHandler(fh)
# Use INFO level to make it easier to find for users.
logger.info("Logging to file: %s", logfile)
LOGGER.info("Logging to file: %s", logfile)
except Exception as ex:
logger.warning("Cannot log to file '%s'. Reason: %s", logfile, ex)
LOGGER.warning("Cannot log to file '%s'. Reason: %s", logfile, ex)
# Qt has its own logging facility that we would like to integrate into our own.
@ -103,4 +105,110 @@ def integrateQtLogging():
# I also feel a lot safer this way. Qt is a curse that just keeps
# on giving, even when it isn't actually at fault. I hate you, Qt.
qInstallMessageHandler(qtMessageHandler)
qInstallMessageHandler(qtMessageHandler)
def versionTupleToString(t):
"""A bit of generic tuple conversion code that hopefully handles all the
different sorts of tuples we may come across while logging versions.
None -> "N/A"
(,) -> "N/A"
(2, 4, 6) -> "2.4.6"
(2, 4, "alpha", 8) -> "2.4-alpha.8"
"""
s = []
if t is None or len(t) == 0:
return "N/A"
else:
s.append(str(t[0]))
def version_chunk(v):
if isinstance(v, str):
return "-", str(v)
else:
return ".", str(v)
s.extend(f for p in t[1:] for f in version_chunk(p))
return "".join(s)
def attributesFromOptionalModule(module, *attributes):
"""It is nice to cut down on the try-except boilerplate by
putting this logic into its own function.
Returns as many values as there are attributes.
A value will be None if it failed to get the attribute."""
assert(len(attributes) != 0)
v = []
try:
m = import_module(module)
for a in attributes:
v.append(getattr(m, a, None))
except ImportError:
v.extend(None for _ in range(len(attributes)))
if len(v) == 1:
# Return the value directly so we can use it in an expression.
return v[0]
else:
# The list is consumed as a part of the unpacking syntax.
return v
def logVersionInformation(logger=None):
"""Logs all important runtime information neatly together.
Due to the generic nature, use the manuskript logger by default."""
if not logger:
logger = logging.getLogger("manuskript")
vt2s = versionTupleToString
afom = attributesFromOptionalModule
# Basic system information.
from platform import python_version, platform, processor, machine
logger.info("Operating System: %s", platform())
logger.info("Hardware: %s / %s", machine(), processor())
# Manuskript and Python info.
from manuskript.version import getVersion
logger.info("Manuskript %s (Python %s)", getVersion(), python_version())
# Installed Python packages.
# PyQt + Qt
from PyQt5.Qt import PYQT_VERSION_STR, qVersion
from PyQt5.QtCore import QT_VERSION_STR
logger.info("* PyQt %s (compiled against Qt %s)", PYQT_VERSION_STR, QT_VERSION_STR)
logger.info(" * Qt %s (runtime)", qVersion())
# Lxml
# See: https://lxml.de/FAQ.html#i-think-i-have-found-a-bug-in-lxml-what-should-i-do
from lxml import etree
logger.info("* lxml.etree %s", vt2s(etree.LXML_VERSION))
logger.info(" * libxml %s (compiled: %s)", vt2s(etree.LIBXML_VERSION), vt2s(etree.LIBXML_COMPILED_VERSION))
logger.info(" * libxslt %s (compiled: %s)", vt2s(etree.LIBXSLT_VERSION), vt2s(etree.LIBXSLT_COMPILED_VERSION))
# Spellcheckers. (Optional)
enchant_mod_ver, enchant_lib_ver = afom("enchant", "__version__", "get_enchant_version")
if enchant_lib_ver:
enchant_lib_ver = enchant_lib_ver()
if isinstance(enchant_lib_ver, bytes): # PyEnchant version < 3.0.2
enchant_lib_ver = enchant_lib_ver.decode('utf-8')
logger.info("* pyEnchant %s (libenchant: %s)", enchant_mod_ver or "N/A", enchant_lib_ver or "N/A")
logger.info("* pySpellChecker %s", afom("spellchecker", "__version__") or "N/A")
logger.info("* Symspellpy %s", afom("symspellpy", "__version__") or "N/A")
# Markdown. (Optional)
logger.info("* Markdown %s", afom("markdown", "__version__") or "N/A")
# Web rendering engine
from manuskript.ui.views.webView import webEngine
logger.info("Web rendering engine: %s", webEngine)
# Do not collect version information for Pandoc; that would require
# executing `pandov -v` and parsing the output, all of which is too slow.

View file

@ -7,7 +7,6 @@ import sys
import signal
import manuskript.logging
import manuskript.ui.views.webView
from PyQt5.QtCore import QLocale, QTranslator, QSettings, Qt
from PyQt5.QtGui import QIcon, QColor, QPalette
from PyQt5.QtWidgets import QApplication, qApp, QStyleFactory
@ -34,7 +33,9 @@ def prepare(arguments, tests=False):
# Handle all sorts of Qt logging messages in Python.
manuskript.logging.integrateQtLogging()
LOGGER.info("Running manuskript version {}.".format(getVersion()))
# Log all the versions for less headaches.
manuskript.logging.logVersionInformation()
icon = QIcon()
for i in [16, 32, 64, 128, 256, 512]:
icon.addFile(appPath("icons/Manuskript/icon-{}px.png".format(i)))

View file

@ -22,16 +22,13 @@ else:
if features['qtwebkit']:
from PyQt5.QtWebKitWidgets import QWebView
print("Debug: Web rendering engine used: QWebView")
webEngine = "QtWebKit"
webView = QWebView
elif features['qtwebengine']:
from PyQt5 import QtWebEngineWidgets
print("Debug: Web rendering engine used: QWebEngineView")
webEngine = "QtWebEngine"
webView = QtWebEngineWidgets.QWebEngineView
else:
from PyQt5.QtWidgets import QTextEdit
print("Debug: Web rendering engine used: QTextEdit")
webEngine = "QTextEdit"
webView = QTextEdit