Passed
Push — main ( e0f181...a5d2a4 )
by Douglas
01:37
created

pocketutils.misc.fancy_loguru   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 559
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 399
dl 0
loc 559
rs 2.96
c 0
b 0
f 0
wmc 68

24 Methods

Rating   Name   Duplication   Size   Complexity  
C FancyLoguru.config_level() 0 32 10
A LoggerWithNotice.notice() 0 2 1
A FancyLoguruExtras.built_in() 0 10 2
A FancyLoguru.paths() 0 5 2
A FancyLoguru.main() 0 6 2
A FancyLoguru.add_path() 0 23 1
B FancyLoguru.from_cli() 0 31 6
A FancyLoguru.logger() 0 3 1
A LoggerWithCaution.caution() 0 2 1
A FancyLoguru.aliases() 0 3 1
A FancyLoguru.remove_path() 0 8 4
A FancyLoguruExtras.extended() 0 28 4
A FancyLoguru.__init__() 0 6 1
A FancyLoguru._set_logger_levels() 0 10 3
A FancyLoguru.new() 0 3 1
B FancyLoguru.config_levels() 0 23 6
A FormatFactory.with_extras() 0 15 2
A FancyLoguru.init() 0 17 2
A InterceptHandler.emit() 0 12 3
A FormatFactory.plain() 0 6 1
A FancyLoguruExtras.rewire_streams_to_utf8() 0 6 1
A FancyLoguru.levels() 0 3 1
B FancyLoguru.config_main() 0 29 8
A LogSinkInfo.guess() 0 17 4

How to fix   Complexity   

Complexity

Complex classes like pocketutils.misc.fancy_loguru 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
- mandes utf-8
12
"""
13
from __future__ import annotations
14
15
import abc
16
import logging
17
import os
18
import sys
19
from dataclasses import dataclass
20
from inspect import cleandoc
21
from pathlib import Path
22
from typing import (
23
    AbstractSet,
24
    Any,
25
    Callable,
26
    Generic,
27
    Mapping,
28
    Optional,
29
    TextIO,
30
    TypeVar,
31
    Union,
32
    Type,
33
    Sequence,
34
)
35
36
# noinspection PyProtectedMember
37
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
38
from loguru import logger
0 ignored issues
show
introduced by
Unable to import 'loguru'
Loading history...
39
import loguru._defaults as _defaults
0 ignored issues
show
introduced by
Unable to import 'loguru._defaults'
Loading history...
40
41
# noinspection PyProtectedMember
42
from loguru._logger import Logger
0 ignored issues
show
introduced by
Unable to import 'loguru._logger'
Loading history...
43
44
# noinspection PyProtectedMember
45
from loguru._logger import Logger
0 ignored issues
show
Unused Code introduced by
The import Logger was already done on line 42. You should be able to
remove this line.
Loading history...
introduced by
Unable to import 'loguru._logger'
Loading history...
46
47
from pocketutils.core.exceptions import IllegalStateError, XValueError
48
49
Formatter = Union[str, Callable[[Mapping[str, Any]], str]]
50
_FMT = cleandoc(
51
    r"""
52
    <bold>{time:YYYY-MM-DD HH:mm:ss.SSS}</bold> |
53
    <level>{level: <8}</level> |
54
    <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan>
55
    — <level>{message}[EXTRA]</level>
56
    {exception}
57
    """
58
).replace("\n", " ")
59
60
61
class FormatFactory:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
62
    @classmethod
63
    def with_extras(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
64
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
65
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
66
        fmt: str = _FMT,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
67
        sep: str = "; ",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
68
        eq_sign: str = " ",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
69
    ) -> Callable[[Mapping[str, Any]], str]:
70
        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...
71
            extra = sep.join([e + eq_sign + "{extra[" + e + "]}" for e in record["extra"].keys()])
72
            if len(extra) > 0:
73
                extra = f" [ {extra} ]"
74
            return fmt.replace("[EXTRA]", extra) + os.linesep
75
76
        return FMT
77
78
    @classmethod
79
    def plain(cls, *, fmt: str = _FMT) -> Callable[[Mapping[str, Any]], str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
80
        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...
81
            return fmt.replace("[EXTRA]", "")
82
83
        return FMT
84
85
86
class _SENTINEL:
87
    pass
88
89
90
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...
91
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...
92
93
94
_LOGGER_ARG_PATTERN = regex.compile(r"(?:([a-zA-Z]+):)?(.*)", flags=regex.V1)
95
log_compressions = {
96
    ".xz",
97
    ".lzma",
98
    ".gz",
99
    ".zip",
100
    ".bz2",
101
    ".tar",
102
    ".tar.gz",
103
    ".tar.bz2",
104
    ".tar.xz",
105
}
106
valid_log_suffixes = {
107
    *{f".log{c}" for c in log_compressions},
108
    *{f".txt{c}" for c in log_compressions},
109
    *{f".json{c}" for c in log_compressions},
110
}
111
112
113
class FancyLoguruDefaults:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
114
    levels_built_in = dict(
115
        TRACE=_defaults.LOGURU_TRACE_NO,
116
        DEBUG=_defaults.LOGURU_DEBUG_NO,
117
        INFO=_defaults.LOGURU_INFO_NO,
118
        WARNING=_defaults.LOGURU_WARNING_NO,
119
        ERROR=_defaults.LOGURU_ERROR_NO,
120
        CRITICAL=_defaults.LOGURU_CRITICAL_NO,
121
    )
122
123
    # the levels for caution and notice are DEFINED here
124
    # trace and success must match loguru's
125
    # and the rest must match logging's
126
    # note that most of these alternate between informative and problematic
127
    # i.e. info (ok), caution (bad), success (ok), warning (bad), notice (ok), error (bad)
128
    levels_extended = {
129
        **levels_built_in,
130
        **dict(
131
            CAUTION=23,
132
            SUCCESS=25,
133
            NOTICE=35,
134
        ),
135
    }
136
137
    colors_built_in = dict(
138
        TRACE=_defaults.LOGURU_TRACE_COLOR,
139
        DEBUG=_defaults.LOGURU_DEBUG_COLOR,
140
        INFO=_defaults.LOGURU_INFO_COLOR,
141
        SUCCESS=_defaults.LOGURU_SUCCESS_COLOR,
142
        WARNING=_defaults.LOGURU_WARNING_COLOR,
143
        ERROR=_defaults.LOGURU_ERROR_COLOR,
144
        CRITICAL=_defaults.LOGURU_CRITICAL_COLOR,
145
    )
146
147
    colors_extended = dict(
148
        **colors_built_in,
149
        CAUTION=_defaults.LOGURU_WARNING_COLOR,
150
        NOTICE=_defaults.LOGURU_INFO_COLOR,
151
    )
152
153
    colors_red_green_safe = dict(
154
        TRACE="<dim>",
155
        DEBUG="<dim>",
156
        INFO="<bold>",
157
        CAUTION="<yellow>",
158
        SUCCESS="<blue>",
159
        WARNING="<yellow>",
160
        NOTICE="<blue>",
161
        ERROR="<red>",
162
        CRITICAL="<red>",
163
    )
164
165
    icons_built_in = dict(
166
        TRACE=_defaults.LOGURU_TRACE_ICON,
167
        DEBUG=_defaults.LOGURU_DEBUG_ICON,
168
        INFO=_defaults.LOGURU_INFO_ICON,
169
        SUCCESS=_defaults.LOGURU_SUCCESS_ICON,
170
        WARNING=_defaults.LOGURU_WARNING_ICON,
171
        ERROR=_defaults.LOGURU_ERROR_ICON,
172
        CRITICAL=_defaults.LOGURU_CRITICAL_ICON,
173
    )
174
175
    icons_extended = dict(
176
        **icons_built_in,
177
        CAUTION="⚐",
178
        NOTICE="★",
179
    )
180
181
    level: str = "INFO"
182
183
    fmt_simplified = FormatFactory.with_extras()
184
    fmt_built_in = FormatFactory.plain()
185
    fmt_built_in_raw = _defaults.LOGURU_FORMAT
186
187
    aliases = dict(NONE=None, NO=None, OFF=None, VERBOSE="INFO", QUIET="ERROR")
188
189
190
@dataclass(frozen=True, repr=True, order=True)
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
191
class HandlerInfo:
192
    hid: int
193
    path: Optional[Path]
194
    level: Optional[str]
195
    fmt: Formatter
196
197
198
@dataclass(frozen=False, repr=True)
199
class _HandlerInfo:
200
    hid: int
201
    sink: Any
202
    level: Optional[int]
203
    fmt: Formatter
204
205
206
@dataclass(frozen=True, repr=True, order=True)
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
207
class LogSinkInfo:
208
    path: Path
209
    base: Path
210
    suffix: str
211
    serialize: bool
212
    compression: Optional[str]
213
214
    @classmethod
215
    def guess(cls, path: Union[str, Path]) -> LogSinkInfo:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
216
        path = Path(path)
217
        base, compression = path.name, None
218
        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...
219
            if path.name.endswith(c):
220
                base, compression = path.name[: -len(c)], c
221
        if not [base.endswith(s) for s in [".json", ".log", ".txt"]]:
222
            raise XValueError(
223
                f"Log filename {path.name} is not .json, .log, .txt, or a compressed variant"
224
            )
225
        return LogSinkInfo(
226
            path=path,
227
            base=path.parent / base,
228
            suffix=compression,
229
            serialize=base.endswith(".json"),
230
            compression=compression,
231
        )
232
233
234
class InterceptHandler(logging.Handler):
235
    """
236
    Redirects standard logging to loguru.
237
    """
238
239
    def emit(self, record):
240
        # Get corresponding Loguru level if it exists
241
        try:
242
            level = logger.level(record.levelname).name
243
        except ValueError:
244
            level = record.levelno
245
        # Find caller from where originated the logged message
246
        frame, depth = logging.currentframe(), 2
247
        while frame.f_code.co_filename == logging.__file__:
248
            frame = frame.f_back
249
            depth += 1
250
        logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
251
252
253
class FancyLoguru(Generic[T]):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
254
    def __init__(self, log: T = logger):
255
        self._levels = dict(FancyLoguruDefaults.levels_built_in)
256
        self._logger = log
257
        self._main = None
258
        self._paths = {}
259
        self._aliases = dict(FancyLoguruDefaults.aliases)
260
261
    @staticmethod
262
    def new(t: Type[Z]) -> FancyLoguru[Z]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
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...
Unused Code introduced by
The argument t seems to be unused.
Loading history...
263
        return FancyLoguru[Z](logger)
264
265
    @property
266
    def logger(self) -> T:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
267
        return self._logger
268
269
    @property
270
    def levels(self) -> Mapping[str, int]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
271
        return dict(self._levels)
272
273
    @property
274
    def aliases(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
275
        return self._aliases
276
277
    @property
278
    def main(self) -> Optional[HandlerInfo]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
279
        if self._main is None:
280
            return None
281
        return HandlerInfo(
282
            hid=self._main.hid, level=self._main.level, fmt=self._main.fmt, path=None
283
        )
284
285
    @property
286
    def paths(self) -> AbstractSet[HandlerInfo]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
287
        if self._main is None:
288
            return set()
289
        return {HandlerInfo(hid=h.hid, level=h.level, fmt=h.fmt, path=h.sink) for h in self._paths}
290
291
    def config_levels(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
292
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
293
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
294
        levels: Mapping[str, int] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
295
        colors: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
296
        icons: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
297
        aliases: Mapping[str, str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
298
        add_methods: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
299
    ) -> __qualname__:
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable '__qualname__'
Loading history...
300
        levels = FancyLoguruDefaults.levels_extended if levels is _SENTINEL else levels
301
        colors = FancyLoguruDefaults.colors_extended if colors is _SENTINEL else colors
302
        icons = FancyLoguruDefaults.icons_extended if icons is _SENTINEL else icons
303
        aliases = FancyLoguruDefaults.aliases if aliases is _SENTINEL else aliases
304
        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...
305
            self.config_level(
306
                k,
307
                v,
308
                color=colors.get(k, _SENTINEL),
309
                icon=icons.get(k, _SENTINEL),
310
                add_methods=add_methods,
311
            )
312
        self._aliases = dict(aliases)
313
        return self
314
315
    def init(
316
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
317
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
318
        level: str = FancyLoguruDefaults.level,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
319
        sink=sys.stderr,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
320
        fmt: Formatter = FancyLoguruDefaults.fmt_built_in,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
321
        intercept: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
322
    ) -> __qualname__:
323
        """
324
        Sets an initial configuration.
325
        """
326
        if intercept:
327
            # noinspection PyArgumentList
328
            logging.basicConfig(handlers=[InterceptHandler()], level=0, encoding="utf-8")
329
        self.logger.remove(None)  # get rid of the built-in handler
330
        self.config_main(level=level, sink=sink, fmt=fmt)
331
        return self
332
333
    def config_level(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
334
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
335
        name: str,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
336
        level: int,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
337
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
338
        color: Union[None, str, _SENTINEL] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
339
        icon: Union[None, str, _SENTINEL] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
340
        add_methods: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
341
        replace: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
342
    ) -> __qualname__:
343
        try:
344
            data = logger.level(name)
345
        except ValueError:
346
            data = None
347
        if data is None:
348
            logger.level(
349
                name,
350
                no=level,
351
                color=None if color is _SENTINEL else color,
352
                icon=None if icon is _SENTINEL else icon,
353
            )
354
        elif replace:
355
            if level != data.no:  # loguru doesn't check whether they're eq; it just errors
356
                raise IllegalStateError(f"Cannot set level={level}!={data.no} for {name}")
357
            logger.level(
358
                name,
359
                color=data.color if color is _SENTINEL else color,
360
                icon=data.icon if icon is _SENTINEL else icon,
361
            )
362
        if add_methods:
363
            self._set_logger_levels([name])
364
        return self
365
366
    def _set_logger_levels(self, levels: Sequence[str]) -> __qualname__:
367
        for level in levels:
368
369
            def _x(__message: str, *args, **kwargs):
370
                logger.log(level, __message, *args, **kwargs)
0 ignored issues
show
introduced by
The variable level does not seem to be defined in case the for loop on line 367 is not entered. Are you sure this can never be the case?
Loading history...
introduced by
Cell variable level defined in loop
Loading history...
371
372
            _x.__name__ = level.lower()
373
            if not hasattr(self._logger, level.lower()):
374
                setattr(self._logger, level.lower(), _x)
375
        return self
376
377
    def config_main(
378
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
379
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
380
        sink: TextIO = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
381
        level: Optional[str] = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
382
        fmt: Formatter = _SENTINEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
383
    ) -> __qualname__:
384
        """
385
        Sets the logging level for the main handler (normally stderr).
386
        """
387
        if level is not None and level is not _SENTINEL:
388
            level = level.upper()
389
        if self._main is None:
390
            self._main = _HandlerInfo(
391
                hid=-1,
392
                sink=sys.stderr,
393
                level=self._levels[FancyLoguruDefaults.level],
394
                fmt=FancyLoguruDefaults.fmt_built_in,
395
            )
396
        else:
397
            try:
398
                logger.remove(self._main.hid)
399
            except ValueError:
400
                logger.error(f"Cannot remove handler {self._main.hid}")
401
        self._main.level = self._main.level if level is _SENTINEL else level
402
        self._main.sink = self._main.sink if sink is _SENTINEL else sink
403
        self._main.fmt = self._main.fmt if fmt is _SENTINEL else fmt
404
        self._main.hid = logger.add(self._main.sink, level=self._main.level, format=self._main.fmt)
405
        return self
406
407
    def remove_path(self, path: Path) -> __qualname__:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
408
        for k, h in self._paths.items():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "h" 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...
409
            if h.path.resolve() == path.resolve():
410
                h.level = None
411
                try:
412
                    logger.remove(k)
413
                except ValueError:
414
                    logger.error(f"Cannot remove handler {k} to {path}")
415
416
    def add_path(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
417
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
418
        path: Path,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
419
        level: str = FancyLoguruDefaults.level,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
420
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
421
        fmt: str = FancyLoguruDefaults.fmt_built_in,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
422
    ) -> __qualname__:
423
        level = level.upper()
424
        ell = self._levels[level]
425
        info = LogSinkInfo.guess(path)
426
        x = logger.add(
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
427
            str(info.base),
428
            format=fmt,
429
            level=level,
430
            compression=info.compression,
431
            serialize=info.serialize,
432
            backtrace=True,
433
            diagnose=True,
434
            enqueue=True,
435
            encoding="utf-8",
436
        )
437
        self._paths[x] = _HandlerInfo(hid=x, sink=info.base, level=ell, fmt=fmt)
438
        return self
439
440
    def from_cli(
441
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
442
        path: Union[None, str, Path] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
443
        main: Optional[str] = FancyLoguruDefaults.level,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
444
    ) -> __qualname__:
445
        """
446
        This function controls logging set via command-line.
447
448
        Args:
449
            main: The level for stderr
450
            path: If set, the path to a file. Can be prefixed with ``:level:`` to set the level
451
                  (e.g. ``:INFO:mandos-run.log.gz``). Can serialize to JSON if .json is used
452
                  instead of .log or .txt.
453
        """
454
        if main is None:
455
            main = FancyLoguruDefaults.level
456
        main = self._aliases.get(main.upper(), main.upper())
457
        if main not in FancyLoguruDefaults.levels_extended:
458
            _permitted = ", ".join(
459
                [*FancyLoguruDefaults.levels_extended, *FancyLoguruDefaults.aliases.keys()]
460
            )
461
            raise XValueError(f"{main.lower()} not a permitted log level (allowed: {_permitted}")
462
        self.config_main(level=main)
463
        if path is not None or len(str(path)) == 0:
464
            match = _LOGGER_ARG_PATTERN.match(str(path))
465
            path_level = "DEBUG" if match.group(1) is None else match.group(1)
466
            path = Path(match.group(2))
467
            self.add_path(path, path_level)
468
            self.logger.info(f"Added logger to {path} at level {path_level}")
469
        self.logger.info(f"Set main log level to {main}")
470
        return self
471
472
    __call__ = from_cli
473
474
475
class LoggerWithNotice(Logger, metaclass=abc.ABCMeta):
476
    """
477
    A wrapper that has fake methods to trick static analysis.
478
    """
479
480
    def notice(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
481
        raise NotImplementedError()  # not real
482
483
484
class LoggerWithCaution(Logger, metaclass=abc.ABCMeta):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
485
    def caution(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
486
        raise NotImplementedError()  # not real
487
488
489
class LoggerWithCautionAndNotice(LoggerWithNotice, LoggerWithCaution, metaclass=abc.ABCMeta):
490
    """
491
    A wrapper that has fake methods to trick static analysis.
492
    """
493
494
495
class FancyLoguruExtras:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
496
    @classmethod
497
    def rewire_streams_to_utf8(cls) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
498
        # we warn the user about this in the docs!
499
        sys.stderr.reconfigure(encoding="utf-8")
500
        sys.stdout.reconfigure(encoding="utf-8")
501
        sys.stdin.reconfigure(encoding="utf-8")
502
503
    @classmethod
504
    def built_in(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
505
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
506
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
507
        level: str = FancyLoguruDefaults.level,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
508
        rewire: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
509
    ) -> FancyLoguru[Logger]:
510
        if rewire:
511
            cls.rewire_streams_to_utf8()
512
        return FancyLoguru.new(Logger).config_levels().init(level=level)
513
514
    @classmethod
515
    def extended(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
516
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
517
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
518
        level: str = FancyLoguruDefaults.level,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
519
        simplify_fmt: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
520
        red_green_safe: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
521
        rewire: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
522
    ) -> FancyLoguru[LoggerWithCautionAndNotice]:
523
        if rewire:
524
            cls.rewire_streams_to_utf8()
525
        return (
526
            FancyLoguru.new(LoggerWithCautionAndNotice)
527
            .config_levels(
528
                levels=FancyLoguruDefaults.levels_extended,
529
                colors=(
530
                    FancyLoguruDefaults.colors_red_green_safe
531
                    if red_green_safe
532
                    else FancyLoguruDefaults.colors_extended
533
                ),
534
                icons=FancyLoguruDefaults.icons_extended,
535
            )
536
            .init(
537
                level=level,
538
                fmt=(
539
                    FancyLoguruDefaults.fmt_simplified
540
                    if simplify_fmt
541
                    else FancyLoguruDefaults.fmt_built_in
542
                ),
543
            )
544
        )
545
546
547
if __name__ == "__main__":
548
    _logger = FancyLoguruExtras.extended().logger
549
    _logger.caution("hello")
550
551
552
__all__ = [
553
    "FancyLoguruDefaults",
554
    "FancyLoguru",
555
    "FancyLoguruExtras",
556
    "HandlerInfo",
557
    "Logger",
558
    "LoggerWithCautionAndNotice",
559
]
560