Passed
Push — dependabot/pip/selenium-4.0.0 ( 707494...3d45c5 )
by
unknown
02:02
created

mandos.cli.main()   A

Complexity

Conditions 4

Size

Total Lines 16
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
nop 0
dl 0
loc 16
rs 9.65
c 0
b 0
f 0
1
"""
2
Command-line interface for mandos.
3
"""
4
5
from __future__ import annotations
6
7
import time
8
from typing import Type
9
10
import typer
0 ignored issues
show
introduced by
Unable to import 'typer'
Loading history...
11
from loguru import logger
0 ignored issues
show
introduced by
Unable to import 'loguru'
Loading history...
12
from pocketutils.core import DictNamespace
0 ignored issues
show
introduced by
Unable to import 'pocketutils.core'
Loading history...
13
from pocketutils.misc.loguru_utils import FancyLoguru
0 ignored issues
show
introduced by
Unable to import 'pocketutils.misc.loguru_utils'
Loading history...
14
from pocketutils.tools.filesys_tools import FilesysTools
0 ignored issues
show
introduced by
Unable to import 'pocketutils.tools.filesys_tools'
Loading history...
15
from pocketutils.tools.sys_tools import SystemTools
0 ignored issues
show
introduced by
Unable to import 'pocketutils.tools.sys_tools'
Loading history...
16
from typer.models import CommandInfo
0 ignored issues
show
introduced by
Unable to import 'typer.models'
Loading history...
17
18
from mandos.model.utils.globals import Globals
19
20
cli = typer.Typer()
21
# .disable("chembl_webresource_client", "requests_cache", "urllib3", "numba")
22
_filter = {"": "WARNING", "mandos": "TRACE"}
23
24
25
def _msg(msg: str):
26
    typer.echo(msg)
27
28
29
def _err(msg: str):
30
    msg = typer.style(msg, fg=typer.colors.RED)
31
    typer.echo(msg, err=True)
32
33
34
class CmdNamespace(DictNamespace):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
35
    @classmethod
36
    def make(cls) -> CmdNamespace:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
37
        from mandos.entry.calc_commands import CalcCommands
0 ignored issues
show
introduced by
Import outside toplevel (mandos.entry.calc_commands.CalcCommands)
Loading history...
38
        from mandos.entry.entry_commands import Entries
0 ignored issues
show
introduced by
Import outside toplevel (mandos.entry.entry_commands.Entries)
Loading history...
39
        from mandos.entry.misc_commands import (
0 ignored issues
show
introduced by
Import outside toplevel (mandos.entry.misc_commands.MiscCommands, mandos.entry.misc_commands._InsertedCommandListSingleton)
Loading history...
40
            MiscCommands,
41
            _InsertedCommandListSingleton,
42
        )
43
        from mandos.entry.plot_commands import PlotCommands
0 ignored issues
show
introduced by
Import outside toplevel (mandos.entry.plot_commands.PlotCommands)
Loading history...
44
45
        cli.registered_commands += [
46
            CommandInfo(":document", callback=MiscCommands.document),
47
            CommandInfo(":search", callback=MiscCommands.search),
48
            CommandInfo(":init", callback=MiscCommands.init, hidden=True),
49
            CommandInfo(":settings", callback=MiscCommands.list_settings, hidden=True),
50
            CommandInfo(":fill", callback=MiscCommands.fill),
51
            CommandInfo(":cache:data", callback=MiscCommands.cache_data),
52
            CommandInfo(":cache:taxa", callback=MiscCommands.cache_taxa),
53
            CommandInfo(":cache:g2p", callback=MiscCommands.cache_g2p),
54
            CommandInfo(":cache:clear", callback=MiscCommands.cache_clear),
55
            CommandInfo(":export:taxa", callback=MiscCommands.export_taxa),
56
            CommandInfo(":concat", callback=MiscCommands.concat),
57
            CommandInfo(":filter", callback=MiscCommands.filter),
58
            CommandInfo(":export:copy", callback=MiscCommands.export_copy),
59
            CommandInfo(":export:state", callback=MiscCommands.export_state),
60
            CommandInfo(":export:reify", callback=MiscCommands.export_reify),
61
            CommandInfo(":export:db", callback=MiscCommands.export_db, hidden=True),
62
            CommandInfo(":init-db", callback=MiscCommands.init_db, hidden=True),
63
            CommandInfo(":serve", callback=MiscCommands.serve, hidden=True),
64
            CommandInfo(":calc:enrichment", callback=CalcCommands.calc_enrichment),
65
            CommandInfo(":calc:phi", callback=CalcCommands.calc_phi),
66
            CommandInfo(":calc:psi", callback=CalcCommands.calc_psi),
67
            CommandInfo(":calc:ecfp", callback=CalcCommands.calc_ecfp, hidden=True),
68
            CommandInfo(":calc:psi-projection", callback=CalcCommands.calc_projection),
69
            CommandInfo(":calc:tau", callback=CalcCommands.calc_tau),
70
            CommandInfo(":plot:enrichment", callback=PlotCommands.plot_enrichment),
71
            CommandInfo(":plot:psi-projection", callback=PlotCommands.plot_projection),
72
            CommandInfo(":plot:psi-heatmap", callback=PlotCommands.plot_heatmap),
73
            CommandInfo(":plot:phi-vs-psi", callback=PlotCommands.plot_phi_psi),
74
            CommandInfo(":plot:tau", callback=PlotCommands.plot_tau),
75
        ]
76
        commands = {c.name: c for c in cli.registered_commands}
77
        # Oh dear this is a nightmare
78
        # it's really hard to create typer commands with dynamically configured params --
79
        # we really need to rely on its inferring of params
80
        # that makes this really hard to do well
81
        for entry in Entries:
82
            cmd = entry.cmd()
83
            info = CommandInfo(cmd, callback=entry.run)
84
            cli.registered_commands.append(info)
85
            # print(f"Registered {entry.cmd()} to {entry}")
86
            commands[cmd] = entry.run
87
        _InsertedCommandListSingleton.commands = cli.registered_commands
88
        return cls(**commands)
89
90
91
class MandosCli:
92
    """
93
    Global entry point for various stuff.
94
    """
95
96
    cli = cli
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable cli does not seem to be defined.
Loading history...
97
    commands = None
98
    log_setup: FancyLoguru = None
99
100
    @classmethod
101
    def as_library(cls) -> Type[MandosCli]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
102
        from mandos.model.utils.setup import LOG_SETUP
0 ignored issues
show
introduced by
Import outside toplevel (mandos.model.utils.setup.LOG_SETUP)
Loading history...
103
104
        Globals.is_cli = False
105
        cls.log_setup = (
106
            LOG_SETUP.set_control(False)
107
            .config_levels(
108
                levels=LOG_SETUP.defaults.levels_extended,
109
                icons=LOG_SETUP.defaults.icons_extended,
110
                colors=LOG_SETUP.defaults.colors_extended,
111
            )
112
            .add_log_methods()
113
        )
114
        cls.start()
115
        cls.commands = CmdNamespace.make()
116
        return cls
117
118
    @classmethod
119
    def as_cli(cls) -> Type[MandosCli]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
120
        from mandos.model.utils.setup import LOG_SETUP
0 ignored issues
show
introduced by
Import outside toplevel (mandos.model.utils.setup.LOG_SETUP)
Loading history...
121
122
        Globals.is_cli = True
123
        cls.log_setup = LOG_SETUP.logger.remove(None)
124
        cls.log_setup = (
125
            LOG_SETUP.set_control(True)
126
            .config_levels(
127
                levels=LOG_SETUP.defaults.levels_extended,
128
                icons=LOG_SETUP.defaults.icons_extended,
129
                colors=LOG_SETUP.defaults.colors_red_green_safe,
130
            )
131
            .add_log_methods()
132
            .config_main(fmt=LOG_SETUP.defaults.fmt_simplified, filter=_filter)
133
            .intercept_std()
134
        )
135
        cls.start()
136
        cls.commands = CmdNamespace.make()
137
        cls.init_apis()
138
        return cls
139
140
    @classmethod
141
    def init_apis(cls):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
142
        from mandos.entry.api_singletons import Apis
0 ignored issues
show
introduced by
Import outside toplevel (mandos.entry.api_singletons.Apis)
Loading history...
143
144
        Apis.set_default()
145
146
    @classmethod
147
    def start(cls):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
148
        from mandos import MandosMetadata
0 ignored issues
show
introduced by
Import outside toplevel (mandos.MandosMetadata)
Loading history...
149
        from mandos.model.utils.setup import logger
0 ignored issues
show
introduced by
Import outside toplevel (mandos.model.utils.setup.logger)
Loading history...
Comprehensibility Bug introduced by
logger is re-defining a name which is already available in the outer-scope (previously defined on line 11).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
Unused Code introduced by
The import logger was already done on line 11. You should be able to
remove this line.
Loading history...
150
151
        if MandosMetadata.version is None:
152
            logger.error("Could not load package metadata for mandos. Is it installed?")
153
        else:
154
            logger.info(f"Mandos v{MandosMetadata.version}")
155
156
157
class MandosTyperCli:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
158
    def __init__(self):
159
        self._mandos = None
160
161
    def main(self) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
162
        try:
163
            self._mandos = MandosCli.as_cli()
164
            self._mandos.cli()
165
        except (KeyboardInterrupt, typer.Abort) as e:
0 ignored issues
show
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...
166
            self._fail(e, abort=True)
167
            quit(1)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
168
        except (SystemExit, typer.Exit) as e:
0 ignored issues
show
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...
169
            _err("Abnormal system exit.")
170
            self._fail(e, abort=True)
171
            quit(2)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
172
        except BaseException as e:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as BaseException is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

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...
173
            _err("Error.")
174
            self._fail(e, abort=False)
175
            quit(-1)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
176
177
    def _fail(self, e: BaseException, *, abort: bool) -> None:
0 ignored issues
show
Coding Style Naming introduced by
Argument 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...
178
        if not abort or True:
179
            msg = (
180
                "\n".join(SystemTools.serialize_exception_msg(e))
181
                .replace("[ exc_info True ]", "")
182
                .strip()
183
            )
184
            if len(msg) > 0:
185
                _err(f"< Command failed: {msg} >")
186
            logger.opt(exception=True).critical(f"Command failed: {msg}")
187
            self._dump_error(e)
188
        if self._mandos and self._mandos.log_setup and self._mandos.log_setup.only_path:
189
            log_path = self._mandos.log_setup.only_path
190
            _err(f"See the log file: {log_path.resolve()}")
191
        time.sleep(0.5)
192
193
    def _dump_error(self, e: BaseException) -> None:
0 ignored issues
show
Coding Style Naming introduced by
Argument 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...
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...
194
        try:
195
            dmp_path = FilesysTools.dump_error(e)
196
            _err(f"Wrote error and system info to: {dmp_path.resolve()}")
197
        except BaseException:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as BaseException is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
198
            _err("Note: Failed to write an error dump")
199
200
201
if __name__ == "__main__":
202
    MandosTyperCli().main()
203
204
205
__all__ = ["CmdNamespace", "MandosCli"]
206