Passed
Push — main ( cee75c...37036d )
by Douglas
02:08
created

mandos.model.utils.setup.MandosSetup.__call__()   B

Complexity

Conditions 6

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 21
nop 4
dl 0
loc 32
rs 8.4426
c 0
b 0
f 0
1
import logging
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import sys
3
from pathlib import Path
4
from typing import Optional
5
6
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
7
from loguru import logger
0 ignored issues
show
introduced by
Unable to import 'loguru'
Loading history...
8
9
from loguru._logger import Logger
0 ignored issues
show
introduced by
Unable to import 'loguru._logger'
Loading history...
10
from typeddfs import FileFormat
0 ignored issues
show
introduced by
Unable to import 'typeddfs'
Loading history...
11
12
13
_LOGGER_ARG_PATTERN = regex.compile(r"(?:[A-Z]+:)??(.*)", flags=regex.V1)
14
15
16
class InterceptHandler(logging.Handler):
17
    """
18
    Redirects standard logging to loguru.
19
    """
20
21
    def emit(self, record):
22
        # Get corresponding Loguru level if it exists
23
        try:
24
            level = logger.level(record.levelname).name
25
        except ValueError:
26
            level = record.levelno
27
        # Find caller from where originated the logged message
28
        frame, depth = logging.currentframe(), 2
29
        while frame.f_code.co_filename == logging.__file__:
30
            frame = frame.f_back
31
            depth += 1
32
        logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
33
34
35
def _notice(__message: str, *args, **kwargs):
36
    return logger.log("NOTICE", __message, *args, **kwargs)
37
38
39
def _caution(__message: str, *args, **kwargs):
40
    return logger.log("CAUTION", __message, *args, **kwargs)
41
42
43
class MyLogger(Logger):
44
    """
45
    A wrapper that has a fake notice() method to trick static analysis.
46
    """
47
48
    def notice(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
49
        raise NotImplementedError()  # not real
50
51
    def caution(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
52
        raise NotImplementedError()  # not real
53
54
55
class MandosLogging:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
56
    # this is required for mandos to run
57
58
    DEFAULT_LEVEL: str = "NOTICE"
59
60
    @classmethod
61
    def init(cls) -> None:
62
        """
63
        Sets an initial configuration.
64
        """
65
        cls._init()
66
        cls.redirect_std_logging()
67
        cls.set_main_level(MandosLogging.DEFAULT_LEVEL)
68
69
    @classmethod
70
    def _init(cls) -> None:
71
        try:
72
            logger.level("NOTICE", no=35)
73
        except TypeError:
74
            # this happens if it's already been added (e.g. from an outside library)
75
            # if we don't have it set after this, we'll find out soon enough
76
            logger.debug("Could not add 'NOTICE' loguru level. Did you already set it?")
77
        try:
78
            logger.level("CAUTION", no=25)
79
        except TypeError:
80
            logger.debug("Could not add 'CAUTION' loguru level. Did you already set it?")
81
        logger.notice = _notice
82
        logger.caution = _caution
83
84
    @classmethod
85
    def redirect_std_logging(cls, level: int = 10) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
86
        # 10 b/c we're really never going to want trace output
87
        logging.basicConfig(handlers=[InterceptHandler()], level=level)
88
89
    @classmethod
90
    def set_main_level(cls, level: str) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
91
        logger.remove()
92
        logger.add(sys.stderr, level=level)
93
94
    @classmethod
95
    def disable_main(cls) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
96
        logger.remove()
97
98
    @classmethod
99
    def add_path_logger(cls, path: Path, level: str) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
100
        serialize = FileFormat.from_path(path) is FileFormat.json
101
        compression = FileFormat.compression_from_path(path).name.lstrip(".")
102
        logger.add(
103
            str(path),
104
            level=level,
105
            compression=compression,
106
            serialize=serialize,
107
            backtrace=True,
108
            diagnose=True,
109
            enqueue=True,
110
        )
111
112
113
class MandosSetup:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
114
115
    LEVELS = ["off", "error", "notice", "warning", "caution", "info", "debug"]
116
    ALIASES = dict(none="off", no="off", verbose="info", quiet="error")
117
118
    def __call__(
119
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
120
        log: Optional[Path] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
121
        level: str = MandosLogging.DEFAULT_LEVEL,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
122
        skip: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
123
    ) -> None:
124
        """
125
        This function controls all aspects of the logging as set via command-line.
126
127
        Args:
128
            level: The level for stderr
129
            log: If set, the path to a file. Can be prefixed with ``:level:`` to set the level
130
                  (e.g. ``:INFO:mandos-run.log.gz``). Can serialize to JSON if .json is used
131
                  instead of .log or .txt.
132
        """
133
        if skip:
134
            return
135
        level = MandosSetup.ALIASES.get(level.lower(), level).upper()
136
        if level not in MandosSetup.LEVELS:
137
            _permitted = ", ".join([*MandosSetup.LEVELS, *MandosSetup.ALIASES.keys()])
138
            raise ValueError(f"{level} not a permitted log level (allowed: {_permitted}")
139
        if level == "OFF":
140
            MandosLogging.disable_main()
141
        else:
142
            MandosLogging.set_main_level(level)
143
        match = _LOGGER_ARG_PATTERN.match(str(log))
144
        if log is not None:
145
            path_level = "DEBUG" if match.group(1) is None else match.group(1)
146
            path = Path(match.group(2))
147
            MandosLogging.add_path_logger(path, path_level)
148
            logger.info(f"Added logger to {path} at level {path_level}")
149
        logger.notice(f"Ready. Set log level to {level}")
150
151
152
# weird as hell, but it works
153
# noinspection PyTypeChecker
154
logger: MyLogger = logger
155
156
157
MANDOS_SETUP = MandosSetup()
158