Passed
Push — main ( 2e1b6b...3a0c28 )
by Douglas
02:06
created

mandos.cli.MandosTyperCli.main()   B

Complexity

Conditions 8

Size

Total Lines 25
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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

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...
151
152
        if MandosMetadata.version is None:
153
            logger.error("Could not load package metadata for mandos. Is it installed?")
154
        else:
155
            logger.info(f"Mandos v{MandosMetadata.version}")
156
157
158
class MandosTyperCli:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
159
    def __init__(self):
160
        self._mandos = None
161
162
    def main(self) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
163
        try:
164
            self._mandos = MandosCli.as_cli()
165
            self._mandos.cli()
166
            _err("Done! Exited successfully.")
167
            if self.log_path:
168
                _err(f"See the log file: {self.log_path.resolve()}")
169
            quit(0)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
170
        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...
171
            self._fail(e, "aborted", abort=True)
172
            quit(130)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
173
        except SystemExit 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...
174
            if str(e) == "0":
175
                _err("Done! Exited successfully.")
176
                if self.log_path:
177
                    _err(f"See the log file: {self.log_path.resolve()}")
178
                quit(0)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
179
            self._fail(e, "unexpected exit", abort=False)
180
            quit(64)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
181
        except Exception 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...
Best Practice introduced by
Catching very general exceptions such as Exception 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...
182
            self._fail(e, "error", abort=False)
183
            quit(70)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
184
        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...
185
            self._fail(e, "system error", abort=False)
186
            quit(71)
0 ignored issues
show
Unused Code introduced by
Consider using sys.exit()
Loading history...
187
188
    @property
189
    def log_setup(self) -> Optional[FancyLoguru]:
0 ignored issues
show
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
introduced by
Missing function or method docstring
Loading history...
190
        if self._mandos and self._mandos.log_setup:
191
            return self._mandos.log_setup
192
193
    @property
194
    def log_path(self) -> Optional[Path]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
195
        if self._mandos and self._mandos.log_setup:
196
            return self._mandos.log_setup.only_path
197
198
    def _fail(self, e: BaseException, what: str, *, 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...
199
        _err("")
200
        _err(f"--- {what} ---".upper())
201
        dump_path = None
202
        if not abort:
203
            msg = "\n".join(SystemTools.serialize_exception_msg(e)).strip()
204
            if len(msg) > 0:
205
                _err("")
206
                _err(f"-- Command failed: {msg} --")
207
            logger.opt(exception=True).critical(f"Command failed: {msg}")
208
            dump_path = self._dump_error(e)
209
        if self.log_path:
210
            _err(f"See the log file: {self.log_path.resolve()}")
211
        if not abort:
212
            if dump_path:
213
                _err(f"Wrote error and system info to: {dump_path}")
214
            else:
215
                _err("Note: Failed to write an error dump")
216
        time.sleep(0.05)  # flush, etc.
217
        _err("")
218
219
    def _dump_error(self, e: BaseException) -> Optional[Path]:
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...
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...
220
        try:
221
            return FilesysTools.dump_error(e)
222
        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...
223
            return None
224
225
226
if __name__ == "__main__":
227
    MandosTyperCli().main()
228
229
230
__all__ = ["CmdNamespace", "MandosCli"]
231