Passed
Push — main ( ec3fe3...82dd22 )
by Douglas
02:00
created

mandos.MandosSetup._set_logging()   A

Complexity

Conditions 5

Size

Total Lines 11
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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