Passed
Push — main ( 65730f...fad324 )
by Douglas
06:54 queued 02:27
created

mandos.entry.tools.docs.Doc.as_dict()   A

Complexity

Conditions 3

Size

Total Lines 7
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
"""
2
Documents Mandos commands.
3
"""
4
5
from __future__ import annotations
6
7
from dataclasses import dataclass
8
from pathlib import Path
9
from textwrap import wrap
10
from typing import Mapping, Optional, Sequence
11
12
import pandas as pd
0 ignored issues
show
introduced by
Unable to import 'pandas'
Loading history...
13
from pocketutils.core.exceptions import ContradictoryRequestError
0 ignored issues
show
introduced by
Unable to import 'pocketutils.core.exceptions'
Loading history...
14
from pocketutils.misc.typer_utils import TyperUtils
0 ignored issues
show
introduced by
Unable to import 'pocketutils.misc.typer_utils'
Loading history...
15
from typeddfs import FileFormat, TypedDfs
0 ignored issues
show
introduced by
Unable to import 'typeddfs'
Loading history...
16
from typeddfs.utils import Utils as TdfUtils
0 ignored issues
show
introduced by
Unable to import 'typeddfs.utils'
Loading history...
17
from typer.models import CommandInfo
0 ignored issues
show
introduced by
Unable to import 'typer.models'
Loading history...
18
19
CommandDocDf = (
20
    TypedDfs.typed("CommandDocDf")
21
    .require("command", dtype=str)
22
    .reserve("description", "parameters", dtype=str)
23
    .strict()
24
    .secure()
25
).build()
26
27
28
@dataclass(frozen=True, repr=True)
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
29
class Doc:
30
    command: str
31
    description: Optional[str]
32
    params: Optional[Mapping[str, str]]
33
34
    def as_lines(self) -> Sequence[str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
35
        lines = [self.command]
36
        if self.description is not None:
37
            lines.append(self.description)
38
        if self.params is not None:
39
            lines.extend(self.description)
40
        return lines
41
42
    def as_dict(self) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
43
        dct = dict(command=self.command)
44
        if self.description is not None:
45
            dct["description"] = self.description
46
        if self.params is not None:
47
            dct["params"] = "\n".join(self.params.values())
48
        return dct
49
50
51
@dataclass(frozen=True, repr=True)
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
52
class Documenter:
53
    level: int
54
    main: bool
55
    search: bool
56
    hidden: bool
57
    common: bool
58
    width: Optional[int]
59
60
    def document(self, commands: Sequence[CommandInfo], to: Path, style: str) -> None:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "to" 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...
introduced by
Missing function or method docstring
Loading history...
61
        fmt = FileFormat.from_path_or_none(to)
62
        if fmt is not None and not fmt.is_text and style != "table":
63
            raise ContradictoryRequestError(f"Cannot write binary {fmt} with style {style}")
64
        if style == "docs":
65
            content = self.get_long_text(commands)
66
            TdfUtils.write(to, content, encoding="utf8")
67
            return
68
        table = self.get_table(commands)
69
        if style == "table":
70
            table.write_file(to)
71
        else:
72
            content = table.pretty_print(style)
73
            TdfUtils.write(to, content, encoding="utf8")
74
75
    def get_table(self, commands: Sequence[CommandInfo]) -> CommandDocDf:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
76
        docs = self.get_docs(commands)
77
        return CommandDocDf.of([pd.Series(d.as_dict()) for d in docs])
78
79
    def get_long_text(self, commands: Sequence[CommandInfo]) -> str:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
80
        if self.search and self.main:
81
            title = "Main and search commands"
82
        elif self.search:
83
            title = "Search commands"
84
        else:
85
            title = "Main commands"
86
        docs = self.get_long(commands)
87
        width = max([len(title), *[len(s) for s in docs.keys()]])
0 ignored issues
show
unused-code introduced by
Consider iterating the dictionary directly instead of calling .keys()
Loading history...
88
        txt = title + "\n" + "=" * width + "\n\n"
89
        for k, v in docs.items():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" 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...
90
            txt += "\n\n" + k + "\n" + "#" * width + "\n" + v + "\n"
91
        return txt
92
93
    def get_long(self, commands: Sequence[CommandInfo]) -> Mapping[str, str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
94
        docs = self.get_docs(commands)
95
        results = {}
96
        for doc in docs:
97
            zz = []
0 ignored issues
show
Coding Style Naming introduced by
Variable name "zz" 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...
98
            for line in doc.as_lines():
99
                zz += line
0 ignored issues
show
Coding Style Naming introduced by
Variable name "zz" 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...
100
            results[doc.command] = "\n".join(zz)
101
        return results
102
103
    def get_docs(self, commands: Sequence[CommandInfo]) -> Sequence[Doc]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
104
        commands = self._commands(commands)
105
        return [self._doc(cmd) for cmd in commands]
106
107
    def _commands(self, commands: Sequence[CommandInfo]):
108
        cmds = [
109
            c
110
            for c in commands
111
            if (self.hidden or not c.hidden)
112
            and (c.name.startswith(":") and self.main or not c.name.startswith(":") and self.search)
113
        ]
114
        return sorted(cmds, key=lambda c: c.name)
115
116
    def _doc(self, cmd: CommandInfo) -> Doc:
117
        desc = self._wrap(self._desc(cmd))
118
        params = TyperUtils.get_help(cmd, hidden=self.hidden)
119
        params = {
120
            k: self._wrap(self._param(a))
121
            for k, a in params.items()
122
            if self._include_arg(cmd.name, k)
123
        }
124
        return Doc(command=self._wrap(cmd.name), description=desc, params=params)
125
126
    def _desc(self, cmd: CommandInfo) -> Optional[str]:
127
        cb = cmd.callback.__doc__
0 ignored issues
show
Coding Style Naming introduced by
Variable name "cb" 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...
128
        desc_lines = self._split(cb)
129
        if self.level >= 3:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
130
            return cb
131
        elif self.level >= 2:
132
            return "\n".join(desc_lines[:2])
133
        elif self.level >= 1:
134
            return desc_lines[0]
135
        return None
136
137
    def _param(self, param: str) -> Optional[str]:
138
        lines = self._split(param)
139
        if self.level >= 3:
140
            return param
141
        if self.level >= 2:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
142
            return lines[0] + "\n" + lines[1]
143
        elif self.level >= 1:
144
            return lines[0]
145
        return None
146
147
    def _include_arg(self, command: str, arg: str) -> bool:
148
        if self.common:
149
            return True
150
        _common = ["path", "--key", "--to", "--as-of", "--replace", "--proceed", "--check"]
151
        return arg not in ["--stderr", "--log"] and (command.startswith(":") or arg not in _common)
152
153
    def _split(self, txt: str) -> Sequence[str]:
154
        zs = []
0 ignored issues
show
Coding Style Naming introduced by
Variable name "zs" 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...
155
        for s in txt.splitlines():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "s" 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...
156
            s = self._clean(s)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "s" 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...
157
            if len(s) > 0:
158
                zs.append(s)
159
        return zs
160
161
    def _clean(self, txt: str) -> 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...
162
        return TdfUtils.strip_control_chars(txt.replace("\n", " ").replace("\t", " ")).strip()
163
164
    def _wrap(self, txt: Optional[str]) -> Optional[str]:
165
        if txt is None:
166
            return None
167
        if self.width is None:
168
            return txt
169
        return "\n".join(wrap(txt, width=self.width))
170
171
172
__all__ = ["Documenter", "Doc", "CommandDocDf"]
173