Source code for bibolamazi.core.blogger

# -*- coding: utf-8 -*-
################################################################################
#                                                                              #
#   This file is part of the Bibolamazi Project.                               #
#   Copyright (C) 2013 by Philippe Faist                                       #
#   philippe.faist@bluewin.ch                                                  #
#                                                                              #
#   Bibolamazi is free software: you can redistribute it and/or modify         #
#   it under the terms of the GNU General Public License as published by       #
#   the Free Software Foundation, either version 3 of the License, or          #
#   (at your option) any later version.                                        #
#                                                                              #
#   Bibolamazi is distributed in the hope that it will be useful,              #
#   but WITHOUT ANY WARRANTY; without even the implied warranty of             #
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
#   GNU General Public License for more details.                               #
#                                                                              #
#   You should have received a copy of the GNU General Public License          #
#   along with Bibolamazi.  If not, see <http://www.gnu.org/licenses/>.        #
#                                                                              #
################################################################################

"""
Set up a logging framework for logging debug, information, warning and error
messages.

Modules should get their logger using Python's standard :py:mod:`logging`
mechanism::

    import logging
    logger = logging.getLogger(__name__)

This allows for the user to be rather specific about which type of messages
she/he would like to see.
"""

#from urllib.parse import urlparse, urlencode
#from urllib.request import urlopen
#from urllib.error import HTTPError


import os
import sys
import logging


# NOTE: This file is imported from bibolamazi.init! DO NOT IMPORT ANY OTHER
# BIBOLAMAZI MODULES!
#
# IMPORTANT: DO NOT IMPORT ANY PYBTEX MODULES! They still need to be patched.
#
#import bibolamazi.init


# ------------------------------------------------------------------------------

# New Level: LONGDEBUG

# note: DEBUG=10, INFO=20, WARNING=30 etc.
LONGDEBUG = 5
logging.addLevelName(LONGDEBUG, "LONGDEBUG")



# ------------------------------------------------------------------------------

# our Logger class

[docs]class BibolamaziLogger(logging.getLoggerClass()): """ A Logger used in Bibolamazi. This logger class knows about an additional log level, :py:const:`LONGDEBUG`. """
[docs] def getSelfLevel(self): """ Returns the level that was set on this logger. If no specific level was set, then returns `logging.NOTSET`. In this respect, this is NOT the same as `getEffectiveLevel()`. """ return self.level # hacked from logging.py source... better way?
[docs] def longdebug(self, msg, *args, **kwargs): """ Produce a log message at level LONGDEBUG. """ if self.isEnabledFor(LONGDEBUG): # Yes, _log takes its args as 'args', not '*args' self._log(LONGDEBUG, msg, args, **kwargs)
logging.setLoggerClass(BibolamaziLogger) # ============================================================================== # LOG FORMATTER. # ============================================================================== _ttycolors = { # 31 == red, 32 == green, 33 == yellow, 34 == blue, 35 == magenta, 36 == cyan # 40 == black bg LONGDEBUG: ('', '', ''), logging.DEBUG: ('', '', ''), logging.INFO: ('\033[0m', '\033[1m', '\033[0m'), logging.WARNING: ('\033[35m', '\033[35;1m', '\033[0m'), # 33 == yellow, 35 == magenta logging.ERROR: ('\033[31m', '\033[31;1m', '\033[0m'), # 31 == red logging.CRITICAL: ('\033[31m', '\033[31;1m', '\033[0m'), }
[docs]class BibolamaziConsoleFormatter(logging.Formatter): """ Format log messages for console output. Customized for bibolamazi. """ def __init__(self, ttycolors=False, show_pos_info_level=None, **kwargs): self.ttycolors = ttycolors self.show_pos_info_level = show_pos_info_level return super().__init__(**kwargs)
[docs] def setShowPosInfoLevel(self, level): self.show_pos_info_level = level
[docs] def format(self, record): # # taken from logging.Formatter.format() source code # message = record.getMessage() head = "" indent = ' '*4 #ttycolorheadstart = '' #ttycolorstart = '' #ttycolorend = '' if self.ttycolors: thettycolorheadstart = lambda : _ttycolors.get(record.levelno, ('','',''))[0] thettycolorstart = lambda : _ttycolors.get(record.levelno, ('','',''))[1] thettycolorend = lambda : _ttycolors.get(record.levelno, ('','',''))[2] else: thettycolorheadstart = lambda : '' thettycolorstart = lambda : '' thettycolorend = lambda : '' if self.show_pos_info_level is not None and record.levelno <= self.show_pos_info_level: head = '[%s l.%d]: %s():\n'%(record.name, record.lineno, record.funcName) if record.levelno == logging.CRITICAL: #head += 'CRITICAL: ' # so that 'CRITICAL' also gets the message tty color, not the head color message = 'CRITICAL: '+message #ttycolorheadstart = '\033[31m' #ttycolorstart = '\033[31;1m' #ttycolorend = '\033[0m' elif record.levelno == logging.ERROR: #head += 'ERROR: ' # so that 'ERROR' also gets the message tty color, not the head color message = 'ERROR: '+message #ttycolorheadstart = '\033[31m' #ttycolorstart = '\033[31;1m' #ttycolorend = '\033[0m' elif record.levelno == logging.WARNING: #head += 'WARNING: ' message = 'WARNING: '+message # 35 == magenta # 33 == yellow elif record.levelno == logging.INFO: indent = '' # 32 == green #ttycolorheadstart = '\033[32m' #ttycolorstart = '\033[32;1m' #ttycolorend = '\033[0m' elif record.levelno == logging.DEBUG: #asctime = self.formatTime(record, self.datefmt) indent = '-- '+ ' '*2 head = '-- '+head.replace('\n', '\n'+indent) elif record.levelno == LONGDEBUG: #asctime = self.formatTime(record, self.datefmt) indent = ' -- '+ ' '*2 head = ' -- '+head.replace('\n', '\n'+indent) msg = message.replace('\n', '\n'+indent) s = thettycolorheadstart() + head + thettycolorend() + thettycolorstart() + msg if record.exc_info: # Cache the traceback text to avoid converting it multiple times # (it's constant anyway) if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: s = s.rstrip() + "\n"+indent exc_text = record.exc_text.replace('\n', '\n'+indent) try: s = s + exc_text except UnicodeError: # Sometimes filenames have non-ASCII chars, which can lead # to errors when s is Unicode and record.exc_text is str # See issue 8924. # We also use replace for when there are multiple # encodings, e.g. UTF-8 for the filesystem and latin-1 # for a script. See issue 13232. s = s + exc_text.decode(sys.getfilesystemencoding(), 'replace') s += thettycolorend() return s
# # Since 2.1, we don't use this ConditionalFormatter, but our new # BibolamaziConsoleFormatter instaad. # # We'll keep this for convenience, and also because the GUI uses it. #
[docs]class ConditionalFormatter(logging.Formatter): """ A formatter class. Very much like logging.Formatter, except that different formats can be specified for different log levels. Specify the different formats to the constructor with keyword arguments. E.g.:: ConditionalFormatter('%(message)s', DEBUG='DEBUG: %(message)s', INFO='just some info... %(message)s') This will use `'%(message)s'` as format for all messages except with level other thand DEBUG or INFO, for which their respective formats are used. """ def __init__(self, defaultfmt=None, datefmt=None, **kwargs): self.default_format = defaultfmt self.special_formats = kwargs return logging.Formatter.__init__(self, defaultfmt, datefmt)
[docs] def format(self, record): if (record.levelname in self.special_formats): return self.do_format(record, self.special_formats[record.levelname]) return self.do_format(record, self.default_format)
[docs] def do_format(self, record, fmt): # # taken from logging.Formatter.format() source code # record.message = record.getMessage() if self.usesTime(): record.asctime = self.formatTime(record, self.datefmt) u = dict([(k,v) for k,v in record.__dict__.items()]) msg = record.message if callable(fmt): u['message'] = msg s = fmt(u) else: s = [] for line in msg.split('\n'): u['message'] = line s.append(fmt % u) s = '\n'.join(s) if record.exc_info: # Cache the traceback text to avoid converting it multiple times # (it's constant anyway) if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: if s[-1:] != "\n": s = s + "\n" s = s + record.exc_text return s
# DEBUG/LOGGING # create logger logger = logging.getLogger('bibolamazi.old_logger') """ (OBSOLETE) The main logger object. This is a :py:class:`logging.Logger` object. .. deprecated:: 2.1 This object is still here to keep old code functioning. New code should use the following idiom somewhere at the top of their module:: import logging logger = logging.getLogger(__name__) (Just make sure the logging mechanism has been set up correctly already, see doc for :py:mod:`~core.blogger` module.) This object has an additional method `longdebug()` (which behaves similarly to `debug()`), for logging long debug output such as dumping the database during intermediate steps, etc. This corresponds to bibolamazi command-line verbosity level 3. """ _simple_console_logging_setup_done = False
[docs]def setup_simple_console_logging(logger=logging.getLogger(), stream=sys.stderr, level=None, capture_warnings=True): """ Sets up the given logger object for simple console output. The main program module may for example invoke this function on the root logger to provide a basic logging mechanism. """ global _simple_console_logging_setup_done if _simple_console_logging_setup_done: print("Simple console logging already set up!!") logger.warning("Simple console logging already set up!") return _simple_console_logging_setup_done = True # create console handler ch = logging.StreamHandler(stream) #ch.setLevel(logging.NOTSET) # propagate all messages # create formatter and add it to the handlers ttycolors = None if sys.platform.startswith('win32'): ttycolors = False else: s_ttycolors = os.environ.get("BIBOLAMAZI_TTY_COLORS", None) if s_ttycolors is not None: sval = s_ttycolors.strip().lower() if sval in ['yes', '1']: ttycolors = True elif sval in ['no', '0']: ttycolors = False elif sval in [ 'auto' ]: ttycolors = None else: sys.stderr.write("Warning: Can't parse value of env['BIBOLAMAZI_TTY_COLORS']. " "Expected 'yes', 'no', or 'auto'") if ttycolors is None: ttycolors = stream.isatty() # instance of our personalized log messages formatter formatter = BibolamaziConsoleFormatter(ttycolors=ttycolors) ch.setFormatter(formatter) # add the handlers to the logger logger.addHandler(ch) # for accessing the bibolamazi formatter. This is so that the main module # can set the level at which the logrecord position info (module, line no, # function) is reproduced logger.bibolamazi_formatter = formatter if level is not None: logger.setLevel(level) if capture_warnings: logging.captureWarnings(True)
# ------------------------------------------------------------------------------ # utility: enum_class for a log level # Avoid cyclic imports!!!! Don't do this here, this module is imported from bibolamazi.init #from bibolamazi.core.bibfilter.argtypes import enum_class # LogLevel = enum_class('LogLevel', # [('CRITICAL', logging.CRITICAL), # ('ERROR', logging.ERROR), # ('WARNING', logging.WARNING), # ('INFO', logging.INFO), # ('DEBUG', logging.DEBUG), # ('LONGDEBUG', LONGDEBUG)], # default_value='INFO', # value_attr_name='levelno')