Completed
Pull Request — master (#2409)
by
unknown
01:54
created

LogPrinter.log_exception()   B

Complexity

Conditions 2

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 36
rs 8.8571
c 0
b 0
f 0
1
import sys
2
import traceback
3
import logging
4
from colorama import Style
5
6
from pyprint.Printer import Printer
7
from pyprint.ColorPrinter import ColorPrinter
8
9
from coalib.output.printers.LOG_LEVEL import LOG_LEVEL
10
from coalib.output.printers.LOG_LEVEL import LOG_LEVEL_COLORS
11
from coalib.processes.communication.LogMessage import LogMessage
12
13
14
class PrinterHandler(logging.StreamHandler):
15
    """
16
    Handler for logging which uses printer to print messages and falls back to
17
    writing to stdout if no printer is provided.
18
    """
19
20
    def __init__(self, printer=None, *args, **kwargs):
21
        """
22
        :param printer:  Printer object which will be used for printing
23
                         messages.
24
        """
25
        self.printer = printer
26
        super().__init__(*args, **kwargs)
27
28
    def emit(self, record):
29
        msg = self.format(record)
30
        if self.printer:
31
            self.printer.print(msg)
32
        else:
33
            sys.stdout.write(msg + "\n")
34
35
36
class ColoredFormatter(logging.Formatter):
37
    """
38
    Formatter for logging which adds colors to the output by inserting special
39
    symbols where appropriate.
40
    """
41
42
    def __init__(self, *args, **kwargs):
43
        """
44
        :param use_color:  If True highlights records levelname with respective
45
                           color.
46
        """
47
        self.use_color = kwargs.pop('use_color', False)
48
        super().__init__(*args, **kwargs)
49
50
    def format(self, record, *args, **kwargs):
51
        if record.levelno in LOG_LEVEL_COLORS and self.use_color:
52
            color_begin = LOG_LEVEL_COLORS[record.levelno]
53
            color_end = Style.RESET_ALL
54
        else:
55
            color_begin = color_end = ''
56
        record.levelname = '{cb}[{levelname}]{ce}'.format(
57
            cb=color_begin,
58
            ce=color_end,
59
            levelname=record.levelname)
60
        return super().format(record, *args, **kwargs)
61
62
63
class LogPrinter:
64
    """
65
    The LogPrinter class allows to print log messages to an underlying Printer.
66
67
    This class is an adapter, means you can create a LogPrinter from every
68
    existing Printer instance.
69
    """
70
    _loggers = {}
71
72
    @staticmethod
73
    def _get_logger(printer, logger_name, time_fmt=None):
74
        """
75
        Returns existing or newly created and configured logger.
76
        :param logger_name: name of the logger to be returned.
77
        :param printer:     printer to initialize logger with if it doesn't
78
                            exist, yet.
79
        :param time_fmt:    format to be used for time output. No time is
80
                            pinter if None.
81
        """
82
        if not LogPrinter._loggers.get(logger_name):
83
            if time_fmt:
84
                format_str = '%(levelname)s[%(asctime)s] %(message)s'
85
            else:
86
                format_str = '%(levelname)s %(message)s'
87
88
            use_color = isinstance(printer, ColorPrinter)
89
            formatter = ColoredFormatter(format_str,
90
                                         time_fmt,
91
                                         use_color=use_color)
92
93
            handler = PrinterHandler(printer)
94
            handler.setFormatter(formatter)
95
96
            logger = logging.getLogger(logger_name)
97
            logger.addHandler(handler)
98
99
            LogPrinter._loggers[logger_name] = logger
100
        return LogPrinter._loggers[logger_name]
101
102
    def __init__(self,
103
                 printer=None,
104
                 log_level=LOG_LEVEL.INFO,
105
                 timestamp_format="%X"):
106
        """
107
        Creates a new log printer from an existing Printer or returns existing
108
        one if it already exist.
109
        Note: log_level changes on every logger instantiation.
110
111
        :param printer:          The underlying Printer where log messages
112
                                 shall be written to. If you inherit from
113
                                 Printer, set it to self.
114
        :param log_level:        The minimum log level, everything below will
115
                                 not be logged.
116
        :param timestamp_format: The format string for the
117
                                 datetime.today().strftime(format) method.
118
        """
119
        self.logger, self.logger_name = LogPrinter.getLogger(
120
            printer, timestamp_format)
121
122
        self._printer = printer
123
        self.log_level = log_level
124
        self.timestamp_format = timestamp_format
125
126
    @staticmethod
127
    def getLogger(printer=None, time_fmt=True):
128
        """
129
        Returns existing or new logger with appropriate printer.
130
131
        If printer is string: returns logger with name 'coala.<printer>'
132
        which may not be configured accordingly.
133
134
        If printer is Printer returns logger with name
135
        'coala.<str(type(printer))>' which may use different printer, but of
136
        the same type.
137
138
        If printer is None: returns logger with name 'coala'.
139
140
        :param printer:    Printer instance or string. Ignored if logger
141
                           already exists.
142
        :param time_fmt:   Date format to use in logger. Ignored if logger
143
                           already exists.
144
        :return:           logger from logging module.
145
        """
146
        if printer is None:
147
            return logging.getLogger('coala'), 'coala'
148
        if isinstance(printer, str):
149
            return logging.getLogger(printer), printer
150
        if not isinstance(printer, Printer):
151
            raise TypeError(
152
                "printer should either be str or Printer."
153
                " Received {} instead.".format(type(printer)))
154
        logger_name = "coala.{}".format(type(printer))
155
        logger = LogPrinter._get_logger(printer, logger_name, time_fmt)
156
        return logger, logger_name
157
158
    @property
159
    def log_level(self):
160
        """
161
        Returns current log_level used is logger.
162
        """
163
        return self._log_level
164
165
    @log_level.setter
166
    def log_level(self, log_level):
167
        """
168
        Sets log_level for logger.
169
        """
170
        self._log_level = log_level
171
        self.logger.setLevel(log_level)
172
173
    @property
174
    def printer(self):
175
        """
176
        Returns the underlying printer where logs are printed to.
177
        """
178
        return self._printer
179
180
    def debug(self, *messages, delimiter=" ", timestamp=None, **kwargs):
181
        self.log_message(LogMessage(LOG_LEVEL.DEBUG,
182
                                    *messages,
183
                                    delimiter=delimiter,
184
                                    timestamp=timestamp),
185
                         **kwargs)
186
187
    def info(self, *messages, delimiter=" ", timestamp=None, **kwargs):
188
        self.log_message(LogMessage(LOG_LEVEL.INFO,
189
                                    *messages,
190
                                    delimiter=delimiter,
191
                                    timestamp=timestamp),
192
                         **kwargs)
193
194
    def warn(self, *messages, delimiter=" ", timestamp=None, **kwargs):
195
        self.log_message(LogMessage(LOG_LEVEL.WARNING,
196
                                    *messages,
197
                                    delimiter=delimiter,
198
                                    timestamp=timestamp),
199
                         **kwargs)
200
201
    def err(self, *messages, delimiter=" ", timestamp=None, **kwargs):
202
        self.log_message(LogMessage(LOG_LEVEL.ERROR,
203
                                    *messages,
204
                                    delimiter=delimiter,
205
                                    timestamp=timestamp),
206
                         **kwargs)
207
208
    def log(self, log_level, message, timestamp=None, **kwargs):
209
        self.log_message(LogMessage(log_level,
210
                                    message,
211
                                    timestamp=timestamp),
212
                         **kwargs)
213
214
    def log_exception(self,
215
                      message,
216
                      exception,
217
                      log_level=LOG_LEVEL.ERROR,
218
                      timestamp=None,
219
                      **kwargs):
220
        """
221
        If the log_level of the printer is greater than DEBUG, it prints
222
        only the message. If it is DEBUG or lower, it shows the message
223
        along with the traceback of the exception.
224
225
        :param message:   The message to print.
226
        :param exception: The exception to print.
227
        :param log_level: The log_level of this message (not used when
228
                          logging the traceback. Tracebacks always have
229
                          a level of DEBUG).
230
        :param timestamp: The time at which this log occured. Defaults to
231
                          the current time.
232
        :param kwargs:    Keyword arguments to be passed when logging the
233
                          message (not used when logging the traceback).
234
        """
235
        if not isinstance(exception, BaseException):
236
            raise TypeError("log_exception can only log derivatives of "
237
                            "BaseException.")
238
239
        traceback_str = "\n".join(
240
            traceback.format_exception(type(exception),
241
                                       exception,
242
                                       exception.__traceback__))
243
244
        self.log(log_level, message, timestamp=timestamp, **kwargs)
245
        self.log_message(
246
            LogMessage(LOG_LEVEL.DEBUG,
247
                       "Exception was:" + "\n" + traceback_str,
248
                       timestamp=timestamp),
249
            **kwargs)
250
251
    def log_message(self, log_message, **kwargs):
252
        if not isinstance(log_message, LogMessage):
253
            raise TypeError("log_message should be of type LogMessage.")
254
        self.logger.log(log_message.log_level, log_message.message)
255
256
    def __getstate__(self):
257
        # on Windows there are problems with serializing loggers, so omit it
258
        oldict = self.__dict__.copy()
259
        del oldict['logger']
260
        return oldict
261
262
    def __setstate__(self, newdict):
263
        self.__dict__.update(newdict)
264
        # restore logger by name
265
        self.logger = logging.getLogger(self.logger_name)
266