replbase 0.0.32__tar.gz → 0.0.34__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {replbase-0.0.32 → replbase-0.0.34}/PKG-INFO +1 -1
- {replbase-0.0.32 → replbase-0.0.34}/pyproject.toml +1 -1
- replbase-0.0.34/replbase/cmd_meta.py +265 -0
- {replbase-0.0.32 → replbase-0.0.34}/replbase/repl_base.py +11 -57
- {replbase-0.0.32 → replbase-0.0.34}/LICENSE +0 -0
- {replbase-0.0.32 → replbase-0.0.34}/README.md +0 -0
- {replbase-0.0.32 → replbase-0.0.34}/replbase/__init__.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "replbase"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.34"
|
|
8
8
|
description = "\"Combination of other REPL tools into a reusable class that generates a REPL\""
|
|
9
9
|
authors = [ "Joseph Bochinski <stirgejr@gmail.com>",]
|
|
10
10
|
license = "MIT"
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
""" Code for auto-generating ArgumentParsers from functions """
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import inspect
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import get_type_hints
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ReplCommand:
|
|
15
|
+
"""Class definition for a provided CLI REPL command"""
|
|
16
|
+
|
|
17
|
+
command: Callable = None
|
|
18
|
+
help_txt: str = ""
|
|
19
|
+
parser: argparse.ArgumentParser = None
|
|
20
|
+
def_kwargs: dict = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CommandMeta:
|
|
25
|
+
"""Class representing the content of a docstring"""
|
|
26
|
+
|
|
27
|
+
func: Callable = None
|
|
28
|
+
""" Function to assemble the docstring meta from """
|
|
29
|
+
|
|
30
|
+
register_cmd: Callable[..., ReplCommand] = None
|
|
31
|
+
""" Function reference to registry command """
|
|
32
|
+
|
|
33
|
+
summary: str = ""
|
|
34
|
+
""" First line of the docstring """
|
|
35
|
+
|
|
36
|
+
additional: str = ""
|
|
37
|
+
""" Additional lines after the summary and before other notations """
|
|
38
|
+
|
|
39
|
+
additional_lines: list[str] = field(default_factory=list)
|
|
40
|
+
""" Raw output of docstring lines for additional """
|
|
41
|
+
|
|
42
|
+
args: dict[str, str] = field(default_factory=dict)
|
|
43
|
+
""" Arg comments """
|
|
44
|
+
|
|
45
|
+
args_lines: list[str] = field(default_factory=list)
|
|
46
|
+
""" Raw output of docstring lines for args """
|
|
47
|
+
|
|
48
|
+
returns: dict[str, str] = field(default_factory=dict)
|
|
49
|
+
""" Return comments """
|
|
50
|
+
|
|
51
|
+
returns_lines: list[str] = field(default_factory=list)
|
|
52
|
+
""" Raw output of docstring lines for returns """
|
|
53
|
+
|
|
54
|
+
yields: dict[str, str] = field(default_factory=dict)
|
|
55
|
+
""" Yields comments """
|
|
56
|
+
|
|
57
|
+
yields_lines: list[str] = field(default_factory=list)
|
|
58
|
+
""" Raw output of docstring lines for yields """
|
|
59
|
+
|
|
60
|
+
examples: dict[str, str] = field(default_factory=dict)
|
|
61
|
+
""" Examples comments """
|
|
62
|
+
|
|
63
|
+
examples_lines: list[str] = field(default_factory=list)
|
|
64
|
+
""" Raw output of docstring lines for examples """
|
|
65
|
+
|
|
66
|
+
raises: dict[str, str] = field(default_factory=dict)
|
|
67
|
+
""" Raises comments """
|
|
68
|
+
|
|
69
|
+
raises_lines: list[str] = field(default_factory=list)
|
|
70
|
+
""" Raw output of docstring lines for raises """
|
|
71
|
+
|
|
72
|
+
type_hints: dict[str, type] = field(default_factory=dict)
|
|
73
|
+
""" Type hints retrieved from inspect """
|
|
74
|
+
|
|
75
|
+
sig_parms: dict[str, inspect.Parameter] = field(default_factory=dict)
|
|
76
|
+
""" Signature of function """
|
|
77
|
+
|
|
78
|
+
flag_names: dict[str, list[str]] = field(default_factory=dict)
|
|
79
|
+
""" Flag names to use for the ReplCommand.parser arguments embedded in the docstring """
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
|
|
83
|
+
if not self.func:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
self.type_hints = get_type_hints(self.func)
|
|
87
|
+
self.sig_parms = dict(inspect.signature(self.func).parameters)
|
|
88
|
+
|
|
89
|
+
docstring = self.func.__doc__
|
|
90
|
+
|
|
91
|
+
header_re = re.compile(r"[A-Z].*?:$")
|
|
92
|
+
|
|
93
|
+
if not docstring:
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
doc_parts = docstring.split("\n")
|
|
97
|
+
|
|
98
|
+
cur_header = ""
|
|
99
|
+
|
|
100
|
+
def get_header(header: str) -> str:
|
|
101
|
+
nonlocal header_re
|
|
102
|
+
header_name = header_re.search(header).groups()[0]
|
|
103
|
+
|
|
104
|
+
return header_name.lower().replace(":", "")
|
|
105
|
+
|
|
106
|
+
for part in doc_parts:
|
|
107
|
+
if header_re.search(part):
|
|
108
|
+
cur_header = get_header(part)
|
|
109
|
+
cur_header += "_lines"
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if cur_header and hasattr(self, cur_header):
|
|
113
|
+
prop = getattr(self, cur_header)
|
|
114
|
+
if isinstance(prop, list):
|
|
115
|
+
prop.append(part)
|
|
116
|
+
|
|
117
|
+
elif not self.summary:
|
|
118
|
+
self.summary = part
|
|
119
|
+
else:
|
|
120
|
+
self.additional_lines.append(part)
|
|
121
|
+
|
|
122
|
+
self.parse_lines()
|
|
123
|
+
self.parse_flags()
|
|
124
|
+
|
|
125
|
+
def gen_command(self) -> ReplCommand:
|
|
126
|
+
"""Generate a ReplCommand object from the provided function
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If no registry command is provided
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
ReplCommand: A ReplCommand object with the function and parser
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if not self.register_cmd:
|
|
136
|
+
raise ValueError("No registry command provided, cannot proceed")
|
|
137
|
+
|
|
138
|
+
help_text = self.summary
|
|
139
|
+
full_help = self.summary + self.additional
|
|
140
|
+
|
|
141
|
+
repl_cmd = self.register_cmd(
|
|
142
|
+
cmd_name=self.func.__name__,
|
|
143
|
+
cmd_func=self.func,
|
|
144
|
+
help_txt=help_text,
|
|
145
|
+
use_parser=True,
|
|
146
|
+
description=full_help,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not self.sig_parms:
|
|
150
|
+
return repl_cmd
|
|
151
|
+
|
|
152
|
+
for arg, parm in self.sig_parms.items():
|
|
153
|
+
arg_type = self.type_hints.get(arg, str)
|
|
154
|
+
|
|
155
|
+
flag_name = self.flag_names.get(arg)
|
|
156
|
+
|
|
157
|
+
cmd_init = {"help": self.args.get(arg)}
|
|
158
|
+
|
|
159
|
+
if arg_type == bool:
|
|
160
|
+
cmd_init["action"] = (
|
|
161
|
+
"store_false" if parm.default is True else "store_true"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if arg_type != str:
|
|
165
|
+
cmd_init["type"] = arg_type
|
|
166
|
+
|
|
167
|
+
if isinstance(arg_type, type) and isinstance(parm.default, arg_type):
|
|
168
|
+
cmd_init["default"] = parm.default
|
|
169
|
+
|
|
170
|
+
if isinstance(arg_type, list):
|
|
171
|
+
cmd_init["nargs"] = "+"
|
|
172
|
+
|
|
173
|
+
if cmd_init:
|
|
174
|
+
repl_cmd.parser.add_argument(flag_name, **cmd_init)
|
|
175
|
+
|
|
176
|
+
else:
|
|
177
|
+
repl_cmd.parser.add_argument(flag_name)
|
|
178
|
+
|
|
179
|
+
return repl_cmd
|
|
180
|
+
|
|
181
|
+
def is_arg_optional(self, arg: str) -> bool:
|
|
182
|
+
"""Check if an argument is optional
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
arg (str): Argument name
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
bool: True if the argument is optional
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
param = self.sig_parms.get(arg)
|
|
192
|
+
if param is None:
|
|
193
|
+
print(
|
|
194
|
+
f"[WARNING]: function signature not assigned for {self.func.__name__}.{arg}"
|
|
195
|
+
)
|
|
196
|
+
return False
|
|
197
|
+
return param.default != param.empty
|
|
198
|
+
|
|
199
|
+
def parse_flags(self) -> None:
|
|
200
|
+
"""Parse the flag names from the arguments of the function signature"""
|
|
201
|
+
|
|
202
|
+
if not self.args:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
for arg in self.args:
|
|
206
|
+
flag = arg.replace("_", "-")
|
|
207
|
+
if self.is_arg_optional(arg):
|
|
208
|
+
flag = "--" + flag
|
|
209
|
+
|
|
210
|
+
self.flag_names[arg] = flag
|
|
211
|
+
|
|
212
|
+
def parse_lines(self) -> None:
|
|
213
|
+
"""Parse the lines of the docstring into the appropriate sections"""
|
|
214
|
+
|
|
215
|
+
if self.args_lines:
|
|
216
|
+
self.parse_arg_strs()
|
|
217
|
+
|
|
218
|
+
for header in ["additional", "returns", "yields", "examples", "raises"]:
|
|
219
|
+
lines = getattr(self, f"{header}_lines")
|
|
220
|
+
if lines:
|
|
221
|
+
header_str = "\n".join(lines)
|
|
222
|
+
setattr(self, header, header_str)
|
|
223
|
+
|
|
224
|
+
def get_param_str(self, param: inspect.Parameter) -> str:
|
|
225
|
+
"""Get a string representation of the parameter
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
param (inspect.Parameter): Parameter to get the string representation of
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
str: String representation of the parameter
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
optional = param.default != param.empty
|
|
235
|
+
opt_str = ""
|
|
236
|
+
if optional:
|
|
237
|
+
opt_str = (", " if param.annotation else "") + "optional"
|
|
238
|
+
|
|
239
|
+
return f"({param.annotation}{opt_str})"
|
|
240
|
+
|
|
241
|
+
def parse_arg_strs(self) -> None:
|
|
242
|
+
"""Parse the argument strings from the docstring"""
|
|
243
|
+
# pylint: disable=C0301
|
|
244
|
+
arg_re = re.compile(
|
|
245
|
+
r"(?P<arg_name>[a-zA-Z_]\w*)\s*(?P<type_hint>\(\w+(?:, optional)*\)):(?P<arg_text>\s*.+?$)"
|
|
246
|
+
)
|
|
247
|
+
# pylint: enable=C0301
|
|
248
|
+
|
|
249
|
+
args = defaultdict(list)
|
|
250
|
+
cur_arg = ""
|
|
251
|
+
|
|
252
|
+
for line in self.args_lines:
|
|
253
|
+
arg_match = arg_re.search(line)
|
|
254
|
+
if arg_match:
|
|
255
|
+
cur_arg, arg_type, arg_text = arg_match.groupdict().values()
|
|
256
|
+
param = self.sig_parms.get(cur_arg)
|
|
257
|
+
|
|
258
|
+
if param:
|
|
259
|
+
arg_type = self.get_param_str(param)
|
|
260
|
+
args[cur_arg].append(f"{arg_type}{arg_text}")
|
|
261
|
+
elif cur_arg:
|
|
262
|
+
args[cur_arg].append(line)
|
|
263
|
+
|
|
264
|
+
arg_strs = {arg: "\n".join(lines) for arg, lines in args.items()}
|
|
265
|
+
self.args.update(arg_strs)
|
|
@@ -26,7 +26,7 @@ import stat
|
|
|
26
26
|
import time
|
|
27
27
|
|
|
28
28
|
from dataclasses import dataclass, field
|
|
29
|
-
from typing import Any, Callable, Literal
|
|
29
|
+
from typing import Any, Callable, Literal, get_type_hints
|
|
30
30
|
|
|
31
31
|
from prompt_toolkit import PromptSession
|
|
32
32
|
from prompt_toolkit.buffer import Buffer
|
|
@@ -41,12 +41,16 @@ from rich.console import Console
|
|
|
41
41
|
from rich.theme import Theme
|
|
42
42
|
from tabulate import tabulate
|
|
43
43
|
|
|
44
|
+
from replbase.cmd_meta import ReplCommand, CommandMeta
|
|
45
|
+
|
|
44
46
|
# endregion Imports
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
# region Constants
|
|
48
50
|
|
|
49
51
|
ColorSystem = Literal["auto", "standard", "256", "truecolor", "windows"]
|
|
52
|
+
|
|
53
|
+
|
|
50
54
|
# endregion Constants
|
|
51
55
|
|
|
52
56
|
|
|
@@ -157,16 +161,6 @@ class ReplTheme(Theme):
|
|
|
157
161
|
super().__init__(styles)
|
|
158
162
|
|
|
159
163
|
|
|
160
|
-
@dataclass
|
|
161
|
-
class ReplCommand:
|
|
162
|
-
"""Class definition for a provided CLI REPL command"""
|
|
163
|
-
|
|
164
|
-
command: Callable = None
|
|
165
|
-
help_txt: str = ""
|
|
166
|
-
parser: argparse.ArgumentParser = None
|
|
167
|
-
def_kwargs: dict = field(default_factory=dict)
|
|
168
|
-
|
|
169
|
-
|
|
170
164
|
@dataclass
|
|
171
165
|
class ReplBase:
|
|
172
166
|
"""Dataclass for CLI options"""
|
|
@@ -208,6 +202,8 @@ class ReplBase:
|
|
|
208
202
|
"""Command dictionary for prompt_toolkit. Keys are command names,
|
|
209
203
|
values are the corresponding description/help text"""
|
|
210
204
|
|
|
205
|
+
docstring_format: str = "google"
|
|
206
|
+
|
|
211
207
|
parent: ReplBase | dict = None
|
|
212
208
|
|
|
213
209
|
session: PromptSession = None
|
|
@@ -411,7 +407,7 @@ class ReplBase:
|
|
|
411
407
|
new_cmd = ReplCommand(command=cmd_func, help_txt=help_txt)
|
|
412
408
|
if use_parser:
|
|
413
409
|
new_cmd.parser = argparse.ArgumentParser(
|
|
414
|
-
description=description or help_txt
|
|
410
|
+
description=description or help_txt,
|
|
415
411
|
)
|
|
416
412
|
if def_kwargs:
|
|
417
413
|
new_cmd.def_kwargs = def_kwargs
|
|
@@ -424,53 +420,11 @@ class ReplBase:
|
|
|
424
420
|
return
|
|
425
421
|
|
|
426
422
|
cmd: Callable = getattr(self, cmd_name)
|
|
427
|
-
anno = cmd.__annotations__
|
|
428
|
-
sig = inspect.signature(cmd)
|
|
429
|
-
parms = sig.parameters
|
|
430
|
-
help_text = cmd.__doc__
|
|
431
|
-
repl_cmd = self.add_command(
|
|
432
|
-
cmd.__name__,
|
|
433
|
-
cmd,
|
|
434
|
-
help_txt=help_text,
|
|
435
|
-
use_parser=True,
|
|
436
|
-
description=help_text,
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
if not parms:
|
|
440
|
-
return repl_cmd
|
|
441
423
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def_val = parm.default
|
|
445
|
-
|
|
446
|
-
optional = def_val != parm.empty
|
|
447
|
-
flag_name = arg.replace("_", "-")
|
|
448
|
-
|
|
449
|
-
if optional:
|
|
450
|
-
flag_name = "--" + flag_name
|
|
451
|
-
|
|
452
|
-
cmd_init = {}
|
|
453
|
-
|
|
454
|
-
if arg_type == bool:
|
|
455
|
-
cmd_init["action"] = (
|
|
456
|
-
"store_false" if def_val is True else "store_true"
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
if arg_type != str:
|
|
460
|
-
cmd_init["type"] = arg_type
|
|
461
|
-
|
|
462
|
-
if isinstance(def_val, arg_type):
|
|
463
|
-
cmd_init["default"] = def_val
|
|
464
|
-
|
|
465
|
-
if isinstance(arg_type, list):
|
|
466
|
-
cmd_init["nargs"] = "+"
|
|
467
|
-
|
|
468
|
-
if cmd_init:
|
|
469
|
-
repl_cmd.parser.add_argument(flag_name, **cmd_init)
|
|
470
|
-
else:
|
|
471
|
-
repl_cmd.parser.add_argument(flag_name)
|
|
424
|
+
if not callable(cmd):
|
|
425
|
+
return
|
|
472
426
|
|
|
473
|
-
return
|
|
427
|
+
return CommandMeta(cmd).gen_command()
|
|
474
428
|
|
|
475
429
|
def setup_cmds(self, *cmd_names: list[str]) -> None:
|
|
476
430
|
"""Automatically configure commands based on the provided names
|
|
File without changes
|
|
File without changes
|
|
File without changes
|