Passed
Push — main ( 81ca09...979ada )
by Douglas
01:38
created

pocketutils.misc.loguru_utils._add_traceback()   A

Complexity

Conditions 2

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nop 1
dl 0
loc 6
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 as _traceback
20
from collections import deque
21
from dataclasses import dataclass
22
from inspect import cleandoc
23
from pathlib import Path
24
from typing import (
0 ignored issues
show
Unused Code introduced by
Unused Iterable imported from typing
Loading history...
25
    AbstractSet,
26
    Any,
27
    Callable,
28
    Deque,
29
    Generic,
30
    Iterable,
31
    Mapping,
32
    MutableMapping,
33
    Optional,
34
    Sequence,
35
    TextIO,
36
    Tuple,
37
    Type,
38
    TypeVar,
39
    Union,
40
)
41
42
import loguru._defaults as _defaults
0 ignored issues
show
introduced by
Unable to import 'loguru._defaults'
Loading history...
43
44
# noinspection PyProtectedMember
45
import loguru._logger
0 ignored issues
show
introduced by
Unable to import 'loguru._logger'
Loading history...
46
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
47
from loguru import logger as _logger
0 ignored issues
show
introduced by
Unable to import 'loguru'
Loading history...
48
49
# noinspection PyProtectedMember
50
from loguru._logger import Logger
0 ignored issues
show
introduced by
Unable to import 'loguru._logger'
Loading history...
51
52
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...
53
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...
54
55
_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...
56
Formatter = Union[str, Callable[[Mapping[str, Any]], str]]
57
DEFAULT_FMT_STRING = cleandoc(
58
    r"""
59
    <bold>{time:YYYY-MM-DD HH:mm:ss.SSS} | </bold>
60
    <level>{level: <7}</level><bold> | </bold>
61
    <cyan>({thread.id}){name}</cyan><bold>:</bold>
62
    <cyan>{function}</cyan><bold>:</bold>
63
    <cyan>{line}</cyan><bold> — </bold>
64
    <level>{message}{{EXTRA}}{{TRACEBACK}}</level>
65
    {exception}
66
    """
67
).replace("\n", "")
68
69
70
class _SENTINEL:
71
    pass
72
73
74
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...
75
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...
76
77
78
def log_traceback(record):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
79
    extra = record["extra"]
80
    if extra.get("traceback", False):
81
        extra["traceback"] = "\n" + "".join(_traceback.format_stack())
82
    else:
83
        extra["traceback"] = ""
84
85
86
_LOGGER_ARG_PATTERN = regex.compile(r"(?:([a-zA-Z]+):)?(.*)", flags=regex.V1)
87
log_compressions = {
88
    ".xz",
89
    ".lzma",
90
    ".gz",
91
    ".zip",
92
    ".bz2",
93
    ".tar",
94
    ".tar.gz",
95
    ".tar.bz2",
96
    ".tar.xz",
97
}
98
valid_log_suffixes = {
99
    *{f".log{c}" for c in log_compressions},
100
    *{f".txt{c}" for c in log_compressions},
101
    *{f".json{c}" for c in log_compressions},
102
}
103
104
105
class _Defaults:
106
    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...
107
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
108
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
109
        fmt: str = DEFAULT_FMT_STRING,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
110
        sep: str = "; ",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
111
        eq_sign: str = " ",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
112
    ) -> Callable[[Mapping[str, Any]], str]:
113
        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...
114
            extra = [e for e in record["extra"] if e != "traceback"]
115
            if len(extra) > 0:
116
                extra = sep.join([e + eq_sign + "{extra[" + e + "]}" for e in extra])
117
                extra = f" [ {extra} ]"
118
            else:
119
                extra = ""
120
            f = fmt.replace("{{EXTRA}}", extra)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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...
121
            tb = record["extra"].get("traceback", False)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "tb" 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...
122
            if tb:
123
                f = f.replace("{{TRACEBACK}}", "{extra[traceback]}")
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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...
124
            else:
125
                f = f.replace("{{TRACEBACK}}", "")
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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 f + os.linesep
127
128
        return FMT
129
130
    def wrap_plain_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...
131
        self, *, fmt: str = DEFAULT_FMT_STRING
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
132
    ) -> Callable[[Mapping[str, Any]], str]:
133
        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...
134
            return fmt.replace("{{EXTRA}}", "") + os.linesep
135
136
        return FMT
137
138
    @property
139
    def levels_current(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
140
        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...
141
        return {e.name: e.no for e in ell.values()}
142
143
    @property
144
    def colors_current(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
145
        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...
146
        return {e.name: e.color for e in ell.values()}
147
148
    @property
149
    def icons_current(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
150
        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...
151
        return {e.name: e.icon for e in ell.values()}
152
153
    @property
154
    def levels_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
155
        return {e.name: e.no for e in _levels.values()}
156
157
    @property
158
    def colors_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
159
        return {e.name: e.color for e in _levels.values()}
160
161
    @property
162
    def icons_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
163
        return {e.name: e.icon for e in _levels.values()}
164
165
    # the levels for caution and notice are DEFINED here
166
    # trace and success must match loguru's
167
    # and the rest must match logging's
168
    # note that most of these alternate between informative and problematic
169
    # i.e. info (ok), caution (bad), success (ok), warning (bad), notice (ok), error (bad)
170
    @property
171
    def levels_extended(self) -> Mapping[str, int]:
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
        levels = {k.name: k.no for k in ell.values()}
174
        levels.setdefault("CAUTION", 23)
175
        levels.setdefault("NOTICE", 37)
176
        return levels
177
178
    @property
179
    def colors_extended(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
180
        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...
181
        colors = {k.name: k.color for k in ell.values()}
182
        colors.setdefault("CAUTION", ell["WARNING"].color)
183
        colors.setdefault("NOTICE", ell["INFO"].color)
184
        return colors
185
186
    @property
187
    def colors_red_green_safe(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
188
        return dict(
189
            TRACE="<dim>",
190
            DEBUG="<dim>",
191
            INFO="<bold>",
192
            CAUTION="<yellow>",
193
            SUCCESS="<blue>",
194
            WARNING="<yellow>",
195
            NOTICE="<blue>",
196
            ERROR="<red>",
197
            CRITICAL="<red>",
198
        )
199
200
    @property
201
    def icons_extended(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
202
        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...
203
        icons = {k.name: k.icon for k in ell.values()}
204
        icons.setdefault("CAUTION", "⚐")
205
        icons.setdefault("NOTICE", "★")
206
        return icons
207
208
    @property
209
    def level(self) -> str:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
210
        return _defaults.LOGURU_LEVEL
211
212
    @property
213
    def fmt_simplified(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
214
        return self.wrap_extended_fmt()
215
216
    @property
217
    def fmt_built_in(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
218
        return self.wrap_plain_fmt()
219
220
    @property
221
    def fmt_built_in_raw(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
222
        return _defaults.LOGURU_FORMAT
223
224
    @property
225
    def fmt_extended_raw(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
226
        return DEFAULT_FMT_STRING
227
228
    @property
229
    def aliases(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
230
        return dict(NONE=None, NO=None, OFF=None, VERBOSE="INFO", QUIET="ERROR")
231
232
    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...
233
        """
234
        Generates functions to attach to a ``loguru._logger.Logger``.
235
        For example, ``LogMethodFactory.new("caution")`` will return
236
        a function that delegates (essentially) to ``logger.log("CAUTION", ...)``.
237
        """
238
239
        def _x(__message: str, *args, **kwargs):
240
            logger._log(level.upper(), None, False, logger._options, __message, args, kwargs)
0 ignored issues
show
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...
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...
241
242
        _x.__name__ = level.lower()
243
        return _x
244
245
246
@dataclass(frozen=True, repr=True, order=True)
247
class HandlerInfo:
248
    """
249
    Information about a loguru handler.
250
    """
251
252
    hid: int
253
    path: Optional[Path]
254
    level: Optional[str]
255
    fmt: Formatter
256
257
258
@dataclass(frozen=False, repr=True)
259
class _HandlerInfo:
260
    hid: int
261
    sink: Any
262
    level: Optional[int]
263
    fmt: Formatter
264
265
    @property
266
    def to_friendly(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
267
        return HandlerInfo(hid=self.hid, level=self.level, fmt=self.fmt, path=self.sink)
268
269
270
@dataclass(frozen=True, repr=True, order=True)
271
class LogSinkInfo:
272
    """
273
    Information about a loguru sink, before it has been added.
274
    """
275
276
    path: Path
277
    base: Path
278
    suffix: str
279
    serialize: bool
280
    compression: Optional[str]
281
282
283
class InterceptHandler(logging.Handler):
284
    """
285
    Redirects standard logging to loguru.
286
    """
287
288
    def emit(self, record):
289
        # Get corresponding Loguru level if it exists
290
        try:
291
            level = _logger.level(record.levelname).name
292
        except ValueError:
293
            level = record.levelno
294
        # Find caller from where originated the logged message
295
        frame, depth = logging.currentframe(), 2
296
        while frame.f_code.co_filename == logging.__file__:
297
            frame = frame.f_back
298
            depth += 1
299
        _logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
300
301
302
class Rememberer:
303
    """
304
    A handler that stores recent messages in a deque.
305
    """
306
307
    def __init__(self, n_messages: int):
308
        self.hid: int = -1
309
        self._messages: Deque[str] = deque(maxlen=n_messages)
310
311
    def __call__(self, msg: str):
312
        self._messages.append(msg)
313
314
315
class LoggerWithCautionAndNotice(Logger, metaclass=abc.ABCMeta):
316
    """
317
    A wrapper that has fake methods to trick static analysis.
318
    """
319
320
    def caution(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
321
        raise NotImplementedError()  # not real
322
323
    def notice(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
324
        raise NotImplementedError()  # not real
325
326
327
class FancyLoguru(Generic[T]):
0 ignored issues
show
best-practice introduced by
Too many public methods (27/20)
Loading history...
328
    """
329
    See :module:`pocketutils.misc.fancy_loguru`.
330
    """
331
332
    def __init__(self, logger: T = _logger):
333
        self._defaults = _Defaults()
334
        self._logger = logger
335
        # noinspection PyTypeChecker
336
        self._main: _HandlerInfo = None
337
        self._rememberer: Rememberer = None
338
        self._paths: MutableMapping[Path, _HandlerInfo] = {}
339
        self._aliases = dict(self._defaults.aliases)
340
        self._control_enabled = True
341
342
    @staticmethod
343
    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...
344
        ell = _logger.patch(log_traceback)
345
        logger = t(ell._core, *ell._options)
346
        return FancyLoguru[Z](logger)
347
348
    @property
349
    def defaults(self) -> _Defaults:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
350
        return self._defaults
351
352
    @property
353
    def logger(self) -> T:
354
        """
355
        Returns the stored logger.
356
        """
357
        return self._logger
358
359
    @property
360
    def levels(self) -> Mapping[str, int]:
361
        """
362
        Returns the global loguru levels.
363
        """
364
        return {e.name: e.no for e in _levels.values()}
365
366
    @property
367
    def aliases(self) -> Mapping[str, Optional[str]]:
368
        """
369
        Returns the aliases to levels.
370
        A ``None`` means no logging ("OFF").
371
        """
372
        return self._aliases
373
374
    @property
375
    def recent_messages(self) -> Sequence[str]:
376
        """
377
        Returns some number of recent messages, if recording.
378
379
        See Also:
380
            :meth:`remember`
381
        """
382
        if self._rememberer is None:
383
            return []
384
        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...
385
386
    @property
387
    def main(self) -> Optional[HandlerInfo]:
388
        """
389
        Returns the main handler info, if configured.
390
        """
391
        if self._main is None:
392
            return None
393
        return HandlerInfo(
394
            hid=self._main.hid, level=self._main.level, fmt=self._main.fmt, path=None
395
        )
396
397
    @property
398
    def paths(self) -> AbstractSet[HandlerInfo]:
399
        """
400
        Lists all path handlers configured in this object.
401
        """
402
        return {h.to_friendly for h in self._paths.values()}
403
404
    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...
405
        """
406
        Returns a path handler to this path, or None if it does not exist.
407
        The path is resolved, following symlinks, via ``pathlib.Path.resolve``.
408
        """
409
        p = Path(p)
410
        p = self._paths.get(p.resolve())
411
        return None if p is None else p.to_friendly
412
413
    def set_control(self, enabled: bool) -> __qualname__:
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable '__qualname__'
Loading history...
414
        """
415
        Enables/disables handler control.
416
        If control is disabled, subsequent calls to
417
        methods like :meth:`from_cli` and :meth:`add_path` do nothing.
418
        """
419
        self._control_enabled = enabled
420
        return self
421
422
    @property
423
    def is_control_enabled(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
424
        return self._control_enabled
425
426
    def config_levels(
427
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
428
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
429
        levels: Mapping[str, int] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
430
        colors: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
431
        icons: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
432
        aliases: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
433
    ) -> __qualname__:
434
        """
435
        Modify loguru's levels.
436
        This is a global operation and will run regardless of :attr:`is_control_enabled`.
437
        """
438
        levels = self._defaults.levels_extended if levels is _SENTINEL else levels
439
        colors = self._defaults.colors_extended if colors is _SENTINEL else colors
440
        icons = self._defaults.icons_extended if icons is _SENTINEL else icons
441
        aliases = self._defaults.aliases if aliases is _SENTINEL else aliases
442
        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...
443
            self.config_level(
444
                k,
445
                v,
446
                color=colors.get(k, _SENTINEL),
447
                icon=icons.get(k, _SENTINEL),
448
            )
449
        self._aliases = dict(aliases)
450
        return self
451
452
    def config_level(
453
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
454
        name: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
455
        level: int,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
456
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
457
        color: Union[None, str, _SENTINEL] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
458
        icon: Union[None, str, _SENTINEL] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
459
        replace: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
460
    ) -> __qualname__:
461
        """
462
        Add a new loguru level.
463
        This is a global operation and will run regardless of :attr:`is_control_enabled`.
464
        """
465
        try:
466
            data = self._logger.level(name)
467
        except ValueError:
468
            data = None
469
        if data is None:
470
            self._logger.level(
471
                name,
472
                no=level,
473
                color=None if color is _SENTINEL else color,
474
                icon=None if icon is _SENTINEL else icon,
475
            )
476
        elif replace:
477
            if level != data.no:  # loguru doesn't check whether they're eq; it just errors
478
                raise IllegalStateError(f"Cannot set level={level}!={data.no} for {name}")
479
            self._logger.level(
480
                name,
481
                color=data.color if color is _SENTINEL else color,
482
                icon=data.icon if icon is _SENTINEL else icon,
483
            )
484
        return self
485
486
    def add_log_methods(self, *, replace: bool = True) -> __qualname__:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
487
        levels = [level.lower() for level in self.levels.keys()]
488
        for level in levels:
489
            _x = self._defaults.new_log_fn(level, self._logger)
490
            if replace or not hasattr(self._logger, level):
491
                setattr(self._logger, level, _x)
492
        return self
493
494
    def enable(self, *names: str) -> __qualname__:
495
        """
496
        Calls ``loguru.logger.enable`` on multiple items.
497
        """
498
        if not self._control_enabled:
499
            return self
500
        for name in names:
501
            _logger.enable(name)
502
        return self
503
504
    def disable(self, *names: str) -> __qualname__:
505
        """
506
        Calls ``loguru.logger.disable`` on multiple items.
507
        """
508
        if not self._control_enabled:
509
            return self
510
        for name in names:
511
            _logger.disable(name)
512
        return self
513
514
    def intercept_std(self, *, warnings: bool = True) -> __qualname__:
515
        """
516
        Sets python builtin ``logging`` to redirect to loguru.
517
        Uses :class:`InterceptHandler`.
518
519
        Args:
520
            warnings: Call ``logging.captureWarnings(True)`` to intercept builtin ``warnings``
521
        """
522
        # noinspection PyArgumentList
523
        logging.basicConfig(handlers=[InterceptHandler()], level=0, encoding="utf-8")
524
        if warnings:
525
            logging.captureWarnings(True)
526
        return self
527
528
    def config_main(
529
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
530
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
531
        sink: TextIO = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
532
        level: Optional[str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
533
        fmt: Formatter = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
534
    ) -> __qualname__:
535
        """
536
        Sets the logging level for the main handler (normally stderr).
537
        """
538
        if not self._control_enabled:
539
            if self._main is not None:
540
                self._main.level = self._main.level if level is _SENTINEL else level
541
                self._main.sink = self._main.sink if sink is _SENTINEL else sink
542
                self._main.fmt = self._main.fmt if fmt is _SENTINEL else fmt
543
            return self
544
        if level is not None and level is not _SENTINEL:
545
            level = level.upper()
546
        if self._main is None:
547
            self._main = _HandlerInfo(
548
                hid=-1,
549
                sink=sys.stderr,
550
                level=self.levels[self._defaults.level],
551
                fmt=self._defaults.fmt_built_in,
552
            )
553
        else:
554
            try:
555
                self._logger.remove(self._main.hid)
556
            except ValueError:
557
                self._logger.error(f"Cannot remove handler {self._main.hid}")
558
        self._main.level = self._main.level if level is _SENTINEL else level
559
        self._main.sink = self._main.sink if sink is _SENTINEL else sink
560
        self._main.fmt = self._main.fmt if fmt is _SENTINEL else fmt
561
        self._main.hid = self._logger.add(
562
            self._main.sink, level=self._main.level, format=self._main.fmt
563
        )
564
        return self
565
566
    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...
567
        """
568
        Adds a handler that stores the last ``n_messages``.
569
        Retrieve the stored messages with :meth:`recent_messages`.
570
        """
571
        if n_messages == 0 and self._rememberer is None:
572
            return
573
        if n_messages == 0:
574
            self._logger.remove(self._rememberer.hid)
575
            return
576
        extant = self.recent_messages
577
        self._rememberer = Rememberer(n_messages)
578
        self._rememberer.hid = self._logger.add(
579
            self._rememberer,
580
            level="TRACE",
581
            format=self._defaults.fmt_simplified if self._main is None else self._main.fmt,
582
        )
583
        for msg in extant:
584
            self._rememberer(msg)
585
        return self
586
587
    def add_path(
588
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
589
        path: PathLike,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
590
        level: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
591
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
592
        fmt: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
593
        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...
594
    ) -> __qualname__:
595
        """
596
        Adds a handler to a file.
597
598
        See Also:
599
            :meth:`remove_path`
600
601
        Args:
602
            path: If it ends with .gz, .zip, .etc., will use compression
603
                  If (ignoring compression) ends with .json, will serialize as JSON.
604
                  Calls ``pathlib.Path.resolve``, meaning that symlinks are followed
605
            level: Min log level
606
            fmt: Formatting string; will wrap into a :class:`Formatter`
607
                 Include ``{{EXTRA}}`` to include all extras
608
                 See: :class:`FormatFactory`
609
            filter: Filtration function of records
610
        """
611
        if not self._control_enabled:
612
            return self
613
        path = Path(path).resolve()
614
        level, ell, fmt = self._get_info(level, fmt)
615
        info = self.guess_file_sink_info(path)
616
        hid = self._logger.add(
617
            str(info.base),
618
            format=fmt,
619
            level=level,
620
            compression=info.compression,
621
            serialize=info.serialize,
622
            backtrace=True,
623
            diagnose=True,
624
            enqueue=True,
625
            filter=filter,
626
            encoding="utf-8",
627
        )
628
        self._paths[path] = _HandlerInfo(hid=hid, sink=info.base, level=ell, fmt=fmt)
629
        return self
630
631
    def remove_paths(self) -> __qualname__:
632
        """
633
        Removes **all** path handlers stored here.
634
635
        See Also:
636
            :meth:`remove_path`
637
        """
638
        if not self._control_enabled:
639
            return self
640
        for p in dict(self._paths).keys():
0 ignored issues
show
unused-code introduced by
Consider iterating the dictionary directly instead of calling .keys()
Loading history...
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...
641
            self.remove_path(p)
642
        return self
643
644
    def remove_path(self, path: Path) -> __qualname__:
645
        """
646
        Removes a path handler (limited to those stored here).
647
        Will log an error and continue if the path is not found.
648
649
        See Also:
650
            :meth:`remove_paths`
651
        """
652
        if not self._control_enabled:
653
            return self
654
        path = path.resolve()
655
        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...
656
        if p is not None:
657
            try:
658
                self._logger.remove(p.hid)
659
                del self._paths[path]
660
            except ValueError:
661
                self._logger.exception(f"Cannot remove handler {p.hid} to {path}")
662
        return self
663
664
    def from_cli(
665
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
666
        path: Union[None, str, Path] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
667
        main: Optional[str] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
668
        _msg_level: str = "OFF",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
669
    ) -> __qualname__:
670
        """
671
        This function controls logging set via command-line.
672
        Deletes any existing path handlers.
673
674
        Args:
675
            main: The level for stderr; if None, does not modify
676
            path: If set, the path to a file. Can be prefixed with ``:level:`` to set the level
677
                  (e.g. ``:INFO:mandos-run.log.gz``). Can serialize to JSON if .json is used
678
                  instead of .log or .txt.
679
            _msg_level: Level for messages about this logging change
680
        """
681
        _msg_level = self._aliases.get(_msg_level.upper(), _msg_level.upper())
682
        if not self._control_enabled:
683
            if self._main is not None:
684
                self._main.level = _msg_level  # just set
685
            return self
686
        if main is not None and main is not _SENTINEL:
687
            main = self._aliases.get(main.upper(), main.upper())
688
            if main not in self._defaults.levels_extended:
689
                _permitted = ", ".join(
690
                    [*self._defaults.levels_extended, *self._defaults.aliases.keys()]
691
                )
692
                raise XValueError(
693
                    f"{main.lower()} not a permitted log level (allowed: {_permitted}", value=main
694
                )
695
            self.config_main(level=main)
696
        self.remove_paths()
697
        if path is not None and len(str(path)) > 0:
698
            match = _LOGGER_ARG_PATTERN.match(str(path))
699
            path_level = "DEBUG" if match.group(1) is None else match.group(1)
700
            path = Path(match.group(2))
701
            self.add_path(path, path_level)
702
            if _msg_level is not None:
703
                self._logger.log(_msg_level, f"Added path handler {path} (level {path_level})")
704
        if _msg_level is not None:
705
            self._logger.log(_msg_level, f"Set main log level to {main}")
706
        return self
707
708
    __call__ = from_cli
709
710
    def rewire_streams_to_utf8(self) -> __qualname__:
711
        """
712
        Calls ``reconfigure`` on ``sys.stderr``, ``sys.stdout``, and ``sys.stdin`` to use utf-8.
713
        Use at your own risk.
714
        """
715
        sys.stderr.reconfigure(encoding="utf-8")
716
        sys.stdout.reconfigure(encoding="utf-8")
717
        sys.stdin.reconfigure(encoding="utf-8")
718
        return self
719
720
    @classmethod
721
    def guess_file_sink_info(cls, path: Union[str, Path]) -> LogSinkInfo:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
722
        path = Path(path)
723
        base, compression = path.name, None
724
        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...
725
            if path.name.endswith(c):
726
                base, compression = path.name[: -len(c)], c
727
        if not [base.endswith(s) for s in [".json", ".log", ".txt"]]:
728
            raise XValueError(
729
                f"Log filename {path.name} is not .json, .log, .txt, or a compressed variant",
730
                value=path.name,
731
            )
732
        return LogSinkInfo(
733
            path=path,
734
            base=path.parent / base,
735
            suffix=compression,
736
            serialize=base.endswith(".json"),
737
            compression=compression,
738
        )
739
740
    def _get_info(self, level: str = _SENTINEL, fmt: str = _SENTINEL) -> Tuple[str, int, Formatter]:
741
        if level is _SENTINEL and self._main is None:
742
            level = self._defaults.level
743
        elif level is _SENTINEL:
744
            level = self._main.level
745
        level = level.upper()
746
        if fmt is _SENTINEL and self._main is None:
747
            fmt = self._defaults.fmt_built_in
748
        elif fmt is _SENTINEL:
749
            fmt = self._main.fmt
750
        if isinstance(fmt, str):
751
            fmt = self._defaults.wrap_extended_fmt(fmt=fmt)
752
        ell = self.levels[level]
753
        return level, ell, fmt
754
755
    @classmethod
756
    def built_in(
757
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
758
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
759
        enable_control: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
760
        sink=sys.stderr,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
761
        level: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
762
    ) -> FancyLoguru[Logger]:
763
        """
764
        Creates a new FancyLoguru using standard loguru levels, etc.
765
766
        Args:
767
            enable_control: If False, all calls to add/remove handlers (except :meth:`remember`)
768
                            will be ignored. :meth:`rewire_streams` will also be ignored.
769
                            This is provided so that you can configure both an "application"
770
                            and a library that works for the same code
771
                            with ``enable_control=<is-command-line>``.
772
            sink: The *main* sink to start with
773
            level: The min log level for the main sink
774
        """
775
        return (
776
            FancyLoguru.new(Logger)
777
            .set_control(enable_control)
778
            .config_levels()
779
            .remember()
780
            .config_main(level=level, sink=sink)
781
        )
782
783
    @classmethod
784
    def extended(
785
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
786
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
787
        enable_control: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
788
        sink=sys.stderr,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
789
        level: str = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
790
        simplify_fmt: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
791
        red_green_safe: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
792
    ) -> FancyLoguru[LoggerWithCautionAndNotice]:
793
        """
794
        Creates a new FancyLoguru with extra levels "caution" and "notice".
795
        - *CAUTION*: Bad, but between levels *INFO* and *SUCCESS*
796
        - *NOTICE*: Good/neutral, but between levels *WARNING* and *ERROR*
797
798
        Args:
799
            enable_control: See :meth:`built_in`
800
            sink: See :meth:`built_in`
801
            level: See :meth:`built_in`
802
            simplify_fmt: Use ``DEFAULT_FMT_STRING``
803
            red_green_safe: Modify the standard colors to use blue instead of green
804
        """
805
        defaults = _Defaults()
806
        levels = defaults.levels_extended
807
        icons = defaults.icons_extended
808
        colors = defaults.colors_red_green_safe if red_green_safe else defaults.colors_extended
809
        fmt = defaults.fmt_simplified if simplify_fmt else defaults.fmt_built_in
810
        return (
811
            FancyLoguru.new(LoggerWithCautionAndNotice)
812
            .set_control(enable_control)
813
            .config_levels(levels=levels, colors=colors, icons=icons)
814
            .remember()
815
            .config_main(level=level, sink=sink, fmt=fmt)
816
        )
817
818
819
FANCY_LOGURU_DEFAULTS = _Defaults()
820
821
822
if __name__ == "__main__":
823
    _logger.remove(None)
824
    lg = (
825
        FancyLoguru.new(LoggerWithCautionAndNotice)
826
        .set_control(False)
827
        .set_control(True)
828
        .config_main(fmt=FANCY_LOGURU_DEFAULTS.fmt_simplified)
829
        .intercept_std()
830
    )
831
    lg.from_cli(path="nope.log.tmp")
832
833
    with lg.logger.contextualize(omg="why"):
834
        lg.logger.info("hello", traceback=True)
835
    print(lg.recent_messages)
836
837
838
__all__ = [
839
    "FancyLoguru",
840
    "FANCY_LOGURU_DEFAULTS",
841
    "InterceptHandler",
842
    "HandlerInfo",
843
    "Logger",
844
    "LoggerWithCautionAndNotice",
845
]
846