Passed
Push — main ( 57139e...393c8e )
by Douglas
04:05
created

LoggerWithCautionAndNotice.caution()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 4
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
"""
2
Loguru logging extension that, configurably:
3
- redirects built-in logging to your loguru logger
4
- remembers the handlers added
5
- auto-detects compression and serialization from filenames
6
- includes extras in non-serializing handlers
7
- has a few alternative (possibly better) choices for colors, icons, and levels
8
- will complain when you do really stupid things
9
- has a convenient notation for configuring from a CLI
10
  (e.g. ``--stderr debug --log :INFO:run.log.gz``)
11
- mandates utf-8
12
"""
13
from __future__ import annotations
14
15
import abc
16
import logging
17
import os
18
import sys
19
import traceback
20
from collections import deque
21
from dataclasses import dataclass
22
from functools import partialmethod
0 ignored issues
show
Unused Code introduced by
Unused partialmethod imported from functools
Loading history...
23
from inspect import cleandoc
24
from pathlib import Path
25
from typing import (
0 ignored issues
show
Unused Code introduced by
Unused Iterable imported from typing
Loading history...
26
    AbstractSet,
27
    Any,
28
    Callable,
29
    Deque,
30
    Generic,
31
    Iterable,
32
    Mapping,
33
    MutableMapping,
34
    Optional,
35
    Sequence,
36
    TextIO,
37
    Tuple,
38
    Type,
39
    TypeVar,
40
    Union,
41
)
42
43
import loguru._defaults as _defaults
0 ignored issues
show
introduced by
Unable to import 'loguru._defaults'
Loading history...
44
45
# noinspection PyProtectedMember
46
import loguru._logger
0 ignored issues
show
introduced by
Unable to import 'loguru._logger'
Loading history...
47
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
48
from loguru import logger as _logger
0 ignored issues
show
introduced by
Unable to import 'loguru'
Loading history...
49
50
# noinspection PyProtectedMember
51
from loguru._logger import Logger
0 ignored issues
show
introduced by
Unable to import 'loguru._logger'
Loading history...
52
53
from pocketutils.core import PathLike
0 ignored issues
show
introduced by
Cannot import 'pocketutils.core' due to syntax error 'invalid syntax (<unknown>, line 134)'
Loading history...
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
54
from pocketutils.core.exceptions import IllegalStateError, XValueError
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
55
56
_levels = loguru.logger._core.levels
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _core was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
57
Formatter = Union[str, Callable[[Mapping[str, Any]], str]]
58
DEFAULT_FMT_STRING = cleandoc(
59
    r"""
60
    <bold>{time:YYYY-MM-DD HH:mm:ss.SSS} | </bold>
61
    <level>{level: <7}</level><bold> | </bold>
62
    <cyan>({thread.id}){name}</cyan><bold>:</bold>
63
    <cyan>{function}</cyan><bold>:</bold>
64
    <cyan>{line}</cyan><bold> — </bold>
65
    <level>{message}{{EXTRA}}</level>
66
    {exception}
67
    """
68
).replace("\n", "")
69
70
71
class _SENTINEL:
72
    pass
73
74
75
T = TypeVar("T", covariant=True, bound=Logger)
0 ignored issues
show
Coding Style Naming introduced by
Class name "T" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
76
Z = TypeVar("Z", covariant=True, bound=Logger)
0 ignored issues
show
Coding Style Naming introduced by
Class name "Z" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
77
78
79
def _add_traceback(record):
80
    extra = record["extra"]
81
    if extra.get("traceback", False):
82
        extra["traceback"] = "\n" + "".join(traceback.format_stack())
83
    else:
84
        extra["traceback"] = ""
85
86
87
_LOGGER_ARG_PATTERN = regex.compile(r"(?:([a-zA-Z]+):)?(.*)", flags=regex.V1)
88
log_compressions = {
89
    ".xz",
90
    ".lzma",
91
    ".gz",
92
    ".zip",
93
    ".bz2",
94
    ".tar",
95
    ".tar.gz",
96
    ".tar.bz2",
97
    ".tar.xz",
98
}
99
valid_log_suffixes = {
100
    *{f".log{c}" for c in log_compressions},
101
    *{f".txt{c}" for c in log_compressions},
102
    *{f".json{c}" for c in log_compressions},
103
}
104
105
106
class _Defaults:
107
    def wrap_extended_fmt(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
108
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
109
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
110
        fmt: str = DEFAULT_FMT_STRING,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
111
        sep: str = "; ",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
112
        eq_sign: str = " ",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
113
    ) -> Callable[[Mapping[str, Any]], str]:
114
        def FMT(record: Mapping[str, Any]) -> str:
0 ignored issues
show
Coding Style Naming introduced by
Function name "FMT" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
115
            extra = sep.join([e + eq_sign + "{extra[" + e + "]}" for e in record["extra"].keys()])
116
            if len(extra) > 0:
117
                extra = f" [ {extra} ]"
118
            return fmt.replace("{{EXTRA}}", extra) + os.linesep
119
120
        return FMT
121
122
    def wrap_plain_fmt(
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
introduced by
Missing function or method docstring
Loading history...
123
        self, *, fmt: str = DEFAULT_FMT_STRING
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
124
    ) -> Callable[[Mapping[str, Any]], str]:
125
        def FMT(record: Mapping[str, Any]) -> str:
0 ignored issues
show
Unused Code introduced by
The argument record seems to be unused.
Loading history...
Coding Style Naming introduced by
Function name "FMT" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
126
            return fmt.replace("{{EXTRA}}", "") + os.linesep
127
128
        return FMT
129
130
    @property
131
    def levels_current(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
132
        ell = loguru.logger._core.levels
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _core was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
133
        return {e.name: e.no for e in ell.values()}
134
135
    @property
136
    def colors_current(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
137
        ell = loguru.logger._core.levels
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _core was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
138
        return {e.name: e.color for e in ell.values()}
139
140
    @property
141
    def icons_current(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
142
        ell = loguru.logger._core.levels
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _core was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
143
        return {e.name: e.icon for e in ell.values()}
144
145
    @property
146
    def levels_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
147
        return {e.name: e.no for e in _levels.values()}
148
149
    @property
150
    def colors_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
151
        return {e.name: e.color for e in _levels.values()}
152
153
    @property
154
    def icons_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
155
        return {e.name: e.icon for e in _levels.values()}
156
157
    # the levels for caution and notice are DEFINED here
158
    # trace and success must match loguru's
159
    # and the rest must match logging's
160
    # note that most of these alternate between informative and problematic
161
    # i.e. info (ok), caution (bad), success (ok), warning (bad), notice (ok), error (bad)
162
    @property
163
    def levels_extended(self) -> Mapping[str, int]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
164
        ell = loguru.logger._core.levels
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _core was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
165
        levels = {k.name: k.no for k in ell.values()}
166
        levels.setdefault("CAUTION", 23)
167
        levels.setdefault("NOTICE", 37)
168
        return levels
169
170
    @property
171
    def colors_extended(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
172
        ell = loguru.logger._core.levels
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _core was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
173
        colors = {k.name: k.color for k in ell.values()}
174
        colors.setdefault("CAUTION", ell["WARNING"].color)
175
        colors.setdefault("NOTICE", ell["INFO"].color)
176
        return colors
177
178
    @property
179
    def colors_red_green_safe(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
180
        return dict(
181
            TRACE="<dim>",
182
            DEBUG="<dim>",
183
            INFO="<bold>",
184
            CAUTION="<yellow>",
185
            SUCCESS="<blue>",
186
            WARNING="<yellow>",
187
            NOTICE="<blue>",
188
            ERROR="<red>",
189
            CRITICAL="<red>",
190
        )
191
192
    @property
193
    def icons_extended(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
194
        ell = loguru.logger._core.levels
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _core was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
195
        icons = {k.name: k.icon for k in ell.values()}
196
        icons.setdefault("CAUTION", "⚐")
197
        icons.setdefault("NOTICE", "★")
198
        return icons
199
200
    @property
201
    def level(self) -> str:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
202
        return _defaults.LOGURU_LEVEL
203
204
    @property
205
    def fmt_simplified(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
206
        return self.wrap_plain_fmt()
207
208
    @property
209
    def fmt_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
210
        return self.wrap_extended_fmt()
211
212
    @property
213
    def fmt_built_in_raw(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
214
        return _defaults.LOGURU_FORMAT
215
216
    @property
217
    def fmt_extended_raw(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
218
        return DEFAULT_FMT_STRING
219
220
    @property
221
    def aliases(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
222
        return dict(NONE=None, NO=None, OFF=None, VERBOSE="INFO", QUIET="ERROR")
223
224
    def new_log_fn(self, level: str, logger: Logger = _logger):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
225
        """
226
        Generates functions to attach to a ``loguru._logger.Logger``.
227
        For example, ``LogMethodFactory.new("caution")`` will return
228
        a function that delegates (essentially) to ``logger.log("CAUTION", ...)``.
229
        """
230
231
        def _x(__message: str, *args, **kwargs):
232
            logger._log(level.upper(), None, False, logger._options, __message, args, kwargs)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _log was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
Coding Style Best Practice introduced by
It seems like _options was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
233
234
        _x.__name__ = level.lower()
235
        return _x
236
237
238
@dataclass(frozen=True, repr=True, order=True)
239
class HandlerInfo:
240
    """
241
    Information about a loguru handler.
242
    """
243
244
    hid: int
245
    path: Optional[Path]
246
    level: Optional[str]
247
    fmt: Formatter
248
249
250
@dataclass(frozen=False, repr=True)
251
class _HandlerInfo:
252
    hid: int
253
    sink: Any
254
    level: Optional[int]
255
    fmt: Formatter
256
257
    @property
258
    def to_friendly(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
259
        return HandlerInfo(hid=self.hid, level=self.level, fmt=self.fmt, path=self.sink)
260
261
262
@dataclass(frozen=True, repr=True, order=True)
263
class LogSinkInfo:
264
    """
265
    Information about a loguru sink, before it has been added.
266
    """
267
268
    path: Path
269
    base: Path
270
    suffix: str
271
    serialize: bool
272
    compression: Optional[str]
273
274
275
class InterceptHandler(logging.Handler):
276
    """
277
    Redirects standard logging to loguru.
278
    """
279
280
    def emit(self, record):
281
        # Get corresponding Loguru level if it exists
282
        try:
283
            level = _logger.level(record.levelname).name
284
        except ValueError:
285
            level = record.levelno
286
        # Find caller from where originated the logged message
287
        frame, depth = logging.currentframe(), 2
288
        while frame.f_code.co_filename == logging.__file__:
289
            frame = frame.f_back
290
            depth += 1
291
        _logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
292
293
294
class Rememberer:
295
    """
296
    A handler that stores recent messages in a deque.
297
    """
298
299
    def __init__(self, n_messages: int):
300
        self.hid: int = -1
301
        self._messages: Deque[str] = deque(maxlen=n_messages)
302
303
    def __call__(self, msg: str):
304
        self._messages.append(msg)
305
306
307
class LoggerWithCautionAndNotice(Logger, metaclass=abc.ABCMeta):
308
    """
309
    A wrapper that has fake methods to trick static analysis.
310
    """
311
312
    def caution(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
313
        raise NotImplementedError()  # not real
314
315
    def notice(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
316
        raise NotImplementedError()  # not real
317
318
319
class FancyLoguru(Generic[T]):
0 ignored issues
show
best-practice introduced by
Too many public methods (27/20)
Loading history...
320
    """
321
    See :module:`pocketutils.misc.fancy_loguru`.
322
    """
323
324
    def __init__(self, logger: T = _logger):
325
        self._defaults = _Defaults()
326
        self._logger = logger
327
        # noinspection PyTypeChecker
328
        self._main: _HandlerInfo = None
329
        self._rememberer: Rememberer = None
330
        self._paths: MutableMapping[Path, _HandlerInfo] = {}
331
        self._aliases = dict(self._defaults.aliases)
332
        self._control_enabled = True
333
334
    @staticmethod
335
    def new(t: Type[Z]) -> FancyLoguru[Z]:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "t" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
introduced by
Missing function or method docstring
Loading history...
336
        ell = _logger.patch(_add_traceback)
337
        logger = t(ell._core, *ell._options)
338
        return FancyLoguru[Z](logger)
339
340
    @property
341
    def defaults(self) -> _Defaults:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
342
        return self._defaults
343
344
    @property
345
    def logger(self) -> T:
346
        """
347
        Returns the stored logger.
348
        """
349
        return self._logger
350
351
    @property
352
    def levels(self) -> Mapping[str, int]:
353
        """
354
        Returns the global loguru levels.
355
        """
356
        return {e.name: e.no for e in _levels.values()}
357
358
    @property
359
    def aliases(self) -> Mapping[str, Optional[str]]:
360
        """
361
        Returns the aliases to levels.
362
        A ``None`` means no logging ("OFF").
363
        """
364
        return self._aliases
365
366
    @property
367
    def recent_messages(self) -> Sequence[str]:
368
        """
369
        Returns some number of recent messages, if recording.
370
371
        See Also:
372
            :meth:`remember`
373
        """
374
        if self._rememberer is None:
375
            return []
376
        return list(self._rememberer._messages)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _messages was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
377
378
    @property
379
    def main(self) -> Optional[HandlerInfo]:
380
        """
381
        Returns the main handler info, if configured.
382
        """
383
        if self._main is None:
384
            return None
385
        return HandlerInfo(
386
            hid=self._main.hid, level=self._main.level, fmt=self._main.fmt, path=None
387
        )
388
389
    @property
390
    def paths(self) -> AbstractSet[HandlerInfo]:
391
        """
392
        Lists all path handlers configured in this object.
393
        """
394
        return {h.to_friendly for h in self._paths.values()}
395
396
    def get_path(self, p: PathLike) -> Optional[HandlerInfo]:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "p" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
397
        """
398
        Returns a path handler to this path, or None if it does not exist.
399
        The path is resolved, following symlinks, via ``pathlib.Path.resolve``.
400
        """
401
        p = Path(p)
402
        p = self._paths.get(p.resolve())
403
        return None if p is None else p.to_friendly
404
405
    def set_control(self, enabled: bool) -> __qualname__:
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable '__qualname__'
Loading history...
406
        """
407
        Enables/disables handler control.
408
        If control is disabled, subsequent calls to
409
        methods like :meth:`from_cli` and :meth:`add_path` do nothing.
410
        """
411
        self._control_enabled = enabled
412
        return self
413
414
    @property
415
    def is_control_enabled(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
416
        return self._control_enabled
417
418
    def config_levels(
419
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
420
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
421
        levels: Mapping[str, int] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
422
        colors: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
423
        icons: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
424
        aliases: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
425
    ) -> __qualname__:
426
        """
427
        Modify loguru's levels.
428
        This is a global operation and will run regardless of :attr:`is_control_enabled`.
429
        """
430
        levels = self._defaults.levels_extended if levels is _SENTINEL else levels
431
        colors = self._defaults.colors_extended if colors is _SENTINEL else colors
432
        icons = self._defaults.icons_extended if icons is _SENTINEL else icons
433
        aliases = self._defaults.aliases if aliases is _SENTINEL else aliases
434
        for k, v in levels.items():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
435
            self.config_level(
436
                k,
437
                v,
438
                color=colors.get(k, _SENTINEL),
439
                icon=icons.get(k, _SENTINEL),
440
            )
441
        self._aliases = dict(aliases)
442
        return self
443
444
    def config_level(
445
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
446
        name: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
447
        level: int,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
448
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
449
        color: Union[None, str, _SENTINEL] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
450
        icon: Union[None, str, _SENTINEL] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
451
        replace: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
452
    ) -> __qualname__:
453
        """
454
        Add a new loguru level.
455
        This is a global operation and will run regardless of :attr:`is_control_enabled`.
456
        """
457
        try:
458
            data = self._logger.level(name)
459
        except ValueError:
460
            data = None
461
        if data is None:
462
            self._logger.level(
463
                name,
464
                no=level,
465
                color=None if color is _SENTINEL else color,
466
                icon=None if icon is _SENTINEL else icon,
467
            )
468
        elif replace:
469
            if level != data.no:  # loguru doesn't check whether they're eq; it just errors
470
                raise IllegalStateError(f"Cannot set level={level}!={data.no} for {name}")
471
            self._logger.level(
472
                name,
473
                color=data.color if color is _SENTINEL else color,
474
                icon=data.icon if icon is _SENTINEL else icon,
475
            )
476
        return self
477
478
    def add_log_methods(self, *, replace: bool = True) -> __qualname__:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
479
        levels = [level.lower() for level in self.levels.keys()]
480
        for level in levels:
481
            _x = self._defaults.new_log_fn(level, self._logger)
482
            if replace or not hasattr(self._logger, level):
483
                setattr(self._logger, level, _x)
484
        return self
485
486
    def enable(self, *names: str) -> __qualname__:
487
        """
488
        Calls ``loguru.logger.enable`` on multiple items.
489
        """
490
        if not self._control_enabled:
491
            return self
492
        for name in names:
493
            _logger.enable(name)
494
        return self
495
496
    def disable(self, *names: str) -> __qualname__:
497
        """
498
        Calls ``loguru.logger.disable`` on multiple items.
499
        """
500
        if not self._control_enabled:
501
            return self
502
        for name in names:
503
            _logger.disable(name)
504
        return self
505
506
    def intercept_std(self, *, warnings: bool = True) -> __qualname__:
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
507
        """
508
        Sets python builtin ``logging`` to redirect to loguru.
509
        Uses :class:`InterceptHandler`.
510
511
        Args:
512
            warnings: Call ``logging.captureWarnings(True)`` to intercept builtin ``warnings``
513
        """
514
        # noinspection PyArgumentList
515
        logging.basicConfig(handlers=[InterceptHandler()], level=0, encoding="utf-8")
516
        if warnings:
517
            logging.captureWarnings(True)
518
519
    def config_main(
520
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
521
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
522
        sink: TextIO = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
523
        level: Optional[str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
524
        fmt: Formatter = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
525
    ) -> __qualname__:
526
        """
527
        Sets the logging level for the main handler (normally stderr).
528
        """
529
        if not self._control_enabled:
530
            if self._main is not None:
531
                self._main.level = self._main.level if level is _SENTINEL else level
532
                self._main.sink = self._main.sink if sink is _SENTINEL else sink
533
                self._main.fmt = self._main.fmt if fmt is _SENTINEL else fmt
534
            return self
535
        if level is not None and level is not _SENTINEL:
536
            level = level.upper()
537
        if self._main is None:
538
            self._main = _HandlerInfo(
539
                hid=-1,
540
                sink=sys.stderr,
541
                level=self.levels[self._defaults.level],
542
                fmt=self._defaults.fmt_built_in,
543
            )
544
        else:
545
            try:
546
                self._logger.remove(self._main.hid)
547
            except ValueError:
548
                self._logger.error(f"Cannot remove handler {self._main.hid}")
549
        self._main.level = self._main.level if level is _SENTINEL else level
550
        self._main.sink = self._main.sink if sink is _SENTINEL else sink
551
        self._main.fmt = self._main.fmt if fmt is _SENTINEL else fmt
552
        self._main.hid = self._logger.add(
553
            self._main.sink, level=self._main.level, format=self._main.fmt
554
        )
555
        return self
556
557
    def remember(self, *, n_messages: int = 100) -> __qualname__:
0 ignored issues
show
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
558
        """
559
        Adds a handler that stores the last ``n_messages``.
560
        Retrieve the stored messages with :meth:`recent_messages`.
561
        """
562
        if n_messages == 0 and self._rememberer is None:
563
            return
564
        if n_messages == 0:
565
            self._logger.remove(self._rememberer.hid)
566
            return
567
        extant = self.recent_messages
568
        self._rememberer = Rememberer(n_messages)
569
        self._rememberer.hid = self._logger.add(
570
            self._rememberer, level="TRACE", format=self._main.fmt
571
        )
572
        for msg in extant:
573
            self._rememberer(msg)
574
        return self
575
576
    def add_path(
577
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
578
        path: PathLike,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
579
        level: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
580
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
581
        fmt: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
582
        filter=None,
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in filter.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
583
    ) -> __qualname__:
584
        """
585
        Adds a handler to a file.
586
587
        See Also:
588
            :meth:`remove_path`
589
590
        Args:
591
            path: If it ends with .gz, .zip, .etc., will use compression
592
                  If (ignoring compression) ends with .json, will serialize as JSON.
593
                  Calls ``pathlib.Path.resolve``, meaning that symlinks are followed
594
            level: Min log level
595
            fmt: Formatting string; will wrap into a :class:`Formatter`
596
                 Include ``{{EXTRA}}`` to include all extras
597
                 See: :class:`FormatFactory`
598
            filter: Filtration function of records
599
        """
600
        if not self._control_enabled:
601
            return self
602
        path = Path(path).resolve()
603
        level, ell, fmt = self._get_info(level, fmt)
604
        info = self.guess_file_sink_info(path)
605
        hid = self._logger.add(
606
            str(info.base),
607
            format=fmt,
608
            level=level,
609
            compression=info.compression,
610
            serialize=info.serialize,
611
            backtrace=True,
612
            diagnose=True,
613
            enqueue=True,
614
            filter=filter,
615
            encoding="utf-8",
616
        )
617
        self._paths[path] = _HandlerInfo(hid=hid, sink=info.base, level=ell, fmt=fmt)
618
        return self
619
620
    def remove_paths(self) -> __qualname__:
621
        """
622
        Removes **all** path handlers stored here.
623
624
        See Also:
625
            :meth:`remove_path`
626
        """
627
        if not self._control_enabled:
628
            return self
629
        for p in dict(self._paths).keys():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "p" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
unused-code introduced by
Consider iterating the dictionary directly instead of calling .keys()
Loading history...
630
            self.remove_path(p)
631
        return self
632
633
    def remove_path(self, path: Path) -> __qualname__:
634
        """
635
        Removes a path handler (limited to those stored here).
636
        Will log an error and continue if the path is not found.
637
638
        See Also:
639
            :meth:`remove_paths`
640
        """
641
        if not self._control_enabled:
642
            return self
643
        path = path.resolve()
644
        p = self._paths.get(path)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "p" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
645
        if p is not None:
646
            try:
647
                self._logger.remove(p.hid)
648
                del self._paths[path]
649
            except ValueError:
650
                self._logger.exception(f"Cannot remove handler {p.hid} to {path}")
651
        return self
652
653
    def from_cli(
654
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
655
        path: Union[None, str, Path] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
656
        main: Optional[str] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
657
        _msg_level: str = "OFF",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
658
    ) -> __qualname__:
659
        """
660
        This function controls logging set via command-line.
661
        Deletes any existing path handlers.
662
663
        Args:
664
            main: The level for stderr; if None, does not modify
665
            path: If set, the path to a file. Can be prefixed with ``:level:`` to set the level
666
                  (e.g. ``:INFO:mandos-run.log.gz``). Can serialize to JSON if .json is used
667
                  instead of .log or .txt.
668
            _msg_level: Level for messages about this logging change
669
        """
670
        _msg_level = self._aliases.get(_msg_level.upper(), _msg_level.upper())
671
        if not self._control_enabled:
672
            if self._main is not None:
673
                self._main.level = _msg_level
674
            return self
675
        if main is _SENTINEL:
676
            main = None
677
        if main is None and self._main is None:
678
            main = self._defaults.level
679
        elif main is None:
680
            main = self._main.level
681
        main = self._aliases.get(main.upper(), main.upper())
682
        if main not in self._defaults.levels_extended:
683
            _permitted = ", ".join(
684
                [*self._defaults.levels_extended, *self._defaults.aliases.keys()]
685
            )
686
            raise XValueError(f"{main.lower()} not a permitted log level (allowed: {_permitted}")
687
        self.config_main(level=main)
688
        self.remove_paths()
689
        if path is not None and len(str(path)) > 0:
690
            match = _LOGGER_ARG_PATTERN.match(str(path))
691
            path_level = "DEBUG" if match.group(1) is None else match.group(1)
692
            path = Path(match.group(2))
693
            self.add_path(path, path_level)
694
            if _msg_level is not None:
695
                self._logger.log(_msg_level, f"Added path handler {path} (level {path_level})")
696
        if _msg_level is not None:
697
            self._logger.log(_msg_level, f"Set main log level to {main}")
698
        return self
699
700
    __call__ = from_cli
701
702
    def rewire_streams_to_utf8(self) -> __qualname__:
703
        """
704
        Calls ``reconfigure`` on ``sys.stderr``, ``sys.stdout``, and ``sys.stdin`` to use utf-8.
705
        Use at your own risk.
706
        """
707
        sys.stderr.reconfigure(encoding="utf-8")
708
        sys.stdout.reconfigure(encoding="utf-8")
709
        sys.stdin.reconfigure(encoding="utf-8")
710
        return self
711
712
    @classmethod
713
    def guess_file_sink_info(cls, path: Union[str, Path]) -> LogSinkInfo:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
714
        path = Path(path)
715
        base, compression = path.name, None
716
        for c in log_compressions:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "c" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
717
            if path.name.endswith(c):
718
                base, compression = path.name[: -len(c)], c
719
        if not [base.endswith(s) for s in [".json", ".log", ".txt"]]:
720
            raise XValueError(
721
                f"Log filename {path.name} is not .json, .log, .txt, or a compressed variant"
722
            )
723
        return LogSinkInfo(
724
            path=path,
725
            base=path.parent / base,
726
            suffix=compression,
727
            serialize=base.endswith(".json"),
728
            compression=compression,
729
        )
730
731
    def _get_info(self, level: str = _SENTINEL, fmt: str = _SENTINEL) -> Tuple[str, int, Formatter]:
732
        if level is _SENTINEL and self._main is None:
733
            level = self._defaults.level
734
        elif level is _SENTINEL:
735
            level = self._main.level
736
        level = level.upper()
737
        if fmt is _SENTINEL and self._main is None:
738
            fmt = self._defaults.fmt_built_in
739
        elif fmt is _SENTINEL:
740
            fmt = self._main.fmt
741
        if isinstance(fmt, str):
742
            fmt = self._defaults.wrap_extended_fmt(fmt=fmt)
743
        ell = self.levels[level]
744
        return level, ell, fmt
745
746
    @classmethod
747
    def built_in(
748
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
749
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
750
        enable_control: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
751
        sink=sys.stderr,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
752
        level: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
753
    ) -> FancyLoguru[Logger]:
754
        """
755
        Creates a new FancyLoguru using standard loguru levels, etc.
756
757
        Args:
758
            enable_control: If False, all calls to add/remove handlers (except :meth:`remember`)
759
                            will be ignored. :meth:`rewire_streams` will also be ignored.
760
                            This is provided so that you can configure both an "application"
761
                            and a library that works for the same code
762
                            with ``enable_control=<is-command-line>``.
763
            sink: The *main* sink to start with
764
            level: The min log level for the main sink
765
        """
766
        return (
767
            FancyLoguru.new(Logger)
768
            .set_control(enable_control)
769
            .config_levels()
770
            .remember()
771
            .config_main(level=level, sink=sink)
772
        )
773
774
    @classmethod
775
    def extended(
776
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
777
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
778
        enable_control: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
779
        sink=sys.stderr,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
780
        level: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
781
        simplify_fmt: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
782
        red_green_safe: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
783
    ) -> FancyLoguru[LoggerWithCautionAndNotice]:
784
        """
785
        Creates a new FancyLoguru with extra levels "caution" and "notice".
786
        - *CAUTION*: Bad, but between levels *INFO* and *SUCCESS*
787
        - *NOTICE*: Good/neutral, but between levels *WARNING* and *ERROR*
788
789
        Args:
790
            enable_control: See :meth:`built_in`
791
            sink: See :meth:`built_in`
792
            level: See :meth:`built_in`
793
            simplify_fmt: Use ``DEFAULT_FMT_STRING``
794
            red_green_safe: Modify the standard colors to use blue instead of green
795
        """
796
        defaults = _Defaults()
797
        levels = defaults.levels_extended
798
        icons = defaults.icons_extended
799
        colors = defaults.colors_red_green_safe if red_green_safe else defaults.colors_extended
800
        fmt = defaults.fmt_simplified if simplify_fmt else defaults.fmt_built_in
801
        return (
802
            FancyLoguru.new(LoggerWithCautionAndNotice)
803
            .set_control(enable_control)
804
            .config_levels(levels=levels, colors=colors, icons=icons)
805
            .remember()
806
            .config_main(level=level, sink=sink, fmt=fmt)
807
        )
808
809
810
FANCY_LOGURU_DEFAULTS = _Defaults()
811
812
813
__all__ = [
814
    "FancyLoguru",
815
    "FANCY_LOGURU_DEFAULTS",
816
    "InterceptHandler",
817
    "HandlerInfo",
818
    "Logger",
819
    "LoggerWithCautionAndNotice",
820
]
821