Source code for skidl.logger

# -*- coding: utf-8 -*-

# The MIT License (MIT) - Copyright (c) Dave Vandenbout.

"""
Logging for generic messages and ERC.
"""

from __future__ import (  # isort:skip
    absolute_import,
    division,
    print_function,
    unicode_literals,
)

import logging
import os
import queue
import sys
from builtins import object, super

try:
    from future import standard_library
    standard_library.install_aliases()
except ImportError:
    pass

from .scriptinfo import get_script_name, get_skidl_trace
from .skidlbaseobj import WARNING
from .utilities import export_to_all


__all__ = ["rt_logger", "erc_logger", "active_logger"]


class CountCalls(object):
    """Decorator for counting the number of times a function is called.

    This is used for counting errors and warnings passed to logging functions,
    making it easy to track if and how many errors/warnings were issued.
    """

    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

    def reset(self):
        self.count = 0


class SkidlLogFileHandler(logging.FileHandler):
    """Logger that outputs messages to a file."""

    def __init__(self, *args, **kwargs):
        try:
            self.filename = kwargs["filename"]
        except KeyError:
            self.filename = args[0]
        try:
            super().__init__(*args, **kwargs)
        except PermissionError as e:
            # Prevents future error when removing non-existent log file.
            self.filename = None
            print(e)

    def remove_log_file(self):
        if self.filename:
            # Close file handle before removing file.
            f_name = self.filename
            self.close()
            os.remove(f_name)
        self.filename = None


class SkidlLogger(logging.getLoggerClass()):
    """SKiDL logger that can stop output to log files and delete them."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.log_file_handlers = []
        self.set_trace_depth(0)

    def addHandler(self, handler):
        if isinstance(handler, SkidlLogFileHandler):
            # Store handlers that output to files so they can be accessed later.
            self.log_file_handlers.append(handler)
        super().addHandler(handler)

    def removeHandler(self, handler):
        if handler in self.log_file_handlers:
            # Remove log files when a log file handler is removed.
            handler.remove_log_file()
            # Remove handler from list of log file handlers.
            self.log_file_handlers.remove(handler)
        super().removeHandler(handler)

    def stop_file_output(self):
        """Stop file outputs for all log handlers of this logger."""
        for handler in self.log_file_handlers[:]:
            self.removeHandler(handler)

    def set_trace_depth(self, depth):
        self.trace_depth = depth

    def get_trace(self):
        if self.trace_depth <= 0:
            return ""
        trace = get_skidl_trace()
        start = len(trace) - self.trace_depth
        return " @ [" + "=>".join(trace[start:]) + "]"

    def debug(self, msg, *args, **kwargs):
        super().debug(msg + self.get_trace(), *args, **kwargs)

    def summary(self, msg, *args, **kwargs):
        super().info(msg, *args, **kwargs)

    def info(self, msg, *args, **kwargs):
        super().info(msg + self.get_trace(), *args, **kwargs)

    def warning(self, msg, *args, **kwargs):
        super().warning(msg + self.get_trace(), *args, **kwargs)

    def error(self, msg, *args, **kwargs):
        super().error(msg + self.get_trace(), *args, **kwargs)

    def critical(self, msg, *args, **kwargs):
        super().critical(msg + self.get_trace(), *args, **kwargs)

    def raise_(self, exc_class, msg):
        """Issue a logging message and then raise an exception.

        Args:
            exc_class (Exception class): Class of exception to raise.
            msg (string): Error message.

        Raises:
            exc_class: Exception class that is raised after error message is logged.
        """
        self.error(msg)
        raise exc_class(msg)

    def report_summary(self, phase_desc):
        """Report total of logged errors and warnings.

        Args:
            phase_desc (string): description of the phase of operations (e.g. "generating netlist").
        """
        if (self.error.count, self.warning.count) == (0, 0):
            self.summary("No errors or warnings found while {}.\n".format(phase_desc))
        else:
            self.summary(
                "{} warnings found while {}.".format(
                    active_logger.warning.count, phase_desc
                )
            )
            self.summary(
                "{} errors found while {}.\n".format(
                    active_logger.error.count, phase_desc
                )
            )


class ActiveLogger(SkidlLogger):
    """Currently-active logger for a given phase of operations."""

    def __init__(self, logger):
        """Create active logger.

        Args:
            logger (SkidlLogger): Logger that will be used for current phase of operations.
        """
        self.prev_loggers = queue.LifoQueue()
        self.set(logger)

    def set(self, logger):
        """Set the active logger.

        Args:
            logger (SkidlLogger): Logger that will be used for current phase of operations.
        """
        self.current_logger = logger
        self.__dict__.update(self.current_logger.__dict__)

    def push(self, logger):
        """Save the currently active logger and activate the given logger.

        Args:
            logger (SkidlLogger): Logger to be activated.
        """
        self.prev_loggers.put(self.current_logger)
        self.set(logger)

    def pop(self):
        """Re-activate the previously active logger."""
        self.set(self.prev_loggers.get())


def _create_logger(title, log_msg_id="", log_file_suffix=".log"):
    """
    Create a logger, usually for run-time errors or ERC violations.
    """

    logging.setLoggerClass(SkidlLogger)
    logger = logging.getLogger(title)

    # Errors & warnings always appear on the terminal.
    handler = logging.StreamHandler(sys.stderr)
    handler.setLevel(logging.INFO)
    handler.setFormatter(logging.Formatter(log_msg_id + "%(levelname)s: %(message)s"))
    logger.addHandler(handler)

    # Errors and warnings are stored in a log file with the top-level script's name.
    handler = SkidlLogFileHandler(get_script_name() + log_file_suffix, mode="w")
    handler.setLevel(logging.INFO)
    handler.setFormatter(logging.Formatter(log_msg_id + "%(levelname)s: %(message)s"))
    logger.addHandler(handler)

    # Set logger to trigger on info, warning, and error messages.
    logger.setLevel(logging.INFO)

    # Augment the logger's functions to count the number of errors and warnings.
    logger.error = CountCalls(logger.error)
    logger.warning = CountCalls(logger.warning)

    return logger


[docs] @export_to_all def stop_log_file_output(stop=True): """Permanently stop loggers from creating files containing log messages.""" if stop: rt_logger.stop_file_output() erc_logger.stop_file_output()
############################################################################### # Create loggers for runtime messages and ERC reports. rt_logger = _create_logger("skidl") rt_logger.set_trace_depth(2) erc_logger = _create_logger("ERC_Logger", "ERC ", ".erc") erc_logger.set_trace_depth(0) # Create active logger that starts off as the runtime logger. active_logger = ActiveLogger(rt_logger) ###############################################################################