replbase 0.0.33__tar.gz → 0.0.35__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: replbase
3
- Version: 0.0.33
3
+ Version: 0.0.35
4
4
  Summary: "Combination of other REPL tools into a reusable class that generates a REPL"
5
5
  License: MIT
6
6
  Author: Joseph Bochinski
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "replbase"
7
- version = "0.0.33"
7
+ version = "0.0.35"
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
- for arg, parm in parms.items():
443
- arg_type = anno.get(arg, str)
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(arg_type, type) and 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 repl_cmd
427
+ return CommandMeta(func=cmd, register_cmd=self.add_command).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