Passed
Push — main ( b312d0...29adcf )
by Douglas
01:37
created

pocketutils.logging.fancy_loguru   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 313
dl 0
loc 438
rs 6
c 0
b 0
f 0
wmc 55

18 Methods

Rating   Name   Duplication   Size   Complexity  
A InterceptHandler.emit() 0 12 3
A FormatFactory.plain() 0 6 1
A FormatFactory.with_extras() 0 15 2
A FancyLoguru.paths() 0 5 2
B FancyLoguru.config_levels() 0 16 6
A FancyLoguru.__init__() 0 6 1
A FancyLoguru.remove_path() 0 8 4
A FancyLoguru.logger() 0 3 1
A FancyLoguru.add_path() 0 23 1
B FancyLoguru.from_cli() 0 31 6
C FancyLoguru.config_level() 0 29 9
A FancyLoguru.aliases() 0 3 1
A FancyLoguru.levels() 0 3 1
A FancyLoguruExtras.force_streams_to_utf8() 0 6 1
A LogSinkInfo.guess() 0 17 4
A FancyLoguru.main() 0 6 2
A FancyLoguru.init() 0 17 2
B FancyLoguru.config_main() 0 29 8

How to fix   Complexity   

Complexity

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