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 |
|
|
|
|
13
|
|
|
from pocketutils.core.exceptions import ContradictoryRequestError |
|
|
|
|
14
|
|
|
from pocketutils.misc.typer_utils import TyperUtils |
|
|
|
|
15
|
|
|
from typeddfs import FileFormat, TypedDfs |
|
|
|
|
16
|
|
|
from typeddfs.utils import Utils as TdfUtils |
|
|
|
|
17
|
|
|
from typer.models import CommandInfo |
|
|
|
|
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) |
|
|
|
|
29
|
|
|
class Doc: |
30
|
|
|
command: str |
31
|
|
|
description: Optional[str] |
32
|
|
|
params: Optional[Mapping[str, str]] |
33
|
|
|
|
34
|
|
|
def as_lines(self) -> Sequence[str]: |
|
|
|
|
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]: |
|
|
|
|
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) |
|
|
|
|
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: |
|
|
|
|
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: |
|
|
|
|
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: |
|
|
|
|
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()]]) |
|
|
|
|
88
|
|
|
txt = title + "\n" + "=" * width + "\n\n" |
89
|
|
|
for k, v in docs.items(): |
|
|
|
|
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]: |
|
|
|
|
94
|
|
|
docs = self.get_docs(commands) |
95
|
|
|
results = {} |
96
|
|
|
for doc in docs: |
97
|
|
|
zz = [] |
|
|
|
|
98
|
|
|
for line in doc.as_lines(): |
99
|
|
|
zz += line |
|
|
|
|
100
|
|
|
results[doc.command] = "\n".join(zz) |
101
|
|
|
return results |
102
|
|
|
|
103
|
|
|
def get_docs(self, commands: Sequence[CommandInfo]) -> Sequence[Doc]: |
|
|
|
|
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__ |
|
|
|
|
128
|
|
|
desc_lines = self._split(cb) |
129
|
|
|
if self.level >= 3: |
|
|
|
|
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: |
|
|
|
|
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 = [] |
|
|
|
|
155
|
|
|
for s in txt.splitlines(): |
|
|
|
|
156
|
|
|
s = self._clean(s) |
|
|
|
|
157
|
|
|
if len(s) > 0: |
158
|
|
|
zs.append(s) |
159
|
|
|
return zs |
160
|
|
|
|
161
|
|
|
def _clean(self, txt: str) -> str: |
|
|
|
|
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
|
|
|
|