falyx 0.1.62__py3-none-any.whl → 0.1.63__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.
- falyx/completer.py +20 -0
- falyx/falyx.py +0 -1
- falyx/parser/argument.py +2 -0
- falyx/parser/command_argument_parser.py +162 -43
- falyx/parser/signature.py +4 -1
- falyx/version.py +1 -1
- {falyx-0.1.62.dist-info → falyx-0.1.63.dist-info}/METADATA +1 -1
- {falyx-0.1.62.dist-info → falyx-0.1.63.dist-info}/RECORD +11 -12
- falyx/falyx_completer.py +0 -128
- {falyx-0.1.62.dist-info → falyx-0.1.63.dist-info}/LICENSE +0 -0
- {falyx-0.1.62.dist-info → falyx-0.1.63.dist-info}/WHEEL +0 -0
- {falyx-0.1.62.dist-info → falyx-0.1.63.dist-info}/entry_points.txt +0 -0
falyx/completer.py
CHANGED
@@ -29,6 +29,26 @@ class FalyxCompleter(Completer):
|
|
29
29
|
yield from self._suggest_commands(tokens[0] if tokens else "")
|
30
30
|
return
|
31
31
|
|
32
|
+
# Identify command
|
33
|
+
command_key = tokens[0].upper()
|
34
|
+
command = self.falyx._name_map.get(command_key)
|
35
|
+
if not command or not command.arg_parser:
|
36
|
+
return
|
37
|
+
|
38
|
+
# If at end of token, e.g., "--t" vs "--tag ", add a stub so suggest_next sees it
|
39
|
+
parsed_args = tokens[1:] if cursor_at_end_of_token else tokens[1:-1]
|
40
|
+
stub = "" if cursor_at_end_of_token else tokens[-1]
|
41
|
+
|
42
|
+
try:
|
43
|
+
suggestions = command.arg_parser.suggest_next(
|
44
|
+
parsed_args + ([stub] if stub else [])
|
45
|
+
)
|
46
|
+
for suggestion in suggestions:
|
47
|
+
if suggestion.startswith(stub):
|
48
|
+
yield Completion(suggestion, start_position=-len(stub))
|
49
|
+
except Exception:
|
50
|
+
return
|
51
|
+
|
32
52
|
def _suggest_commands(self, prefix: str) -> Iterable[Completion]:
|
33
53
|
prefix = prefix.upper()
|
34
54
|
keys = [self.falyx.exit_command.key]
|
falyx/falyx.py
CHANGED
@@ -507,7 +507,6 @@ class Falyx:
|
|
507
507
|
message=self.prompt,
|
508
508
|
multiline=False,
|
509
509
|
completer=self._get_completer(),
|
510
|
-
reserve_space_for_menu=1,
|
511
510
|
validator=CommandValidator(self, self._get_validator_error_message()),
|
512
511
|
bottom_toolbar=self._get_bottom_bar_render(),
|
513
512
|
key_bindings=self.key_bindings,
|
falyx/parser/argument.py
CHANGED
@@ -26,6 +26,7 @@ class Argument:
|
|
26
26
|
resolver (BaseAction | None):
|
27
27
|
An action object that resolves the argument, if applicable.
|
28
28
|
lazy_resolver (bool): True if the resolver should be called lazily, False otherwise
|
29
|
+
suggestions (list[str] | None): A list of suggestions for the argument.
|
29
30
|
"""
|
30
31
|
|
31
32
|
flags: tuple[str, ...]
|
@@ -40,6 +41,7 @@ class Argument:
|
|
40
41
|
positional: bool = False
|
41
42
|
resolver: BaseAction | None = None
|
42
43
|
lazy_resolver: bool = False
|
44
|
+
suggestions: list[str] | None = None
|
43
45
|
|
44
46
|
def get_positional_text(self) -> str:
|
45
47
|
"""Get the positional text for the argument."""
|
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from collections import defaultdict
|
6
6
|
from copy import deepcopy
|
7
|
-
from
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from typing import Any, Iterable, Sequence
|
8
9
|
|
9
10
|
from rich.console import Console
|
10
11
|
from rich.markup import escape
|
@@ -19,6 +20,12 @@ from falyx.parser.utils import coerce_value
|
|
19
20
|
from falyx.signals import HelpSignal
|
20
21
|
|
21
22
|
|
23
|
+
@dataclass
|
24
|
+
class ArgumentState:
|
25
|
+
arg: Argument
|
26
|
+
consumed: bool = False
|
27
|
+
|
28
|
+
|
22
29
|
class CommandArgumentParser:
|
23
30
|
"""
|
24
31
|
Custom argument parser for Falyx Commands.
|
@@ -64,6 +71,8 @@ class CommandArgumentParser:
|
|
64
71
|
self._flag_map: dict[str, Argument] = {}
|
65
72
|
self._dest_set: set[str] = set()
|
66
73
|
self._add_help()
|
74
|
+
self._last_positional_states: dict[str, ArgumentState] = {}
|
75
|
+
self._last_keyword_states: dict[str, ArgumentState] = {}
|
67
76
|
|
68
77
|
def _add_help(self):
|
69
78
|
"""Add help argument to the parser."""
|
@@ -359,19 +368,19 @@ class CommandArgumentParser:
|
|
359
368
|
)
|
360
369
|
|
361
370
|
self._register_argument(argument)
|
362
|
-
self._register_argument(negated_argument)
|
371
|
+
self._register_argument(negated_argument, bypass_validation=True)
|
363
372
|
|
364
|
-
def _register_argument(
|
373
|
+
def _register_argument(
|
374
|
+
self, argument: Argument, bypass_validation: bool = False
|
375
|
+
) -> None:
|
365
376
|
|
366
377
|
for flag in argument.flags:
|
367
|
-
if
|
368
|
-
flag in self._flag_map
|
369
|
-
and not argument.action == ArgumentAction.STORE_BOOL_OPTIONAL
|
370
|
-
):
|
378
|
+
if flag in self._flag_map and not bypass_validation:
|
371
379
|
existing = self._flag_map[flag]
|
372
380
|
raise CommandArgumentError(
|
373
381
|
f"Flag '{flag}' is already used by argument '{existing.dest}'"
|
374
382
|
)
|
383
|
+
|
375
384
|
for flag in argument.flags:
|
376
385
|
self._flag_map[flag] = argument
|
377
386
|
if not argument.positional:
|
@@ -396,6 +405,7 @@ class CommandArgumentParser:
|
|
396
405
|
dest: str | None = None,
|
397
406
|
resolver: BaseAction | None = None,
|
398
407
|
lazy_resolver: bool = True,
|
408
|
+
suggestions: list[str] | None = None,
|
399
409
|
) -> None:
|
400
410
|
"""Add an argument to the parser.
|
401
411
|
For `ArgumentAction.ACTION`, `nargs` and `type` determine how many and what kind
|
@@ -415,6 +425,8 @@ class CommandArgumentParser:
|
|
415
425
|
help: A brief description of the argument.
|
416
426
|
dest: The name of the attribute to be added to the object returned by parse_args().
|
417
427
|
resolver: A BaseAction called with optional nargs specified parsed arguments.
|
428
|
+
lazy_resolver: If True, the resolver is called lazily when the argument is accessed.
|
429
|
+
suggestions: A list of suggestions for the argument.
|
418
430
|
"""
|
419
431
|
expected_type = type
|
420
432
|
self._validate_flags(flags)
|
@@ -445,6 +457,10 @@ class CommandArgumentParser:
|
|
445
457
|
f"Default value '{default}' not in allowed choices: {choices}"
|
446
458
|
)
|
447
459
|
required = self._determine_required(required, positional, nargs, action)
|
460
|
+
if not isinstance(suggestions, Sequence) and suggestions is not None:
|
461
|
+
raise CommandArgumentError(
|
462
|
+
f"suggestions must be a list or None, got {type(suggestions)}"
|
463
|
+
)
|
448
464
|
if not isinstance(lazy_resolver, bool):
|
449
465
|
raise CommandArgumentError(
|
450
466
|
f"lazy_resolver must be a boolean, got {type(lazy_resolver)}"
|
@@ -465,6 +481,7 @@ class CommandArgumentParser:
|
|
465
481
|
positional=positional,
|
466
482
|
resolver=resolver,
|
467
483
|
lazy_resolver=lazy_resolver,
|
484
|
+
suggestions=suggestions,
|
468
485
|
)
|
469
486
|
self._register_argument(argument)
|
470
487
|
|
@@ -490,6 +507,27 @@ class CommandArgumentParser:
|
|
490
507
|
)
|
491
508
|
return defs
|
492
509
|
|
510
|
+
def raise_remaining_args_error(
|
511
|
+
self, token: str, arg_states: dict[str, ArgumentState]
|
512
|
+
) -> None:
|
513
|
+
consumed_dests = [
|
514
|
+
state.arg.dest for state in arg_states.values() if state.consumed
|
515
|
+
]
|
516
|
+
remaining_flags = [
|
517
|
+
flag
|
518
|
+
for flag, arg in self._keyword.items()
|
519
|
+
if arg.dest not in consumed_dests and flag.startswith(token)
|
520
|
+
]
|
521
|
+
|
522
|
+
if remaining_flags:
|
523
|
+
raise CommandArgumentError(
|
524
|
+
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(remaining_flags)}?"
|
525
|
+
)
|
526
|
+
else:
|
527
|
+
raise CommandArgumentError(
|
528
|
+
f"Unrecognized option '{token}'. Use --help to see available options."
|
529
|
+
)
|
530
|
+
|
493
531
|
def _consume_nargs(
|
494
532
|
self, args: list[str], start: int, spec: Argument
|
495
533
|
) -> tuple[list[str], int]:
|
@@ -535,6 +573,7 @@ class CommandArgumentParser:
|
|
535
573
|
result: dict[str, Any],
|
536
574
|
positional_args: list[Argument],
|
537
575
|
consumed_positional_indicies: set[int],
|
576
|
+
arg_states: dict[str, ArgumentState],
|
538
577
|
from_validate: bool = False,
|
539
578
|
) -> int:
|
540
579
|
remaining_positional_args = [
|
@@ -580,17 +619,7 @@ class CommandArgumentParser:
|
|
580
619
|
except Exception as error:
|
581
620
|
if len(args[i - new_i :]) == 1 and args[i - new_i].startswith("-"):
|
582
621
|
token = args[i - new_i]
|
583
|
-
|
584
|
-
flag for flag in self._flag_map if flag.startswith(token)
|
585
|
-
]
|
586
|
-
if valid_flags:
|
587
|
-
raise CommandArgumentError(
|
588
|
-
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
|
589
|
-
) from error
|
590
|
-
else:
|
591
|
-
raise CommandArgumentError(
|
592
|
-
f"Unrecognized option '{token}'. Use --help to see available options."
|
593
|
-
) from error
|
622
|
+
self.raise_remaining_args_error(token, arg_states)
|
594
623
|
else:
|
595
624
|
raise CommandArgumentError(
|
596
625
|
f"Invalid value for '{spec.dest}': {error}"
|
@@ -606,6 +635,7 @@ class CommandArgumentParser:
|
|
606
635
|
raise CommandArgumentError(
|
607
636
|
f"[{spec.dest}] Action failed: {error}"
|
608
637
|
) from error
|
638
|
+
arg_states[spec.dest].consumed = True
|
609
639
|
elif not typed and spec.default:
|
610
640
|
result[spec.dest] = spec.default
|
611
641
|
elif spec.action == ArgumentAction.APPEND:
|
@@ -618,8 +648,10 @@ class CommandArgumentParser:
|
|
618
648
|
assert result.get(spec.dest) is not None, "dest should not be None"
|
619
649
|
result[spec.dest].extend(typed)
|
620
650
|
elif spec.nargs in (None, 1, "?"):
|
651
|
+
arg_states[spec.dest].consumed = True
|
621
652
|
result[spec.dest] = typed[0] if len(typed) == 1 else typed
|
622
653
|
else:
|
654
|
+
arg_states[spec.dest].consumed = True
|
623
655
|
result[spec.dest] = typed
|
624
656
|
|
625
657
|
if spec.nargs not in ("*", "+"):
|
@@ -628,15 +660,7 @@ class CommandArgumentParser:
|
|
628
660
|
if i < len(args):
|
629
661
|
if len(args[i:]) == 1 and args[i].startswith("-"):
|
630
662
|
token = args[i]
|
631
|
-
|
632
|
-
if valid_flags:
|
633
|
-
raise CommandArgumentError(
|
634
|
-
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
|
635
|
-
)
|
636
|
-
else:
|
637
|
-
raise CommandArgumentError(
|
638
|
-
f"Unrecognized option '{token}'. Use --help to see available options."
|
639
|
-
)
|
663
|
+
self.raise_remaining_args_error(token, arg_states)
|
640
664
|
else:
|
641
665
|
plural = "s" if len(args[i:]) > 1 else ""
|
642
666
|
raise CommandArgumentError(
|
@@ -670,6 +694,7 @@ class CommandArgumentParser:
|
|
670
694
|
positional_args: list[Argument],
|
671
695
|
consumed_positional_indices: set[int],
|
672
696
|
consumed_indices: set[int],
|
697
|
+
arg_states: dict[str, ArgumentState],
|
673
698
|
from_validate: bool = False,
|
674
699
|
) -> int:
|
675
700
|
if token in self._keyword:
|
@@ -679,6 +704,7 @@ class CommandArgumentParser:
|
|
679
704
|
if action == ArgumentAction.HELP:
|
680
705
|
if not from_validate:
|
681
706
|
self.render_help()
|
707
|
+
arg_states[spec.dest].consumed = True
|
682
708
|
raise HelpSignal()
|
683
709
|
elif action == ArgumentAction.ACTION:
|
684
710
|
assert isinstance(
|
@@ -691,24 +717,29 @@ class CommandArgumentParser:
|
|
691
717
|
raise CommandArgumentError(
|
692
718
|
f"Invalid value for '{spec.dest}': {error}"
|
693
719
|
) from error
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
720
|
+
if not spec.lazy_resolver or not from_validate:
|
721
|
+
try:
|
722
|
+
result[spec.dest] = await spec.resolver(*typed_values)
|
723
|
+
except Exception as error:
|
724
|
+
raise CommandArgumentError(
|
725
|
+
f"[{spec.dest}] Action failed: {error}"
|
726
|
+
) from error
|
727
|
+
arg_states[spec.dest].consumed = True
|
700
728
|
consumed_indices.update(range(i, new_i))
|
701
729
|
i = new_i
|
702
730
|
elif action == ArgumentAction.STORE_TRUE:
|
703
731
|
result[spec.dest] = True
|
732
|
+
arg_states[spec.dest].consumed = True
|
704
733
|
consumed_indices.add(i)
|
705
734
|
i += 1
|
706
735
|
elif action == ArgumentAction.STORE_FALSE:
|
707
736
|
result[spec.dest] = False
|
737
|
+
arg_states[spec.dest].consumed = True
|
708
738
|
consumed_indices.add(i)
|
709
739
|
i += 1
|
710
740
|
elif action == ArgumentAction.STORE_BOOL_OPTIONAL:
|
711
741
|
result[spec.dest] = spec.type(True)
|
742
|
+
arg_states[spec.dest].consumed = True
|
712
743
|
consumed_indices.add(i)
|
713
744
|
i += 1
|
714
745
|
elif action == ArgumentAction.COUNT:
|
@@ -778,19 +809,11 @@ class CommandArgumentParser:
|
|
778
809
|
)
|
779
810
|
else:
|
780
811
|
result[spec.dest] = typed_values
|
812
|
+
arg_states[spec.dest].consumed = True
|
781
813
|
consumed_indices.update(range(i, new_i))
|
782
814
|
i = new_i
|
783
815
|
elif token.startswith("-"):
|
784
|
-
|
785
|
-
valid_flags = [flag for flag in self._flag_map if flag.startswith(token)]
|
786
|
-
if valid_flags:
|
787
|
-
raise CommandArgumentError(
|
788
|
-
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
|
789
|
-
)
|
790
|
-
else:
|
791
|
-
raise CommandArgumentError(
|
792
|
-
f"Unrecognized option '{token}'. Use --help to see available options."
|
793
|
-
)
|
816
|
+
self.raise_remaining_args_error(token, arg_states)
|
794
817
|
else:
|
795
818
|
# Get the next flagged argument index if it exists
|
796
819
|
next_flagged_index = -1
|
@@ -805,6 +828,7 @@ class CommandArgumentParser:
|
|
805
828
|
result,
|
806
829
|
positional_args,
|
807
830
|
consumed_positional_indices,
|
831
|
+
arg_states=arg_states,
|
808
832
|
from_validate=from_validate,
|
809
833
|
)
|
810
834
|
i += args_consumed
|
@@ -817,6 +841,14 @@ class CommandArgumentParser:
|
|
817
841
|
if args is None:
|
818
842
|
args = []
|
819
843
|
|
844
|
+
arg_states = {arg.dest: ArgumentState(arg) for arg in self._arguments}
|
845
|
+
self._last_positional_states = {
|
846
|
+
arg.dest: arg_states[arg.dest] for arg in self._positional.values()
|
847
|
+
}
|
848
|
+
self._last_keyword_states = {
|
849
|
+
arg.dest: arg_states[arg.dest] for arg in self._keyword_list
|
850
|
+
}
|
851
|
+
|
820
852
|
result = {arg.dest: deepcopy(arg.default) for arg in self._arguments}
|
821
853
|
positional_args: list[Argument] = [
|
822
854
|
arg for arg in self._arguments if arg.positional
|
@@ -838,6 +870,7 @@ class CommandArgumentParser:
|
|
838
870
|
positional_args,
|
839
871
|
consumed_positional_indices,
|
840
872
|
consumed_indices,
|
873
|
+
arg_states=arg_states,
|
841
874
|
from_validate=from_validate,
|
842
875
|
)
|
843
876
|
|
@@ -862,6 +895,7 @@ class CommandArgumentParser:
|
|
862
895
|
)
|
863
896
|
|
864
897
|
if spec.choices and result.get(spec.dest) not in spec.choices:
|
898
|
+
arg_states[spec.dest].consumed = False
|
865
899
|
raise CommandArgumentError(
|
866
900
|
f"Invalid value for '{spec.dest}': must be one of {{{', '.join(spec.choices)}}}"
|
867
901
|
)
|
@@ -914,6 +948,91 @@ class CommandArgumentParser:
|
|
914
948
|
kwargs_dict[arg.dest] = parsed[arg.dest]
|
915
949
|
return tuple(args_list), kwargs_dict
|
916
950
|
|
951
|
+
def suggest_next(self, args: list[str]) -> list[str]:
|
952
|
+
"""
|
953
|
+
Suggest the next possible flags or values given partially typed arguments.
|
954
|
+
|
955
|
+
This does NOT raise errors. It is intended for completions, not validation.
|
956
|
+
|
957
|
+
Returns:
|
958
|
+
A list of possible completions based on the current input.
|
959
|
+
"""
|
960
|
+
|
961
|
+
# Case 1: Next positional argument
|
962
|
+
next_non_consumed_positional: Argument | None = None
|
963
|
+
for state in self._last_positional_states.values():
|
964
|
+
if not state.consumed:
|
965
|
+
next_non_consumed_positional = state.arg
|
966
|
+
break
|
967
|
+
if next_non_consumed_positional:
|
968
|
+
if next_non_consumed_positional.choices:
|
969
|
+
return sorted(
|
970
|
+
(str(choice) for choice in next_non_consumed_positional.choices)
|
971
|
+
)
|
972
|
+
if next_non_consumed_positional.suggestions:
|
973
|
+
return sorted(next_non_consumed_positional.suggestions)
|
974
|
+
|
975
|
+
consumed_dests = [
|
976
|
+
state.arg.dest
|
977
|
+
for state in self._last_keyword_states.values()
|
978
|
+
if state.consumed
|
979
|
+
]
|
980
|
+
|
981
|
+
remaining_flags = [
|
982
|
+
flag for flag, arg in self._keyword.items() if arg.dest not in consumed_dests
|
983
|
+
]
|
984
|
+
|
985
|
+
last = args[-1]
|
986
|
+
next_to_last = args[-2] if len(args) > 1 else ""
|
987
|
+
suggestions: list[str] = []
|
988
|
+
|
989
|
+
# Case 2: Mid-flag (e.g., "--ver")
|
990
|
+
if last.startswith("-") and last not in self._keyword:
|
991
|
+
if (
|
992
|
+
len(args) > 1
|
993
|
+
and next_to_last in self._keyword
|
994
|
+
and next_to_last in remaining_flags
|
995
|
+
):
|
996
|
+
# If the last token is a mid-flag, suggest based on the previous flag
|
997
|
+
arg = self._keyword[next_to_last]
|
998
|
+
if arg.choices:
|
999
|
+
suggestions.extend(arg.choices)
|
1000
|
+
elif arg.suggestions:
|
1001
|
+
suggestions.extend(arg.suggestions)
|
1002
|
+
else:
|
1003
|
+
possible_flags = [
|
1004
|
+
flag
|
1005
|
+
for flag, arg in self._keyword.items()
|
1006
|
+
if flag.startswith(last) and arg.dest not in consumed_dests
|
1007
|
+
]
|
1008
|
+
suggestions.extend(possible_flags)
|
1009
|
+
# Case 3: Flag that expects a value (e.g., ["--tag"])
|
1010
|
+
elif last in self._keyword:
|
1011
|
+
arg = self._keyword[last]
|
1012
|
+
if arg.choices:
|
1013
|
+
suggestions.extend(arg.choices)
|
1014
|
+
elif arg.suggestions:
|
1015
|
+
suggestions.extend(arg.suggestions)
|
1016
|
+
# Case 4: Last flag with choices mid-choice (e.g., ["--tag", "v"])
|
1017
|
+
elif next_to_last in self._keyword:
|
1018
|
+
arg = self._keyword[next_to_last]
|
1019
|
+
if arg.choices and last not in arg.choices:
|
1020
|
+
suggestions.extend(arg.choices)
|
1021
|
+
elif (
|
1022
|
+
arg.suggestions
|
1023
|
+
and last not in arg.suggestions
|
1024
|
+
and not any(last.startswith(suggestion) for suggestion in arg.suggestions)
|
1025
|
+
and any(suggestion.startswith(last) for suggestion in arg.suggestions)
|
1026
|
+
):
|
1027
|
+
suggestions.extend(arg.suggestions)
|
1028
|
+
else:
|
1029
|
+
suggestions.extend(remaining_flags)
|
1030
|
+
# Case 5: Suggest all remaining flags
|
1031
|
+
else:
|
1032
|
+
suggestions.extend(remaining_flags)
|
1033
|
+
|
1034
|
+
return sorted(set(suggestions))
|
1035
|
+
|
917
1036
|
def get_options_text(self, plain_text=False) -> str:
|
918
1037
|
# Options
|
919
1038
|
# Add all keyword arguments to the options list
|
falyx/parser/signature.py
CHANGED
@@ -54,8 +54,10 @@ def infer_args_from_func(
|
|
54
54
|
if arg_type is bool:
|
55
55
|
if param.default is False:
|
56
56
|
action = "store_true"
|
57
|
-
|
57
|
+
default = None
|
58
|
+
elif param.default is True:
|
58
59
|
action = "store_false"
|
60
|
+
default = None
|
59
61
|
|
60
62
|
if arg_type is list:
|
61
63
|
action = "append"
|
@@ -75,6 +77,7 @@ def infer_args_from_func(
|
|
75
77
|
"action": action,
|
76
78
|
"help": metadata.get("help", ""),
|
77
79
|
"choices": metadata.get("choices"),
|
80
|
+
"suggestions": metadata.get("suggestions"),
|
78
81
|
}
|
79
82
|
)
|
80
83
|
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.63"
|
@@ -28,15 +28,14 @@ falyx/action/signal_action.py,sha256=GxV-0zqYqODOQUa3-tvFTZ2AS1W1QpW6ExonxmWNWbs
|
|
28
28
|
falyx/action/user_input_action.py,sha256=Up47lumscxnhORMvaft0X-NWpxTXc2GmMZMua__pGhA,3524
|
29
29
|
falyx/bottom_bar.py,sha256=B62N3YCQF_h2Rw_hpc2_FUuLNARI-XIGbQkg-1XvaYE,7405
|
30
30
|
falyx/command.py,sha256=QdcwLEFIaq3a4Lfot4cV3zHbVJNQxwSpShprBgLBkh8,16891
|
31
|
-
falyx/completer.py,sha256=
|
31
|
+
falyx/completer.py,sha256=tCePNM6NoVmgbDobE6HSrR34KiJ9N5GwXYL2lcdqCfk,2461
|
32
32
|
falyx/config.py,sha256=OFEq9pFhV39o6_D7dP_QUDjqEusSQNpgomRsh5AAZYY,9621
|
33
33
|
falyx/console.py,sha256=WIZ004R9x6DJp2g3onBQ4DOJ7iDeTOr8HqJCwRt30Rc,143
|
34
34
|
falyx/context.py,sha256=M7iWEKto_NwI3GM-VCDPwXT0dBpFPf1Y_RvHQKodZgI,10804
|
35
35
|
falyx/debug.py,sha256=pguI0XQcZ-7jte5YUPexAufa1oxxalYO1JgmO6GU3rI,1557
|
36
36
|
falyx/exceptions.py,sha256=58D4BYkyJ7PlpZoNk37GsUsFThm_gIlb2Ex2XXhLklI,1099
|
37
37
|
falyx/execution_registry.py,sha256=RLRMOEmfDElFy4tuC8L9tRyNToX7GJ4GoEBh2Iri8zo,7662
|
38
|
-
falyx/falyx.py,sha256=
|
39
|
-
falyx/falyx_completer.py,sha256=MsfuZXpfGwbsGG-4Zp-j-vNsNnaote-UAJkJh0s2NZI,5236
|
38
|
+
falyx/falyx.py,sha256=WajWR9MZwZAno4gnXQR-RCIS4hEqiY25Egrfu6Wz2ls,49290
|
40
39
|
falyx/hook_manager.py,sha256=TFuHQnAncS_rk6vuw-VSx8bnAppLuHfrZCrzLwqcO9o,2979
|
41
40
|
falyx/hooks.py,sha256=xMfQROib0BNsaQF4AXJpmCiGePoE1f1xpcdibgnVZWM,2913
|
42
41
|
falyx/init.py,sha256=fZ8cvJ9rTGOhvAiAUmA7aPw9FsOUIuobTPW3sz47My8,3287
|
@@ -45,12 +44,12 @@ falyx/menu.py,sha256=9kvLZhkC8PoSQvv1NZQsPIFSDy11dXfFgqVAuDmtfsM,3752
|
|
45
44
|
falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
|
46
45
|
falyx/parser/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
46
|
falyx/parser/__init__.py,sha256=NbxAovKIY-duFTs6DAsdM_OzL7s3VIu19KMOmltX9ts,512
|
48
|
-
falyx/parser/argument.py,sha256=
|
47
|
+
falyx/parser/argument.py,sha256=pX1qTAtsqw2qBlu8dKRdIfViNC3Pg3QZtdUb7ZTT5oc,4226
|
49
48
|
falyx/parser/argument_action.py,sha256=Lcpb9siYr_q2T8qU-jXVtqFb11bFPPKEGH3gurJv2NM,757
|
50
|
-
falyx/parser/command_argument_parser.py,sha256=
|
49
|
+
falyx/parser/command_argument_parser.py,sha256=n6yTIGuwproH0RC2ZJ1T4RNpr97OVs4kt8GxVB2pbcw,46092
|
51
50
|
falyx/parser/parser_types.py,sha256=DLLuIXE8cAVLS41trfsNy-XJmtqSa1HfnJVAYIIc42w,315
|
52
51
|
falyx/parser/parsers.py,sha256=vb-l_NNh5O9L98Lcafhz91flRLxC1BnW6U8JdeabRCw,14118
|
53
|
-
falyx/parser/signature.py,sha256=
|
52
|
+
falyx/parser/signature.py,sha256=yGcd_Clcoz1YmbCgmP61MR2aNU5H6X_PhdW1fW6pvKs,2673
|
54
53
|
falyx/parser/utils.py,sha256=GlxB1WORwoJ5XUtmmAVBUPaDV2nF9Hio7TbvNJvd8oY,3006
|
55
54
|
falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
|
56
55
|
falyx/protocols.py,sha256=vd9JL-TXdLEiAQXLw2UKLd3MUMivoG7iMLo08ZggwYQ,539
|
@@ -63,9 +62,9 @@ falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
|
|
63
62
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
64
63
|
falyx/utils.py,sha256=U45xnZFUdoFC4xiji_9S1jHS5V7MvxSDtufP8EgB0SM,6732
|
65
64
|
falyx/validators.py,sha256=AXpMGnk1_7J7MAbbol6pkMAiSIdNHoF5pwtA2-xS6H8,6029
|
66
|
-
falyx/version.py,sha256=
|
67
|
-
falyx-0.1.
|
68
|
-
falyx-0.1.
|
69
|
-
falyx-0.1.
|
70
|
-
falyx-0.1.
|
71
|
-
falyx-0.1.
|
65
|
+
falyx/version.py,sha256=cTmhGBJnpgNHayq6sOIrPTQIgyT8ZhqsvcCSuLORZYQ,23
|
66
|
+
falyx-0.1.63.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
67
|
+
falyx-0.1.63.dist-info/METADATA,sha256=-gRHGPkv51x3udjxmGyVcavpHi1SlOSZj_EyIUvM40s,5561
|
68
|
+
falyx-0.1.63.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
69
|
+
falyx-0.1.63.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
70
|
+
falyx-0.1.63.dist-info/RECORD,,
|
falyx/falyx_completer.py
DELETED
@@ -1,128 +0,0 @@
|
|
1
|
-
from collections import Counter
|
2
|
-
from prompt_toolkit.completion import Completer, Completion
|
3
|
-
from prompt_toolkit.document import Document
|
4
|
-
from typing import Iterable, Set, Optional
|
5
|
-
import shlex
|
6
|
-
|
7
|
-
from falyx.command import Command
|
8
|
-
from falyx.parser.command_argument_parser import CommandArgumentParser
|
9
|
-
from falyx.parser.argument import Argument
|
10
|
-
from falyx.parser.argument_action import ArgumentAction
|
11
|
-
|
12
|
-
class FalyxCompleter(Completer):
|
13
|
-
"""Completer for Falyx commands and their arguments."""
|
14
|
-
def __init__(self, falyx: "Falyx"):
|
15
|
-
self.falyx = falyx
|
16
|
-
self._used_args: Set[str] = set()
|
17
|
-
self._used_args_counter: Counter = Counter()
|
18
|
-
|
19
|
-
def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
|
20
|
-
text = document.text_before_cursor
|
21
|
-
try:
|
22
|
-
tokens = shlex.split(text)
|
23
|
-
cursor_at_end_of_token = document.text_before_cursor.endswith((' ', '\t'))
|
24
|
-
except ValueError:
|
25
|
-
return
|
26
|
-
|
27
|
-
if not tokens or (len(tokens) == 1 and not cursor_at_end_of_token):
|
28
|
-
# Suggest command keys and aliases
|
29
|
-
yield from self._suggest_commands(tokens[0] if tokens else "")
|
30
|
-
return
|
31
|
-
|
32
|
-
command = self._match_command(tokens[0])
|
33
|
-
if not command:
|
34
|
-
return
|
35
|
-
|
36
|
-
if command.arg_parser is None:
|
37
|
-
return
|
38
|
-
|
39
|
-
self._set_used_args(tokens, command)
|
40
|
-
|
41
|
-
next_arg = self._next_expected_argument(tokens, command.arg_parser)
|
42
|
-
|
43
|
-
if next_arg:
|
44
|
-
# Positional arguments or required flagged arguments
|
45
|
-
yield from self._suggest_argument(next_arg, document)
|
46
|
-
else:
|
47
|
-
# Optional arguments
|
48
|
-
for arg in command.arg_parser._keyword.values():
|
49
|
-
if not self._arg_already_used(arg.dest):
|
50
|
-
yield from self._suggest_argument(arg, document)
|
51
|
-
|
52
|
-
def _set_used_args(self, tokens: list[str], command: Command) -> None:
|
53
|
-
"""Extracts used argument flags from the provided tokens."""
|
54
|
-
if not command.arg_parser:
|
55
|
-
return
|
56
|
-
self._used_args.clear()
|
57
|
-
self._used_args_counter.clear()
|
58
|
-
for token in tokens[1:]:
|
59
|
-
if token.startswith('-'):
|
60
|
-
if keyword_argument := command.arg_parser._keyword.get(token):
|
61
|
-
self._used_args_counter[keyword_argument.dest] += 1
|
62
|
-
if isinstance(keyword_argument.nargs, int) and self._used_args_counter[keyword_argument.dest] > keyword_argument.nargs:
|
63
|
-
continue
|
64
|
-
elif isinstance(keyword_argument.nargs, str) and keyword_argument.nargs in ("?"):
|
65
|
-
self._used_args.add(keyword_argument.dest)
|
66
|
-
else:
|
67
|
-
self._used_args.add(keyword_argument.dest)
|
68
|
-
else:
|
69
|
-
# Handle positional arguments
|
70
|
-
if command.arg_parser._positional:
|
71
|
-
for arg in command.arg_parser._positional.values():
|
72
|
-
if arg.dest not in self._used_args:
|
73
|
-
self._used_args.add(arg.dest)
|
74
|
-
break
|
75
|
-
print(f"Used args: {self._used_args}, Counter: {self._used_args_counter}")
|
76
|
-
|
77
|
-
def _suggest_commands(self, prefix: str) -> Iterable[Completion]:
|
78
|
-
prefix = prefix.upper()
|
79
|
-
seen = set()
|
80
|
-
for cmd in self.falyx.commands.values():
|
81
|
-
for key in [cmd.key] + cmd.aliases:
|
82
|
-
if key.upper().startswith(prefix) and key not in seen:
|
83
|
-
yield Completion(key, start_position=-len(prefix))
|
84
|
-
seen.add(key)
|
85
|
-
|
86
|
-
def _match_command(self, token: str) -> Optional[Command]:
|
87
|
-
token = token.lstrip("?").upper()
|
88
|
-
return self.falyx._name_map.get(token)
|
89
|
-
|
90
|
-
def _next_expected_argument(
|
91
|
-
self, tokens: list[str], parser: CommandArgumentParser
|
92
|
-
) -> Optional[Argument]:
|
93
|
-
"""Determine the next expected argument based on the current tokens."""
|
94
|
-
# Positional arguments first
|
95
|
-
for arg in parser._positional.values():
|
96
|
-
if arg.dest not in self._used_args:
|
97
|
-
return arg
|
98
|
-
|
99
|
-
# Then required keyword arguments
|
100
|
-
for arg in parser._keyword_list:
|
101
|
-
if arg.required and not self._arg_already_used(arg.dest):
|
102
|
-
return arg
|
103
|
-
|
104
|
-
return None
|
105
|
-
|
106
|
-
def _arg_already_used(self, dest: str) -> bool:
|
107
|
-
print(f"Checking if argument '{dest}' is already used: {dest in self._used_args} - Used args: {self._used_args}")
|
108
|
-
return dest in self._used_args
|
109
|
-
|
110
|
-
def _suggest_argument(self, arg: Argument, document: Document) -> Iterable[Completion]:
|
111
|
-
if not arg.positional:
|
112
|
-
for flag in arg.flags:
|
113
|
-
yield Completion(flag, start_position=0)
|
114
|
-
|
115
|
-
if arg.choices:
|
116
|
-
for choice in arg.choices:
|
117
|
-
yield Completion(
|
118
|
-
choice,
|
119
|
-
start_position=0,
|
120
|
-
display=f"{arg.dest}={choice}"
|
121
|
-
)
|
122
|
-
|
123
|
-
if arg.default is not None and arg.action == ArgumentAction.STORE:
|
124
|
-
yield Completion(
|
125
|
-
str(arg.default),
|
126
|
-
start_position=0,
|
127
|
-
display=f"{arg.dest} (default: {arg.default})"
|
128
|
-
)
|
File without changes
|
File without changes
|
File without changes
|