Passed
Push — main ( 75b58f...d2739b )
by Douglas
01:59
created

FancyLoguru.config_main()   F

Complexity

Conditions 15

Size

Total Lines 44
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 37
nop 6
dl 0
loc 44
rs 2.9998
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like pocketutils.misc.loguru_utils.FancyLoguru.config_main() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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