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 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