endpoint.cli 2.0.0.6__py3-none-any.whl
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.
- endpoint/__init__.py +699 -0
- endpoint/py.typed +0 -0
- endpoint/tests/__init__.py +0 -0
- endpoint/tests/test_endpoint.py +22 -0
- endpoint_cli-2.0.0.6.dist-info/METADATA +115 -0
- endpoint_cli-2.0.0.6.dist-info/RECORD +9 -0
- endpoint_cli-2.0.0.6.dist-info/WHEEL +5 -0
- endpoint_cli-2.0.0.6.dist-info/licenses/LICENSE +674 -0
- endpoint_cli-2.0.0.6.dist-info/top_level.txt +1 -0
endpoint/__init__.py
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""TBA"""
|
|
2
|
+
from argparse import ArgumentParser as _ArgumentParser
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Standard typing imports for aps
|
|
6
|
+
import typing_extensions as _te
|
|
7
|
+
import collections.abc as _a
|
|
8
|
+
import typing as _ty
|
|
9
|
+
|
|
10
|
+
if _ty.TYPE_CHECKING:
|
|
11
|
+
import _typeshed as _tsh
|
|
12
|
+
import types as _ts
|
|
13
|
+
|
|
14
|
+
__all__ = ["NoDefault", "analyze_function", "Endpoint", "AutoCLI"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NoDefault:
|
|
18
|
+
def __repr__(self) -> str:
|
|
19
|
+
return "<NoDefault Object>"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def analyze_function(function: _a.Callable) -> dict[str, list[_ty.Any] | str | None]:
|
|
23
|
+
"""
|
|
24
|
+
Analyzes a given function's signature and docstring, returning a structured summary of its
|
|
25
|
+
arguments, including default values, types, keyword-only flags, documentation hints, and
|
|
26
|
+
choices for `Literal`-type arguments. Also extracts information on `*args`, `**kwargs`,
|
|
27
|
+
and the return type.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
function (types.FunctionType): The function to analyze.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
dict: A dictionary containing the following keys:
|
|
34
|
+
- "name" (str): The name of the function.
|
|
35
|
+
- "doc" (str): The function's docstring.
|
|
36
|
+
- "arguments" (List[Dict[str, Union[str, None]]]): Details of each argument:
|
|
37
|
+
- "name" (str): The argument's name.
|
|
38
|
+
- "default" (Any or None): The default value, if provided.
|
|
39
|
+
- "choices" (List[Any] or []): Options for `Literal` type hints, if applicable.
|
|
40
|
+
- "type" (Any or None): The argument's type hint.
|
|
41
|
+
- "doc_help" (str): The extracted docstring help for the argument.
|
|
42
|
+
- "kwarg_only" (bool): True if the argument is keyword-only.
|
|
43
|
+
- "has_*args" (bool): True if the function accepts variable positional arguments.
|
|
44
|
+
- "has_**kwargs" (bool): True if the function accepts variable keyword arguments.
|
|
45
|
+
- "return_type" (Any or None): The function's return type hint.
|
|
46
|
+
- "return_choices" (List[Any] or []): Options for `Literal` type hints for the return type, if applicable.
|
|
47
|
+
- "return_doc_help" (str): The extracted docstring help for the return type.
|
|
48
|
+
"""
|
|
49
|
+
if hasattr(function, "__func__"):
|
|
50
|
+
function = function.__func__
|
|
51
|
+
elif not isinstance(function, _ts.FunctionType):
|
|
52
|
+
raise ValueError(f"Only a real function can be analyzed, not '{function}'")
|
|
53
|
+
|
|
54
|
+
name = function.__name__
|
|
55
|
+
arg_count = (
|
|
56
|
+
function.__code__.co_argcount
|
|
57
|
+
+ function.__code__.co_kwonlyargcount
|
|
58
|
+
+ function.__code__.co_posonlyargcount
|
|
59
|
+
)
|
|
60
|
+
argument_names = list(function.__code__.co_varnames[:arg_count] or ())
|
|
61
|
+
has_args = (function.__code__.co_flags & 0b0100) == 4
|
|
62
|
+
has_kwargs = (function.__code__.co_flags & 0b1000) == 8
|
|
63
|
+
defaults = [NoDefault() for _ in range(len(argument_names))]
|
|
64
|
+
defaults.extend(list(function.__defaults__ or ()))
|
|
65
|
+
if function.__kwdefaults__ is not None:
|
|
66
|
+
defaults.extend(list(function.__kwdefaults__.values()))
|
|
67
|
+
defaults = defaults[len(defaults) - len(argument_names) :]
|
|
68
|
+
types = function.__annotations__ or {}
|
|
69
|
+
docstring = function.__doc__ or ""
|
|
70
|
+
type_hints = _ty.get_type_hints(function)
|
|
71
|
+
|
|
72
|
+
pos_argcount = function.__code__.co_argcount # After which i we have kwarg only
|
|
73
|
+
if has_args:
|
|
74
|
+
argument_names.insert(pos_argcount, "args")
|
|
75
|
+
defaults.insert(pos_argcount, None)
|
|
76
|
+
pos_argcount += 1
|
|
77
|
+
if has_kwargs:
|
|
78
|
+
argument_names.append("kwargs")
|
|
79
|
+
defaults.append(None)
|
|
80
|
+
argument_names.append("return")
|
|
81
|
+
defaults.append(None)
|
|
82
|
+
|
|
83
|
+
result = {
|
|
84
|
+
"name": name,
|
|
85
|
+
"doc": docstring,
|
|
86
|
+
"arguments": [],
|
|
87
|
+
"has_*args": has_args,
|
|
88
|
+
"has_**kwargs": has_kwargs,
|
|
89
|
+
"return_type": function.__annotations__.get("return"),
|
|
90
|
+
"return_choices": [],
|
|
91
|
+
"return_doc_help": "",
|
|
92
|
+
}
|
|
93
|
+
for i, (argument_name, default) in enumerate(zip(argument_names, defaults)):
|
|
94
|
+
argument_start = docstring.find(argument_name)
|
|
95
|
+
help_str, choices, type_choices = "", tuple(), tuple()
|
|
96
|
+
if argument_start != -1:
|
|
97
|
+
help_start = argument_start + len(
|
|
98
|
+
argument_name
|
|
99
|
+
) # Where argument_name ends in docstring
|
|
100
|
+
newline_offset = docstring[argument_start:].find("\n")
|
|
101
|
+
if newline_offset == -1:
|
|
102
|
+
newline_offset = len(docstring) - argument_start
|
|
103
|
+
next_line = argument_start + newline_offset
|
|
104
|
+
help_str = docstring[help_start:next_line].strip(": \n\t")
|
|
105
|
+
if argument_name == "return":
|
|
106
|
+
type_hint = result["return_type"]
|
|
107
|
+
if getattr(type_hint, "__origin__", None) is _ty.Literal:
|
|
108
|
+
choices = type_hint.__args__
|
|
109
|
+
result["return_choices"] = choices
|
|
110
|
+
result["return_doc_help"] = help_str
|
|
111
|
+
continue
|
|
112
|
+
type_hint = type_hints.get(argument_name)
|
|
113
|
+
if getattr(type_hint, "__origin__", None) is _ty.Literal:
|
|
114
|
+
choices = type_hint.__args__
|
|
115
|
+
elif getattr(type_hint, "__origin__", None) is _ty.Union or type(type_hint) is _ts.UnionType:
|
|
116
|
+
type_choices = type_hint.__args__
|
|
117
|
+
result["arguments"].append(
|
|
118
|
+
{
|
|
119
|
+
"name": argument_name,
|
|
120
|
+
"default": default,
|
|
121
|
+
"choices": choices,
|
|
122
|
+
"type": types.get(argument_name),
|
|
123
|
+
"type_choices": type_choices,
|
|
124
|
+
"doc_help": help_str,
|
|
125
|
+
"kwarg_only": True if i >= pos_argcount else False,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ArgumentParsingError(Exception):
|
|
132
|
+
"""Exception raised when an error occurs during argument parsing.
|
|
133
|
+
|
|
134
|
+
This exception is used to indicate issues when parsing command-line arguments.
|
|
135
|
+
It includes a message and an index to indicate where the error occurred, helping
|
|
136
|
+
users or developers identify the issue in the input command.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
index (int): The position in the argument list where the error was detected.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, message: str, index: int) -> None:
|
|
143
|
+
super().__init__(message)
|
|
144
|
+
self.index: int = index
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Endpoint:
|
|
148
|
+
"""Represents the endpoint of a trace from an argument structure object.
|
|
149
|
+
|
|
150
|
+
The `EndPoint` class serves as a container for functions associated with
|
|
151
|
+
a particular argument path, providing a way to call the function with
|
|
152
|
+
predefined arguments and keyword arguments.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
analysis (dict): Dictionary containing analysis data of the function's
|
|
156
|
+
arguments, such as names and types, generated by `analyze_function`.
|
|
157
|
+
_arg_index (dict): A mapping of argument names to their indices, allowing
|
|
158
|
+
quick lookup of argument positions by name.
|
|
159
|
+
_function (_ts.FunctionType): The actual function associated with this endpoint,
|
|
160
|
+
which will be called when the endpoint is invoked.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, function: _ts.FunctionType) -> None:
|
|
164
|
+
self.analysis: dict[str, list[_ty.Any] | str | None] = analyze_function(
|
|
165
|
+
function
|
|
166
|
+
)
|
|
167
|
+
self._arg_index: dict[str, int] = {
|
|
168
|
+
arg["name"]: i for i, arg in enumerate(self.analysis["arguments"])
|
|
169
|
+
}
|
|
170
|
+
self._function: _ts.FunctionType = function
|
|
171
|
+
|
|
172
|
+
def call(self, *args, **kwargs) -> None:
|
|
173
|
+
"""Executes the internal function using the specified arguments.
|
|
174
|
+
|
|
175
|
+
This method forwards all positional and keyword arguments to the stored
|
|
176
|
+
function, allowing flexible invocation from various contexts.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
*args: Positional arguments to pass to the function.
|
|
180
|
+
**kwargs: Keyword arguments to pass to the function.
|
|
181
|
+
"""
|
|
182
|
+
self._function(*args, **kwargs)
|
|
183
|
+
|
|
184
|
+
def __repr__(self) -> str:
|
|
185
|
+
args = [
|
|
186
|
+
f"{key}: {self.analysis['arguments'][index]}"
|
|
187
|
+
for (key, index) in self._arg_index.items()
|
|
188
|
+
]
|
|
189
|
+
return f"Endpoint(arguments={args})"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ArgStructBuilder:
|
|
193
|
+
"""A utility class for constructing and managing a hierarchical argument structure.
|
|
194
|
+
|
|
195
|
+
`ArgStructBuilder` allows users to build a nested dictionary structure that
|
|
196
|
+
represents CLI commands and their subcommands. The resulting structure can be
|
|
197
|
+
used in CLI parsers or for other command-line interfaces.
|
|
198
|
+
|
|
199
|
+
Attributes:
|
|
200
|
+
_commands (dict): The command structure dictionary, storing commands and
|
|
201
|
+
subcommands with their respective mappings.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
self._commands: dict[str, dict | str] = {}
|
|
206
|
+
|
|
207
|
+
def add_command(self, command: str, subcommands: dict | None = None) -> None:
|
|
208
|
+
"""Adds a top-level command with optional subcommands.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
command (str): The main command to add to the structure.
|
|
212
|
+
subcommands (Optional[dict], optional): Dictionary of subcommands,
|
|
213
|
+
if applicable. If not provided, an empty dictionary is assigned.
|
|
214
|
+
"""
|
|
215
|
+
if subcommands is None:
|
|
216
|
+
subcommands = {}
|
|
217
|
+
self._commands[command] = subcommands
|
|
218
|
+
|
|
219
|
+
def add_subcommand(self, parent: str, subcommand: str) -> None:
|
|
220
|
+
"""Adds a subcommand under an existing top-level command.
|
|
221
|
+
|
|
222
|
+
If the parent command does not exist, raises a `ValueError`.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
parent (str): The parent command under which to add the subcommand.
|
|
226
|
+
subcommand (str): The subcommand to add within the parent command.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ValueError: If the parent command does not exist or is incompatible
|
|
230
|
+
with having subcommands.
|
|
231
|
+
"""
|
|
232
|
+
if parent not in self._commands:
|
|
233
|
+
raise ValueError(f"Command '{parent}' not found.")
|
|
234
|
+
if isinstance(self._commands[parent], dict):
|
|
235
|
+
self._commands[parent][subcommand] = {}
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError(f"Command '{parent}' cannot have subcommands.")
|
|
238
|
+
|
|
239
|
+
def add_nested_command(
|
|
240
|
+
self, parent: str, command: str, subcommand: str | dict | None
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Adds a nested command within a command structure hierarchy.
|
|
243
|
+
|
|
244
|
+
Navigates to the specified parent path in the command structure, allowing
|
|
245
|
+
creation of complex, multi-level command hierarchies.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
parent (str): Dot-separated string representing the parent command path.
|
|
249
|
+
command (str): The command to add within the specified parent path.
|
|
250
|
+
subcommand (Optional[str | dict]): Structure for the subcommand. If
|
|
251
|
+
`None`, an empty dictionary is used.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If the specified parent path is not valid.
|
|
255
|
+
"""
|
|
256
|
+
if subcommand is None:
|
|
257
|
+
subcommand = {}
|
|
258
|
+
|
|
259
|
+
# Navigate to the correct parent level
|
|
260
|
+
parts = parent.split(".")
|
|
261
|
+
current_level = self._commands
|
|
262
|
+
for part in parts:
|
|
263
|
+
if part not in current_level or not isinstance(current_level[part], dict):
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"Command '{parent}' not found or is not a valid parent."
|
|
266
|
+
)
|
|
267
|
+
current_level = current_level[part]
|
|
268
|
+
|
|
269
|
+
if isinstance(subcommand, str):
|
|
270
|
+
current_level[command] = {subcommand: {}}
|
|
271
|
+
else:
|
|
272
|
+
current_level[command] = subcommand
|
|
273
|
+
|
|
274
|
+
def get_structure(self) -> dict[str, dict | str]:
|
|
275
|
+
"""Retrieves the full command structure dictionary.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
dict: The dictionary representing all commands and subcommands,
|
|
279
|
+
which can be used directly by other classes or modules for parsing.
|
|
280
|
+
"""
|
|
281
|
+
return self._commands
|
|
282
|
+
|
|
283
|
+
_A = _ty.TypeVar("_A")
|
|
284
|
+
StructureType = dict[str, "StructureType", Endpoint | None]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class AutoCLI:
|
|
288
|
+
"""A command-line argument parser that uses structured arguments and endpoints.
|
|
289
|
+
|
|
290
|
+
Argumint is designed to parse CLI arguments using a predefined argument structure.
|
|
291
|
+
It allows users to define and manage argument paths, replace the argument structure,
|
|
292
|
+
and execute endpoints based on parsed arguments.
|
|
293
|
+
|
|
294
|
+
Attributes:
|
|
295
|
+
default_endpoint (Endpoint): The default endpoint to call if a path cannot be resolved.
|
|
296
|
+
_structure (dict): The dictionary representing the current argument structure.
|
|
297
|
+
_endpoints (dict): A mapping of argument paths to endpoint functions.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(
|
|
301
|
+
self, default_endpoint: Endpoint, structure: dict[str, dict | str | Endpoint] = {}, auto_add_structure: bool = True
|
|
302
|
+
) -> None:
|
|
303
|
+
if not self._verify_structure(structure):
|
|
304
|
+
raise ValueError("Structure must be valid.")
|
|
305
|
+
self.default_endpoint: Endpoint = default_endpoint
|
|
306
|
+
self.structure: dict[str, dict | str] = structure
|
|
307
|
+
# self._structure: dict[str, dict | str] = structure
|
|
308
|
+
# self._endpoints: dict[str, Endpoint] = {}
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def _verify_structure(cls, structure: dict[str, dict | str | Endpoint]) -> bool:
|
|
312
|
+
if any(type(x) not in (str,) for x in structure.keys()):
|
|
313
|
+
return False
|
|
314
|
+
for value in structure.values():
|
|
315
|
+
if isinstance(value, dict):
|
|
316
|
+
if not cls._verify_structure(value):
|
|
317
|
+
return False
|
|
318
|
+
elif not isinstance(value, (str, Endpoint)):
|
|
319
|
+
return False
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def _error(i: int, command_string: str) -> None:
|
|
324
|
+
"""Displays a caret (`^`) pointing to an error in the command string.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
i (int): Index in the command string where the error occurred.
|
|
328
|
+
command_string (str): The command string with the error.
|
|
329
|
+
"""
|
|
330
|
+
print(f"{command_string}\n{' ' * i + '^'}")
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def _lst_error(
|
|
334
|
+
i: int, arg_i: int, command_lst: list[str], do_exit: bool = False
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Displays an error caret in a list of command arguments.
|
|
337
|
+
|
|
338
|
+
This method calculates the error position in a CLI argument list, displaying
|
|
339
|
+
a caret to indicate where the error was found. Optionally, it can exit
|
|
340
|
+
the program.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
i (int): Index of the problematic argument in the list.
|
|
344
|
+
arg_i (int): Position within the argument string to place the caret.
|
|
345
|
+
command_lst (list[str]): List of command-line arguments.
|
|
346
|
+
do_exit (bool, optional): If True, exits the program. Defaults to False.
|
|
347
|
+
"""
|
|
348
|
+
length = sum(len(item) for item in command_lst[:i]) + i
|
|
349
|
+
print(" ".join(command_lst) + "\n" + " " * (length + arg_i) + "^")
|
|
350
|
+
if do_exit:
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
def _check_path(self, path: str, overwrite_pre_args: dict | None = None) -> bool:
|
|
354
|
+
"""Verifies if a specified path exists within the argument structure.
|
|
355
|
+
|
|
356
|
+
This method traverses the structure to confirm whether each segment of the
|
|
357
|
+
path is valid and points to an existing command or subcommand.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
path (str): The dot-separated path to check within the argument structure.
|
|
361
|
+
overwrite_pre_args (Optional[dict], optional): An optional argument structure
|
|
362
|
+
to check against instead of the default `_arg_struct`.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
bool: True if the path exists, False otherwise.
|
|
366
|
+
"""
|
|
367
|
+
overwrite_pre_args = overwrite_pre_args or self._structure
|
|
368
|
+
current_level: str | dict[str, str | dict] = overwrite_pre_args
|
|
369
|
+
for point in path.split("."):
|
|
370
|
+
if point not in current_level or not isinstance(current_level[point], dict):
|
|
371
|
+
return False
|
|
372
|
+
current_level = current_level[point]
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
def replace_arg_struct(self, new_arg_struct: dict) -> None:
|
|
376
|
+
"""Replaces the current argument structure with a new one.
|
|
377
|
+
|
|
378
|
+
Updates the structure and removes any existing endpoints that do not match
|
|
379
|
+
the new structure.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
new_arg_struct (dict): The new argument structure to replace the current one.
|
|
383
|
+
"""
|
|
384
|
+
to_del = []
|
|
385
|
+
for path, endpoint in self._endpoints.items():
|
|
386
|
+
if self._check_path(path, new_arg_struct):
|
|
387
|
+
continue
|
|
388
|
+
else:
|
|
389
|
+
to_del.append(path)
|
|
390
|
+
self._structure = new_arg_struct
|
|
391
|
+
print(
|
|
392
|
+
f"Removed {len([self._endpoints.pop(epPath) for epPath in to_del])} endpoints."
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def add_endpoint(self, path: str, endpoint: Endpoint) -> None:
|
|
396
|
+
"""Adds an endpoint at a specified path within the structure.
|
|
397
|
+
|
|
398
|
+
The endpoint will be callable from the CLI if the provided path matches.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
path (str): Dot-separated path where the endpoint will be added.
|
|
402
|
+
endpoint (Endpoint): The endpoint instance to associate with the path.
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If the path does not exist within the argument structure
|
|
406
|
+
or if it already has an endpoint assigned.
|
|
407
|
+
"""
|
|
408
|
+
if self._check_path(path):
|
|
409
|
+
if not self._endpoints.get(path):
|
|
410
|
+
self._endpoints[path] = endpoint
|
|
411
|
+
else:
|
|
412
|
+
raise ValueError(f"The path {path} already has an endpoint.")
|
|
413
|
+
else:
|
|
414
|
+
raise ValueError(f"The path '{path}' doesn't exist.")
|
|
415
|
+
|
|
416
|
+
def replace_endpoint(self, path: str, endpoint: Endpoint) -> None:
|
|
417
|
+
"""Replaces an existing endpoint at a given path.
|
|
418
|
+
|
|
419
|
+
This method checks if the specified path exists in the argument structure
|
|
420
|
+
before replacing the existing endpoint with the new one. If the path does
|
|
421
|
+
not exist, an error is raised.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
path (str): The path where the endpoint will be replaced.
|
|
425
|
+
endpoint (Endpoint): The new endpoint to assign to the specified path.
|
|
426
|
+
|
|
427
|
+
Raises:
|
|
428
|
+
ValueError: If the specified path does not exist in the argument structure.
|
|
429
|
+
"""
|
|
430
|
+
if self._check_path(path):
|
|
431
|
+
self._endpoints[path] = endpoint
|
|
432
|
+
else:
|
|
433
|
+
raise ValueError(f"The path '{path}' doesn't exist.")
|
|
434
|
+
|
|
435
|
+
def _parse_pre_args(self, pre_args: list[str]) -> list[str]:
|
|
436
|
+
"""Parses and validates preliminary arguments from the CLI.
|
|
437
|
+
|
|
438
|
+
This method traverses the argument structure and verifies that the provided
|
|
439
|
+
arguments match a valid command path. It returns a structured list of the
|
|
440
|
+
parsed arguments.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
pre_args (list[str]): List of preliminary command arguments from the CLI.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
list[str]: A list of parsed arguments that form a valid command path.
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
IndexError: If an argument does not match any expected value in the structure.
|
|
450
|
+
KeyError: If a required argument is missing from the structure.
|
|
451
|
+
"""
|
|
452
|
+
struct_lst = []
|
|
453
|
+
|
|
454
|
+
current_struct = self._structure
|
|
455
|
+
i = call = None
|
|
456
|
+
try:
|
|
457
|
+
for i, call in enumerate(pre_args):
|
|
458
|
+
if call in current_struct or (
|
|
459
|
+
"ANY" in current_struct and len(current_struct) == 1
|
|
460
|
+
):
|
|
461
|
+
struct_lst.append(call)
|
|
462
|
+
if not i == len(pre_args) - 1:
|
|
463
|
+
current_struct = current_struct[call]
|
|
464
|
+
elif len(current_struct) == 0: # At endpoint
|
|
465
|
+
break
|
|
466
|
+
else:
|
|
467
|
+
raise IndexError
|
|
468
|
+
except TypeError:
|
|
469
|
+
print("Too many pre arguments.")
|
|
470
|
+
self._lst_error(i, 0, pre_args, True)
|
|
471
|
+
except (IndexError, KeyError):
|
|
472
|
+
print(
|
|
473
|
+
f"The argument '{call}' doesn't exist in current_struct ({current_struct})."
|
|
474
|
+
)
|
|
475
|
+
self._lst_error(i, 0, pre_args, True)
|
|
476
|
+
return struct_lst
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _to_type(to_type: str, type_: _ty.Type[_A] | None) -> _A | None:
|
|
480
|
+
"""Converts a string to a specified type.
|
|
481
|
+
|
|
482
|
+
This method attempts to convert a string into a specified type. It supports
|
|
483
|
+
basic types, collections, and literals. For collections, it splits the input
|
|
484
|
+
string by whitespace. If a type cannot be determined, it returns None.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
to_type (str): The string to be converted.
|
|
488
|
+
type_ (_ty.Type[_A] | None): The target type for conversion.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
_A | None: The converted value, or None if the type is invalid.
|
|
492
|
+
"""
|
|
493
|
+
if not type_:
|
|
494
|
+
return None
|
|
495
|
+
if type_ in [list, tuple, set]:
|
|
496
|
+
tmp = to_type.split()
|
|
497
|
+
return type_(tmp)
|
|
498
|
+
elif _ty.get_origin(type_) is _ty.Literal:
|
|
499
|
+
choices = type_.__args__
|
|
500
|
+
choice_types = set(type(choice) for choice in choices)
|
|
501
|
+
if len(choice_types) == 1:
|
|
502
|
+
type_ = choice_types.pop() # All choices have the same type
|
|
503
|
+
else: # Choices have different types
|
|
504
|
+
for type_ in choice_types:
|
|
505
|
+
try:
|
|
506
|
+
result = type_(to_type)
|
|
507
|
+
except Exception:
|
|
508
|
+
continue
|
|
509
|
+
else:
|
|
510
|
+
return result
|
|
511
|
+
return type_(to_type)
|
|
512
|
+
|
|
513
|
+
@classmethod
|
|
514
|
+
def _parse_args_native_light(
|
|
515
|
+
cls, args: list[str], endpoint: Endpoint, smart_typing: bool = True
|
|
516
|
+
) -> dict[str, _ty.Any]:
|
|
517
|
+
"""Parses command-line arguments in a lightweight manner.
|
|
518
|
+
|
|
519
|
+
This method parses arguments for an endpoint function, supporting positional
|
|
520
|
+
arguments, keyword arguments (preceded by '--'), and flags (preceded by '-').
|
|
521
|
+
It also assigns default values if not all arguments are provided.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
args (list[str]): The list of arguments from the CLI.
|
|
525
|
+
endpoint (Endpoint): The endpoint for which arguments are parsed.
|
|
526
|
+
smart_typing (bool, optional): If True, attempts to match argument types
|
|
527
|
+
intelligently based on their default values.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
dict[str, _ty.Any]: A dictionary of parsed argument names and values.
|
|
531
|
+
|
|
532
|
+
Raises:
|
|
533
|
+
ArgumentParsingError: If an unknown argument is encountered.
|
|
534
|
+
"""
|
|
535
|
+
parsed_args = {}
|
|
536
|
+
remaining_args = list(endpoint.analysis["arguments"])
|
|
537
|
+
|
|
538
|
+
for i, arg in enumerate(args):
|
|
539
|
+
# Check for keyword argument
|
|
540
|
+
if arg.startswith("--"):
|
|
541
|
+
key, _, value = arg[2:].partition("=")
|
|
542
|
+
if not any(a["name"] == key for a in endpoint.analysis["arguments"]):
|
|
543
|
+
raise ArgumentParsingError(f"Unknown argument: {key}", i)
|
|
544
|
+
elif not value:
|
|
545
|
+
raise ArgumentParsingError(
|
|
546
|
+
f"No value: {key}, pleae use the format {key}=[value]", i
|
|
547
|
+
)
|
|
548
|
+
arg_obj = next(
|
|
549
|
+
a for a in endpoint.analysis["arguments"] if a["name"] == key
|
|
550
|
+
)
|
|
551
|
+
parsed_args[key] = (
|
|
552
|
+
cls._to_type(value, arg_obj["type"]) if arg_obj["type"] else value
|
|
553
|
+
)
|
|
554
|
+
remaining_args.remove(arg_obj)
|
|
555
|
+
|
|
556
|
+
# Check for flag argument
|
|
557
|
+
elif arg.startswith("-"):
|
|
558
|
+
key = arg[1:]
|
|
559
|
+
if not any(
|
|
560
|
+
a["name"] == key and a["type"] is bool
|
|
561
|
+
for a in endpoint.analysis["arguments"]
|
|
562
|
+
):
|
|
563
|
+
raise ArgumentParsingError(f"Unknown flag argument: {key}", i)
|
|
564
|
+
parsed_args[key] = True
|
|
565
|
+
remaining_arg = next(
|
|
566
|
+
a for a in endpoint.analysis["arguments"] if a["name"] == key
|
|
567
|
+
)
|
|
568
|
+
remaining_args.remove(remaining_arg)
|
|
569
|
+
|
|
570
|
+
# Handle positional argument
|
|
571
|
+
else:
|
|
572
|
+
if smart_typing:
|
|
573
|
+
# Find the first argument with a matching type
|
|
574
|
+
for pos_arg in remaining_args:
|
|
575
|
+
if (
|
|
576
|
+
isinstance(pos_arg["default"], type(arg))
|
|
577
|
+
or pos_arg["default"] is None
|
|
578
|
+
):
|
|
579
|
+
parsed_args[pos_arg["name"]] = cls._to_type(
|
|
580
|
+
arg, pos_arg["type"]
|
|
581
|
+
)
|
|
582
|
+
remaining_args.remove(pos_arg)
|
|
583
|
+
break
|
|
584
|
+
else:
|
|
585
|
+
raise ArgumentParsingError("No matching argument type found", i)
|
|
586
|
+
else:
|
|
587
|
+
# Assign to the next available argument
|
|
588
|
+
if remaining_args:
|
|
589
|
+
pos_arg = remaining_args.pop(0)
|
|
590
|
+
parsed_args[pos_arg["name"]] = cls._to_type(
|
|
591
|
+
arg, pos_arg["type"]
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# Assign default values for missing optional arguments
|
|
595
|
+
for remaining_arg in remaining_args:
|
|
596
|
+
if remaining_arg["default"] is not None:
|
|
597
|
+
parsed_args[remaining_arg["name"]] = remaining_arg["default"]
|
|
598
|
+
|
|
599
|
+
return parsed_args
|
|
600
|
+
|
|
601
|
+
@staticmethod
|
|
602
|
+
def _parse_args_arg_parse(
|
|
603
|
+
args: list[str], endpoint: Endpoint
|
|
604
|
+
) -> dict[str, _ty.Any]:
|
|
605
|
+
"""Parses command-line arguments using the argparse library.
|
|
606
|
+
|
|
607
|
+
Sets up argparse to support keyword arguments (prefixed with '--') and flag
|
|
608
|
+
arguments (prefixed with '-'), along with custom help texts.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
args (list[str]): The list of command-line arguments to parse.
|
|
612
|
+
endpoint (Endpoint): The endpoint that defines the argument structure.
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
dict[str, _ty.Any]: A dictionary of parsed argument names and values.
|
|
616
|
+
"""
|
|
617
|
+
parser = _ArgumentParser()
|
|
618
|
+
|
|
619
|
+
# Set up argparse for keyword and flag arguments
|
|
620
|
+
for arg in endpoint.analysis["arguments"]:
|
|
621
|
+
if arg["type"] is bool: # For boolean flags
|
|
622
|
+
parser.add_argument(
|
|
623
|
+
f"-{arg['name'][0]}",
|
|
624
|
+
f"--{arg['name']}",
|
|
625
|
+
action="store_true",
|
|
626
|
+
help=arg.get("help", "No help available"),
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
parser.add_argument(
|
|
630
|
+
f"--{arg['name']}",
|
|
631
|
+
type=arg["type"],
|
|
632
|
+
default=arg["default"],
|
|
633
|
+
help=arg.get("help", "No help available"),
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# Parse arguments with argparse
|
|
637
|
+
parsed_args = parser.parse_args(args)
|
|
638
|
+
return vars(parsed_args)
|
|
639
|
+
|
|
640
|
+
def _parse_args(
|
|
641
|
+
self,
|
|
642
|
+
args: list[str],
|
|
643
|
+
endpoint: Endpoint,
|
|
644
|
+
mode: _ty.Literal["arg_parse", "native_light"] = "arg_parse",
|
|
645
|
+
) -> dict[str, _ty.Any]:
|
|
646
|
+
"""Dispatches argument parsing to a specified mode.
|
|
647
|
+
|
|
648
|
+
This method selects the appropriate parsing function (argparse or native light)
|
|
649
|
+
and processes the arguments accordingly.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
args (list[str]): The list of command-line arguments.
|
|
653
|
+
endpoint (Endpoint): The endpoint defining argument requirements.
|
|
654
|
+
mode (Literal["arg_parse", "native_light"], optional): Parsing mode.
|
|
655
|
+
Defaults to `"arg_parse"`, but `"native_light"` can be used for lightweight parsing.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
dict[str, _ty.Any]: Parsed arguments as a dictionary.
|
|
659
|
+
"""
|
|
660
|
+
func = (
|
|
661
|
+
self._parse_args_native_light
|
|
662
|
+
if mode == "native_light"
|
|
663
|
+
else self._parse_args_arg_parse
|
|
664
|
+
)
|
|
665
|
+
return func(args, endpoint)
|
|
666
|
+
|
|
667
|
+
def parse_cli(
|
|
668
|
+
self,
|
|
669
|
+
system: sys = sys,
|
|
670
|
+
mode: _ty.Literal["arg_parse", "native_light"] = "arg_parse",
|
|
671
|
+
) -> None:
|
|
672
|
+
"""Parses CLI arguments and calls the endpoint based on the parsed path.
|
|
673
|
+
|
|
674
|
+
This method processes command-line input, navigates the argument structure,
|
|
675
|
+
and calls the relevant endpoint function. If the path is unmatched, it calls
|
|
676
|
+
the `default_endpoint`.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
system (sys, optional): System module to access arguments from `argv`.
|
|
680
|
+
mode (Literal["arg_parse", "native_light"], optional): Mode to parse
|
|
681
|
+
arguments. Defaults to `"arg_parse"`, but `"native_light"` can be used
|
|
682
|
+
for lightweight parsing.
|
|
683
|
+
"""
|
|
684
|
+
arguments = system.argv
|
|
685
|
+
pre_args = self._parse_pre_args(arguments)
|
|
686
|
+
path = ".".join(pre_args)
|
|
687
|
+
preargs_stop_idx: int
|
|
688
|
+
if len(pre_args) != 0:
|
|
689
|
+
preargs_stop_idx = (
|
|
690
|
+
arguments.index(pre_args[-1]) + 1
|
|
691
|
+
) # Fix => remove single "root" arg in next big update, also update the class name
|
|
692
|
+
else:
|
|
693
|
+
preargs_stop_idx = 1 # Temp fix
|
|
694
|
+
args = arguments[
|
|
695
|
+
preargs_stop_idx:
|
|
696
|
+
] # Will return an empty list, if [i:] is longer than the list
|
|
697
|
+
endpoint = self._endpoints.get(path) or self.default_endpoint
|
|
698
|
+
arguments = self._parse_args(args, endpoint, mode)
|
|
699
|
+
endpoint.call(**arguments)
|
endpoint/py.typed
ADDED
|
File without changes
|