Passed
Push — main ( 9813db...5006f2 )
by Douglas
01:43
created

mandos.MandosLogging._add_path_logger()   A

Complexity

Conditions 3

Size

Total Lines 17
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nop 2
dl 0
loc 17
rs 9.55
c 0
b 0
f 0
1
"""
2
Metadata for this project.
3
"""
4
5
import logging
6
import sys
7
from importlib.metadata import PackageNotFoundError
0 ignored issues
show
Bug introduced by
The name metadata does not seem to exist in module importlib.
Loading history...
introduced by
Unable to import 'importlib.metadata'
Loading history...
8
from importlib.metadata import metadata as __load
0 ignored issues
show
Bug introduced by
The name metadata does not seem to exist in module importlib.
Loading history...
introduced by
Unable to import 'importlib.metadata'
Loading history...
9
from pathlib import Path
10
from typing import Optional
11
12
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
13
from loguru import logger
0 ignored issues
show
introduced by
Unable to import 'loguru'
Loading history...
14
from loguru._logger import Logger
0 ignored issues
show
introduced by
Unable to import 'loguru._logger'
Loading history...
15
from typeddfs.file_formats import FileFormat, compression_suffixes
0 ignored issues
show
introduced by
Unable to import 'typeddfs.file_formats'
Loading history...
16
from typeddfs import TypedDfs
0 ignored issues
show
introduced by
Unable to import 'typeddfs'
Loading history...
17
18
from mandos.model import MANDOS_SETTINGS
19
20
pkg = "mandos"
0 ignored issues
show
Coding Style Naming introduced by
Constant name "pkg" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\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...
21
_metadata = None
0 ignored issues
show
Coding Style Naming introduced by
Constant name "_metadata" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\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...
22
try:
23
    _metadata = __load(Path(__file__).absolute().parent.name)
24
    __status__ = "Development"
25
    __copyright__ = "Copyright 2020–2021"
26
    __date__ = "2020-08-14"
27
    __uri__ = _metadata["home-page"]
28
    __title__ = _metadata["name"]
29
    __summary__ = _metadata["summary"]
30
    __license__ = _metadata["license"]
31
    __version__ = _metadata["version"]
32
    __author__ = _metadata["author"]
33
    __maintainer__ = _metadata["maintainer"]
34
    __contact__ = _metadata["maintainer"]
35
except PackageNotFoundError:  # pragma: no cover
36
    logger.error(f"Could not load package metadata for {pkg}. Is it installed?")
37
38
39
class MandosMetadata:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
40
    version = __version__
0 ignored issues
show
introduced by
The variable __version__ does not seem to be defined for all execution paths.
Loading history...
41
42
43
class InterceptHandler(logging.Handler):
44
    """
45
    Redirects standard logging to loguru.
46
    """
47
48
    def emit(self, record):
49
        # Get corresponding Loguru level if it exists
50
        try:
51
            level = logger.level(record.levelname).name
52
        except ValueError:
53
            level = record.levelno
54
        # Find caller from where originated the logged message
55
        frame, depth = logging.currentframe(), 2
56
        while frame.f_code.co_filename == logging.__file__:
57
            frame = frame.f_back
58
            depth += 1
59
        logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
60
61
62
def _notice(__message: str, *args, **kwargs):
63
    return logger.log("NOTICE", __message, *args, **kwargs)
64
65
66
def _caution(__message: str, *args, **kwargs):
67
    return logger.log("CAUTION", __message, *args, **kwargs)
68
69
70
class MyLogger(Logger):
71
    """
72
    A wrapper that has a fake notice() method to trick static analysis.
73
    """
74
75
    def notice(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
76
        raise NotImplementedError()  # not real
77
78
    def caution(self, __message: str, *args, **kwargs):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
79
        raise NotImplementedError()  # not real
80
81
82
class MandosLogging:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
83
    # this is required for mandos to run
84
    try:
85
        logger.level("NOTICE", no=35)
86
    except TypeError:
87
        # this happens if it's already been added (e.g. from an outside library)
88
        # if we don't have it set after this, we'll find out soon enough
89
        logger.debug("Could not add 'NOTICE' loguru level. Did you already set it?")
90
    try:
91
        logger.level("CAUTION", no=25)
92
    except TypeError:
93
        logger.debug("Could not add 'CAUTION' loguru level. Did you already set it?")
94
    logger.notice = _notice
95
    logger.caution = _caution
96
97
    @classmethod
98
    def init(cls) -> None:
99
        """
100
        Sets an initial configuration.
101
        """
102
        cls.redirect_std_logging()
103
        cls.set_log_level("INFO", None)
104
105
    @classmethod
106
    def redirect_std_logging(cls, level: int = 10) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
107
        # 10 b/c we're really never going to want trace output
108
        logging.basicConfig(handlers=[InterceptHandler()], level=level)
109
110
    @classmethod
111
    def set_log_level(cls, level: str, path: Optional[Path]) -> None:
112
        """
113
        This function will control all aspects of the logging as set via command-line.
114
115
        Args:
116
            level: The level to use for output to stderr
117
            path: If set, the path to a file. Can be prefixed with ``:level:`` to set the level
118
                  (e.g. ``:INFO:mandos-run.log.gz``). Can serialize to JSON if .json is used
119
                  instead of .log.
120
        """
121
        logger.remove()
122
        logger.add(sys.stderr, level=level)
123
        cls._add_path_logger(path)
124
125
    @classmethod
126
    def _add_path_logger(cls, path: Path) -> None:
127
        if path is None:
128
            return
129
        match = regex.compile(r"(?:[A-Z]+:)??(.*)", flags=regex.V1).match(str(path))
130
        level = "DEBUG" if match.group(1) is None else match.group(1)
131
        path = Path(match.group(2))
132
        serialize = FileFormat.from_path(path) is FileFormat.json
133
        compression = FileFormat.compression_from_path(path).name.lstrip(".")
134
        logger.add(
135
            str(path),
136
            level=level,
137
            compression=compression,
138
            serialize=serialize,
139
            backtrace=True,
140
            diagnose=True,
141
            enqueue=True,
142
        )
143
144
145
# weird as hell, but it works
146
# noinspection PyTypeChecker
147
logger: MyLogger = logger
148
149
150
class MandosSetup:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
151
    def __init__(self):
152
        self._set_up = False
153
154
    def __call__(
155
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
156
        log: Optional[Path],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
157
        quiet: bool,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
158
        verbose: bool,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
159
        no_setup: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
160
    ):
161
        if not self._set_up:
162
            self.run_command_setup(verbose, quiet, log, no_setup)
163
164
    def run_command_setup(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
165
        self, verbose: bool, quiet: bool, log: Optional[Path], skip_setup: bool
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
166
    ) -> None:
167
        if not skip_setup:
168
            level = self._set_logging(verbose, quiet, log)
169
            logger.notice(f"Ready. Set log level to {level}")
170
171
    def _set_logging(self, verbose: bool, quiet: bool, log: Optional[Path]) -> str:
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...
172
        if verbose and quiet:
0 ignored issues
show
Unused Code introduced by
Unnecessary "elif" after "raise"
Loading history...
173
            raise ValueError(f"Cannot set both --quiet and --verbose")
0 ignored issues
show
introduced by
Using an f-string that does not have any interpolated variables
Loading history...
174
        elif quiet:
175
            level = "ERROR"
176
        elif verbose:
177
            level = "INFO"
178
        else:
179
            level = "WARNING"
180
        MandosLogging.set_log_level(level, log)
181
        return level
182
183
184
MANDOS_SETUP = MandosSetup()
185
186
187
if __name__ == "__main__":  # pragma: no cover
188
    if _metadata is not None:
189
        print(f"{pkg} (v{_metadata['version']})")
190
    else:
191
        print("Unknown project info")
192
193
194
__all__ = ["MandosMetadata", "logger", "MandosLogging", "MandosSetup", "MANDOS_SETUP"]
195