freyacli 0.1.0__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.
- freyacli/__init__.py +18 -0
- freyacli/_arguments/__init__.py +0 -0
- freyacli/_arguments/arg_count.py +118 -0
- freyacli/_arguments/arg_dtype.py +61 -0
- freyacli/_arguments/argument_rule.py +217 -0
- freyacli/_arguments/subcommand.py +170 -0
- freyacli/_core/__init__.py +0 -0
- freyacli/_core/app.py +164 -0
- freyacli/_misc/__init__.py +0 -0
- freyacli/_misc/arg_dtype_error.py +12 -0
- freyacli/_misc/freya_syntax_error.py +12 -0
- freyacli/_misc/help_str.py +53 -0
- freyacli/_parsers/__init__.py +0 -0
- freyacli/_parsers/args_parser.py +219 -0
- freyacli/_parsers/freya_parser.py +161 -0
- freyacli/_utils/__init__.py +0 -0
- freyacli/_utils/color.py +69 -0
- freyacli/_utils/path_assertion.py +60 -0
- freyacli/_version.py +1 -0
- freyacli-0.1.0.dist-info/METADATA +91 -0
- freyacli-0.1.0.dist-info/RECORD +24 -0
- freyacli-0.1.0.dist-info/WHEEL +5 -0
- freyacli-0.1.0.dist-info/licenses/LICENSE.md +22 -0
- freyacli-0.1.0.dist-info/top_level.txt +1 -0
freyacli/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ._misc.freya_syntax_error import FreyaSyntaxError
|
|
2
|
+
from ._misc.arg_dtype_error import ArgDTypeError
|
|
3
|
+
from ._misc.help_str import HelpStr
|
|
4
|
+
|
|
5
|
+
from ._utils.color import Color
|
|
6
|
+
from ._utils.path_assertion import PathAssertion
|
|
7
|
+
|
|
8
|
+
from ._arguments.arg_count import ArgCount
|
|
9
|
+
from ._arguments.arg_dtype import ArgDType
|
|
10
|
+
from ._arguments.argument_rule import ArgumentRule
|
|
11
|
+
from ._arguments.subcommand import Subcommand
|
|
12
|
+
|
|
13
|
+
from ._parsers.freya_parser import FreyaParser
|
|
14
|
+
from ._parsers.args_parser import ArgsParser
|
|
15
|
+
|
|
16
|
+
from ._core.app import App
|
|
17
|
+
|
|
18
|
+
WIDTH_TERMINAL: int = 1 # automatically set to the current terminal width when App is initialized
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import freyacli as fy
|
|
2
|
+
|
|
3
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
4
|
+
class ArgCount:
|
|
5
|
+
"""
|
|
6
|
+
`FYR` syntax follows a similarity to regex conventions for specifying amount of expected arguments.
|
|
7
|
+
|
|
8
|
+
- `n` -> exactly n
|
|
9
|
+
- `min_nvalues = n`, `val_optional = False`, `unlimited = False`
|
|
10
|
+
- `FYR` example: `~sphere[s,sphere]4.FLOAT`
|
|
11
|
+
- this means that `-s` must be passed with exactly `4` float values.
|
|
12
|
+
- Automatic help string:
|
|
13
|
+
- n = 1: `<value>`
|
|
14
|
+
- n > 1: `<value(n)>`
|
|
15
|
+
|
|
16
|
+
- `n?` -> 0 or n
|
|
17
|
+
- `min_nvalues = n`, `val_optional = True`, `unlimited = False`
|
|
18
|
+
- `FYR` example: `~out_mrc[m,mrc]1?.PATH`
|
|
19
|
+
- this means `-m` can be passed with exactly `1` value or without a value.
|
|
20
|
+
- Automatic help string:
|
|
21
|
+
- n = 1: `[<value>]`
|
|
22
|
+
- n > 1: `[<value(n)>]`
|
|
23
|
+
|
|
24
|
+
- `*` -> 0 or more
|
|
25
|
+
- `min_nvalues = 0`, `val_optional = True`, `unlimited = True`
|
|
26
|
+
- `FYR` example: `~configs[c,config]*.STR`
|
|
27
|
+
- this means that `-c` can be passed with or without values. Amount of values is arbitrary.
|
|
28
|
+
- Automatic help string: `[<value...>]`
|
|
29
|
+
|
|
30
|
+
- `n+` -> n or more
|
|
31
|
+
- `min_nvalues = n`, `val_optional = False`, `unlimited = True`
|
|
32
|
+
- `FYR` example: `~resids[i,resids]1+.STR`
|
|
33
|
+
- this means that `-i` must be passed with *at least* `1` value.
|
|
34
|
+
- Automatic help string:
|
|
35
|
+
- n = 1: `<value...>`
|
|
36
|
+
- n > 1: `<value(n)[...]>`
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# --------------------------------------------------------------------------
|
|
40
|
+
def __init__(self, min_nvalues: int = 0, val_optional: bool = False, unlimited: bool = False):
|
|
41
|
+
self.min_nvalues = min_nvalues # 0 for *, n otherwise
|
|
42
|
+
self.val_optional = val_optional # True for n? and *, False for n and n+
|
|
43
|
+
self.unlimited = unlimited # True for * and n+, False for n and n?
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --------------------------------------------------------------------------
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_str(cls, string: str) -> "ArgCount":
|
|
49
|
+
def _safe_substr(s: str) -> str:
|
|
50
|
+
substr = s[:-1]
|
|
51
|
+
if not substr: raise fy.FreyaSyntaxError(
|
|
52
|
+
f"Invalid argument count specification: '{s}'."+\
|
|
53
|
+
"Expected format examples: '3', '3?', '*', '3+'."+\
|
|
54
|
+
"Number is mandatory for all formats except '*', which always means '0 or more'."
|
|
55
|
+
)
|
|
56
|
+
return substr
|
|
57
|
+
|
|
58
|
+
def _safe_int(s: str) -> int:
|
|
59
|
+
try: return int(s)
|
|
60
|
+
except ValueError: raise fy.FreyaSyntaxError(
|
|
61
|
+
f"Invalid number in argument count specification: '{s}'."+\
|
|
62
|
+
"Expected format examples: '3', '3?', '*', '3+'."+\
|
|
63
|
+
"Number is mandatory for all formats except '*', which always means '0 or more'."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if string.endswith("?"): return cls(
|
|
67
|
+
min_nvalues = _safe_int(_safe_substr(string)),
|
|
68
|
+
unlimited = False,
|
|
69
|
+
val_optional = True
|
|
70
|
+
)
|
|
71
|
+
if string.endswith("*"): return cls(
|
|
72
|
+
min_nvalues = 0,
|
|
73
|
+
unlimited = True,
|
|
74
|
+
val_optional = True
|
|
75
|
+
)
|
|
76
|
+
if string.endswith("+"): return cls(
|
|
77
|
+
min_nvalues = _safe_int(_safe_substr(string)),
|
|
78
|
+
unlimited = True,
|
|
79
|
+
val_optional = False
|
|
80
|
+
)
|
|
81
|
+
return cls(
|
|
82
|
+
min_nvalues = _safe_int(string),
|
|
83
|
+
unlimited = False,
|
|
84
|
+
val_optional = False
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------
|
|
89
|
+
def get_help_str(self, buffer: str) -> str:
|
|
90
|
+
if self.val_optional:
|
|
91
|
+
if self.unlimited: return f"[<{buffer}...>]" # * case
|
|
92
|
+
if self.min_nvalues == 1: return f"[<{buffer}>]" # n? case with n=1
|
|
93
|
+
return f"[<{buffer}({self.min_nvalues})>]" # n? case with n>1
|
|
94
|
+
|
|
95
|
+
if self.unlimited:
|
|
96
|
+
if self.min_nvalues == 1: return f"<{buffer}...>" # n+ case with n=1
|
|
97
|
+
return f"<{buffer}({self.min_nvalues})[...]>" # n+ case with n>1
|
|
98
|
+
|
|
99
|
+
if self.min_nvalues == 1: return f"<{buffer}>" # n case with n=1
|
|
100
|
+
return f"<{buffer}({self.min_nvalues})>" # n case with n>1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --------------------------------------------------------------------------
|
|
104
|
+
def is_full(self, nvalues: int) -> bool:
|
|
105
|
+
"""Returns `True` for an argument that shouldn't take more values."""
|
|
106
|
+
if self.unlimited: return False
|
|
107
|
+
return nvalues >= self.min_nvalues
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
def has_enough_values(self, nvalues: int) -> bool:
|
|
112
|
+
"""Returns `True` for an argument that has already received enough values."""
|
|
113
|
+
if self.val_optional: return True
|
|
114
|
+
if self.unlimited: return nvalues >= self.min_nvalues
|
|
115
|
+
return nvalues == self.min_nvalues
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import freyacli as fy
|
|
5
|
+
|
|
6
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
7
|
+
class ArgDType(Enum):
|
|
8
|
+
NONE = auto()
|
|
9
|
+
TOGGLE = auto()
|
|
10
|
+
STR = auto()
|
|
11
|
+
PATH = auto()
|
|
12
|
+
INT = auto()
|
|
13
|
+
FLOAT = auto()
|
|
14
|
+
|
|
15
|
+
# --------------------------------------------------------------------------
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_str(cls, s: str | None) -> "ArgDType":
|
|
18
|
+
if s is None: return cls.NONE
|
|
19
|
+
s = s.lower()
|
|
20
|
+
if not s: return cls.TOGGLE
|
|
21
|
+
if s == "str": return cls.STR
|
|
22
|
+
if s == "path": return cls.PATH
|
|
23
|
+
if s == "float": return cls.FLOAT
|
|
24
|
+
if s == "int": return cls.INT
|
|
25
|
+
raise fy.FreyaSyntaxError(f"Invalid flag type specified: '{s}'")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------
|
|
29
|
+
def stores_data(self) -> bool:
|
|
30
|
+
return self != ArgDType.NONE and self != ArgDType.TOGGLE
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------
|
|
33
|
+
def parse_value(self, arg_name: str, raw_value: str) -> fy.ArgDTypeError |\
|
|
34
|
+
bool|str|Path|int|float:
|
|
35
|
+
|
|
36
|
+
if self == ArgDType.NONE:
|
|
37
|
+
raise fy.FreyaSyntaxError(f"Argument doesn't accept any values, but somehow got '{raw_value}'.")
|
|
38
|
+
|
|
39
|
+
if self == ArgDType.TOGGLE:
|
|
40
|
+
raise fy.FreyaSyntaxError(f"Argument is a flag of type TOGGLE and doesn't accept any values, but somehow got '{raw_value}'.")
|
|
41
|
+
|
|
42
|
+
if self == ArgDType.STR:
|
|
43
|
+
return raw_value.strip('"').strip("'")
|
|
44
|
+
|
|
45
|
+
if self == ArgDType.PATH:
|
|
46
|
+
return Path(raw_value)
|
|
47
|
+
|
|
48
|
+
if self == ArgDType.INT:
|
|
49
|
+
try: return int(raw_value)
|
|
50
|
+
except ValueError: return fy.ArgDTypeError(
|
|
51
|
+
f"Argument '{arg_name}' expected an integer value, but got '{raw_value}'."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if self == ArgDType.FLOAT:
|
|
55
|
+
try: return float(raw_value)
|
|
56
|
+
except ValueError: return fy.ArgDTypeError(
|
|
57
|
+
f"Argument '{arg_name}' expected a float value, but got '{raw_value}'."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import freyacli as fy
|
|
4
|
+
|
|
5
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
class ArgumentRule:
|
|
7
|
+
_INDENT_LONG_DESC = 8
|
|
8
|
+
|
|
9
|
+
# --------------------------------------------------------------------------
|
|
10
|
+
def __init__(self, raw_rule: str | None):
|
|
11
|
+
self._raw_rule = raw_rule
|
|
12
|
+
self.help_str: fy.HelpStr = fy.HelpStr()
|
|
13
|
+
|
|
14
|
+
self._raw_user_values: list[str] = [] # raw string values provided by the user
|
|
15
|
+
self._n_user_values: int = 0 # amount of values provided by the user so far
|
|
16
|
+
self._was_used: bool = False # True when the flag of this argument was used at least once.
|
|
17
|
+
|
|
18
|
+
self.name: str = "" # name that will be displayed between <> (e.g. "<value>") in the help string, and used to fetch any given values
|
|
19
|
+
self.is_positional: bool = False # True when the argument doesn't need a preceding "flag" (e.g. just "<value>" instead of "--flag <value>")
|
|
20
|
+
self.is_optional: bool = False # display with surrounding [] in the help string (e.g. "[<value>]")
|
|
21
|
+
self.flag_short: str = "" # must be 1 character long. It's always used with a single preceding dash (e.g. "-f")
|
|
22
|
+
self.flag_long: str = "" # used with a double preceding dash (e.g. "--flag")
|
|
23
|
+
self.default_value: str = "" # when a default value is present, it's placed at the beginning of the respective help string, right after the flag type (e.g. "INT (default: 0)").
|
|
24
|
+
self.arg_dtype = fy.ArgDType.NONE # placed at the beginning of the help string. TOGGLE type is used for flags with no arguments, and the respective type (e.g. STR, INT) is used for flags with arguments.
|
|
25
|
+
self.arg_count = fy.ArgCount() # contains information about the expected number of passed values and whether they are optional.
|
|
26
|
+
|
|
27
|
+
if raw_rule is None: return
|
|
28
|
+
|
|
29
|
+
self.is_optional = raw_rule.startswith('~')
|
|
30
|
+
|
|
31
|
+
(buffer,
|
|
32
|
+
self.default_value
|
|
33
|
+
) = self._parse_default_value(raw_rule)
|
|
34
|
+
|
|
35
|
+
(buffer,
|
|
36
|
+
self.name
|
|
37
|
+
) = self._parse_name(buffer)
|
|
38
|
+
|
|
39
|
+
(buffer,
|
|
40
|
+
self.is_positional,
|
|
41
|
+
self.flag_short,
|
|
42
|
+
self.flag_long
|
|
43
|
+
) = self._parse_flags(buffer)
|
|
44
|
+
|
|
45
|
+
(buffer,
|
|
46
|
+
self.arg_count
|
|
47
|
+
) = self._parse_arg_count(buffer)
|
|
48
|
+
|
|
49
|
+
self.arg_dtype = fy.ArgDType.from_str(buffer)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --------------------------------------------------------------------------
|
|
53
|
+
def register_user_value(self, value: str) -> bool:
|
|
54
|
+
"""Returns `True` when the argument is "full" (i.e. won't accept any more user values)"""
|
|
55
|
+
self._raw_user_values.append(value)
|
|
56
|
+
self._n_user_values += 1
|
|
57
|
+
self.touch()
|
|
58
|
+
return self.arg_count.is_full(self._n_user_values)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --------------------------------------------------------------------------
|
|
62
|
+
def touch(self):
|
|
63
|
+
"""
|
|
64
|
+
Marks the argument as used even if it doesn't receive any user values.
|
|
65
|
+
Useful when dealing with flags with optional keyword arguments
|
|
66
|
+
"""
|
|
67
|
+
self._was_used = True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
def parse_user_values(self) -> None | fy.ArgDTypeError |\
|
|
72
|
+
bool|str|Path|int|float |\
|
|
73
|
+
list[bool]|list[str]|list[Path]|list[int]|list[float]:
|
|
74
|
+
"""
|
|
75
|
+
Parses the raw user values according to the argument type (e.g. `INT`, `STR`, etc.) and returns the parsed values in the respective types.
|
|
76
|
+
When the argument accepts only 1 value, the value is returned directly instead of a `list`.
|
|
77
|
+
A boolean is returned for flags of `TOGGLE` type. Alternatively, `True` can be returned when a flag is passed but its optional keyword argument isn't.
|
|
78
|
+
An `ArgDTypeError` instance is returned when parsing fails due to invalid user input.
|
|
79
|
+
`None` is returned for non-used optional arguments, be it positional or non-toggle flags (i.e. those that should receive values).
|
|
80
|
+
"""
|
|
81
|
+
if self._was_used and not self.arg_count.has_enough_values(self._n_user_values):
|
|
82
|
+
s = 's' if self.arg_count.min_nvalues > 1 else ''
|
|
83
|
+
return fy.ArgDTypeError(f"Expected {self.arg_count.min_nvalues} value{s} for argument '{self.name}', but got {len(self._raw_user_values)}.")
|
|
84
|
+
|
|
85
|
+
if self.arg_dtype in (fy.ArgDType.INT, fy.ArgDType.FLOAT):
|
|
86
|
+
### for numeric types, split the raw user values, just in case they are received as a single space-separated string
|
|
87
|
+
self._raw_user_values = [
|
|
88
|
+
v for lst in self._raw_user_values for v in lst.split()
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
parsed_values = [
|
|
92
|
+
self.arg_dtype.parse_value(self.name, raw_value)
|
|
93
|
+
for raw_value in self._raw_user_values
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
if not parsed_values:
|
|
97
|
+
if self._was_used: return True # used flag has value of "True"
|
|
98
|
+
if self.is_positional: return None # unused optional positional argument has value of "None"
|
|
99
|
+
if self.arg_dtype is fy.ArgDType.TOGGLE: return False # unused toggle flag has value of "False"
|
|
100
|
+
return None # unused optional flag with arguments has value of "None"
|
|
101
|
+
|
|
102
|
+
if self.arg_count.min_nvalues == 1: return parsed_values[0]
|
|
103
|
+
return parsed_values
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# --------------------------------------------------------------------------
|
|
107
|
+
def get_usage_str_positional(self) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Usage string is only relevant for positionals.
|
|
110
|
+
Flag arguments are summarized as [options...] in the usage string.
|
|
111
|
+
A more detailed description of them is provided in the longer help string.
|
|
112
|
+
"""
|
|
113
|
+
if not self.is_positional:
|
|
114
|
+
raise fy.FreyaSyntaxError(f"Usage string can only be generated for positional arguments, but got a flag argument ('{self._raw_rule}').")
|
|
115
|
+
|
|
116
|
+
buffer = self.arg_count.get_help_str(self.name)
|
|
117
|
+
return fy.Color.blue(buffer)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# --------------------------------------------------------------------------
|
|
121
|
+
def get_help_string_description(self) -> str:
|
|
122
|
+
preffix = ""
|
|
123
|
+
if self.is_optional: preffix += "[optional] "
|
|
124
|
+
if self.arg_dtype.stores_data():
|
|
125
|
+
preffix += self.arg_dtype.name
|
|
126
|
+
if self.default_value: preffix += f" (default: {self.default_value})"
|
|
127
|
+
preffix += ". "
|
|
128
|
+
|
|
129
|
+
arg_desc = self._get_arg_description()
|
|
130
|
+
long_desc = self.help_str.wrapped_text(
|
|
131
|
+
indent = self._INDENT_LONG_DESC,
|
|
132
|
+
width = fy.WIDTH_TERMINAL - self._INDENT_LONG_DESC,
|
|
133
|
+
preffix = fy.Color.yellow(preffix)
|
|
134
|
+
)
|
|
135
|
+
return f"{arg_desc}\n{self._INDENT_LONG_DESC*' '}{long_desc}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# --------------------------------------------------------------------------
|
|
139
|
+
def _parse_default_value(self, buffer: str) -> tuple[str, str]:
|
|
140
|
+
idx = buffer.find('=')
|
|
141
|
+
if idx == -1: return buffer, ''
|
|
142
|
+
return buffer[:idx], buffer[idx+1:]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --------------------------------------------------------------------------
|
|
146
|
+
def _parse_name(self, buffer: str) -> tuple[str, str]:
|
|
147
|
+
name, buffer = self._safe_split_into_2(buffer, '[')
|
|
148
|
+
return buffer, name[int(self.is_optional):]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# --------------------------------------------------------------------------
|
|
152
|
+
def _parse_flags(self, buffer: str) -> tuple[str, bool, str, str]:
|
|
153
|
+
flags, buffer = self._safe_split_into_2(buffer, ']')
|
|
154
|
+
if not flags: return buffer, True, '', ''
|
|
155
|
+
|
|
156
|
+
flag_short, flag_long = self._safe_split_into_2(flags, ',')
|
|
157
|
+
if len(flag_short) > 1:
|
|
158
|
+
raise fy.FreyaSyntaxError(f"Short flags should be 1 character long, but got '{self.flag_short}'.")
|
|
159
|
+
|
|
160
|
+
return buffer, False, flag_short, flag_long
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# --------------------------------------------------------------------------
|
|
164
|
+
def _parse_arg_count(self, buffer: str) -> fy.ArgCount:
|
|
165
|
+
if not buffer:
|
|
166
|
+
if self.default_value:
|
|
167
|
+
raise fy.FreyaSyntaxError(f"Flags with no arguments can't have a default value ('{self._raw_rule}')")
|
|
168
|
+
if self.is_positional:
|
|
169
|
+
raise fy.FreyaSyntaxError(f"Positional arguments must specify type and number of arguments ('{self._raw_rule}')")
|
|
170
|
+
|
|
171
|
+
return "", fy.ArgCount()
|
|
172
|
+
|
|
173
|
+
min_nvalues, buffer = self._safe_split_into_2(buffer, '.')
|
|
174
|
+
arg_count = fy.ArgCount.from_str(min_nvalues)
|
|
175
|
+
if self.is_positional:
|
|
176
|
+
### having a distinction between self.is_optional (e.g. "passing -x" is optional)
|
|
177
|
+
### vs arg_count.val_optional (e.g. "passing a value for -x" is optional)
|
|
178
|
+
### is only relevant for flags, not for positionals
|
|
179
|
+
arg_count.val_optional = self.is_optional
|
|
180
|
+
|
|
181
|
+
return buffer, arg_count
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --------------------------------------------------------------------------
|
|
185
|
+
def _safe_split_into_2(self, buffer: str, char_split: str) -> tuple[str, str]:
|
|
186
|
+
splitted = buffer.split(char_split)
|
|
187
|
+
if len(splitted) == 2: return splitted
|
|
188
|
+
raise fy.FreyaSyntaxError(f"Invalid substring found inside argument rule: '{self._raw_rule}'")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# --------------------------------------------------------------------------
|
|
192
|
+
def _get_arg_description(self) -> str:
|
|
193
|
+
buffer = self.arg_count.get_help_str(self.name)
|
|
194
|
+
if self.is_positional: return fy.Color.blue(" " + buffer)
|
|
195
|
+
|
|
196
|
+
buffer = f" {buffer}" if self.arg_dtype.stores_data() else ""
|
|
197
|
+
|
|
198
|
+
if self.is_optional:
|
|
199
|
+
str_flags_0 = "["
|
|
200
|
+
str_flags_1 = "]"
|
|
201
|
+
else:
|
|
202
|
+
str_flags_0 = ""
|
|
203
|
+
str_flags_1 = ""
|
|
204
|
+
|
|
205
|
+
flag_short = f"-{self.flag_short}" if self.flag_short else ""
|
|
206
|
+
flag_long = f"--{self.flag_long}" if self.flag_long else ""
|
|
207
|
+
|
|
208
|
+
str_flags_0 += (
|
|
209
|
+
f"{flag_short}, {flag_long}" if self.flag_long else flag_short
|
|
210
|
+
) if self.flag_short else flag_long
|
|
211
|
+
|
|
212
|
+
buffer = f" {fy.Color.green(str_flags_0)}{fy.Color.blue(buffer)}"
|
|
213
|
+
if str_flags_1: buffer += fy.Color.green(str_flags_1)
|
|
214
|
+
return buffer
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import freyacli as fy
|
|
2
|
+
|
|
3
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
4
|
+
class Subcommand:
|
|
5
|
+
def __init__(self, name: str, parent: "Subcommand|None"):
|
|
6
|
+
self.name = name
|
|
7
|
+
self.parent: "Subcommand|None" = parent
|
|
8
|
+
self.children: dict[str, "Subcommand"] = {}
|
|
9
|
+
self.depth = 0 if self.is_root() else parent.depth + 1
|
|
10
|
+
self.help_str: fy.HelpStr = fy.HelpStr()
|
|
11
|
+
|
|
12
|
+
self.rules: dict[str, fy.ArgumentRule] = {}
|
|
13
|
+
self.rules_posit: list[fy.ArgumentRule] = []
|
|
14
|
+
self.rules_flags: list[fy.ArgumentRule] = []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --------------------------------------------------------------------------
|
|
18
|
+
def __repr__(self):
|
|
19
|
+
values = ','.join('' if v is None else str(v) for v in self.children.values())
|
|
20
|
+
return f"Subcommand({self.depth}:{self.name}){{{values if values else ''}}}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --------------------------------------------------------------------------
|
|
24
|
+
def __getitem__(self, name: str) -> "Subcommand":
|
|
25
|
+
return self.get_child(name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------
|
|
29
|
+
def add_child(self, name: str) -> "Subcommand":
|
|
30
|
+
self._assert_unique_child(name)
|
|
31
|
+
child = Subcommand(name, self)
|
|
32
|
+
self.children[name] = child
|
|
33
|
+
return child
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------
|
|
37
|
+
def get_child(self, name: str) -> "Subcommand":
|
|
38
|
+
if name not in self.children:
|
|
39
|
+
raise fy.FreyaSyntaxError(f"Branch '{name}' is not defined in the CLI rules.")
|
|
40
|
+
return self.children[name]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------
|
|
44
|
+
def add_rule(self, rule: fy.ArgumentRule):
|
|
45
|
+
if rule.name in self.rules:
|
|
46
|
+
raise fy.FreyaSyntaxError(f"Duplicate definition of '{rule}'.")
|
|
47
|
+
|
|
48
|
+
self.rules[rule.name] = rule
|
|
49
|
+
lst = self.rules_posit if rule.is_positional else self.rules_flags
|
|
50
|
+
lst.append(rule)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --------------------------------------------------------------------------
|
|
54
|
+
def is_root(self) -> bool: return self.parent is None
|
|
55
|
+
def is_leaf(self) -> bool: return not self.children
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --------------------------------------------------------------------------
|
|
59
|
+
def get_args_with_flag(self, flag: str, short_name: bool) -> list[fy.ArgumentRule]:
|
|
60
|
+
"""List of rules returned is ensured to contain between 0 and 1 elements."""
|
|
61
|
+
matches = list(filter(
|
|
62
|
+
lambda rule: flag == rule.flag_short if short_name else flag == rule.flag_long,
|
|
63
|
+
self.rules_flags
|
|
64
|
+
))
|
|
65
|
+
if len(matches) > 1:
|
|
66
|
+
raise fy.FreyaSyntaxError(f"Multiple rules with the same flag: '{flag}'.")
|
|
67
|
+
return matches
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
def get_path_to_root(self) -> list[str]:
|
|
72
|
+
path = []
|
|
73
|
+
node = self
|
|
74
|
+
while node:
|
|
75
|
+
if node.parent is None: break
|
|
76
|
+
path.append(node.name)
|
|
77
|
+
node = node.parent
|
|
78
|
+
return path[::-1]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------------------------
|
|
82
|
+
def str_help_long(self, py_name: str) -> str:
|
|
83
|
+
return '\n'.join((
|
|
84
|
+
self._get_usage_str(py_name),
|
|
85
|
+
self.help_str.nl_surround(),
|
|
86
|
+
self._get_str_leaf_arguments() if self.is_leaf() \
|
|
87
|
+
else self._get_str_subcommands(),
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --------------------------------------------------------------------------
|
|
92
|
+
def _get_usage_str(self, py_name: str) -> str:
|
|
93
|
+
path_to_root = [py_name] + self.get_path_to_root()
|
|
94
|
+
preffix = " " + ' '.join(path_to_root)
|
|
95
|
+
|
|
96
|
+
usage_posits = ' '.join(
|
|
97
|
+
rule.get_usage_str_positional() for rule in self.rules_posit
|
|
98
|
+
)
|
|
99
|
+
if usage_posits: usage_posits += ' '
|
|
100
|
+
|
|
101
|
+
if self.is_leaf():
|
|
102
|
+
### [NOTE] options isn't currently supported for non-leaf nodes
|
|
103
|
+
return f"{preffix} {usage_posits}{fy.Color.green('[options...]')}"
|
|
104
|
+
|
|
105
|
+
return f"{preffix} {fy.Color.magenta('COMMAND')} ..."
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# --------------------------------------------------------------------------
|
|
109
|
+
def _get_str_subcommands(self) -> str:
|
|
110
|
+
max_name_len = max(map(len, self.children.keys()))
|
|
111
|
+
width_name = len(fy.HelpStr.pad_name("", max_name_len))
|
|
112
|
+
width_desc = max(1, fy.WIDTH_TERMINAL - width_name)
|
|
113
|
+
|
|
114
|
+
rows_commands = '\n'.join((
|
|
115
|
+
fy.Color.magenta(fy.HelpStr.pad_name(name, max_name_len)) +\
|
|
116
|
+
child.help_str.wrapped_text(width_name, width_desc)
|
|
117
|
+
for name, child in self.children.items()
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
return '\n'.join((
|
|
121
|
+
fy.Color.magenta("commands:", bright = False),
|
|
122
|
+
" The following subcommands are available:",
|
|
123
|
+
"",
|
|
124
|
+
rows_commands,
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# --------------------------------------------------------------------------
|
|
129
|
+
def _get_str_leaf_arguments(self) -> str:
|
|
130
|
+
return '\n'.join((
|
|
131
|
+
self._get_str_leaf_posits(),
|
|
132
|
+
self._get_str_leaf_flags(),
|
|
133
|
+
))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --------------------------------------------------------------------------
|
|
137
|
+
def _get_str_leaf_posits(self) -> str:
|
|
138
|
+
if not self.rules_posit: return ""
|
|
139
|
+
|
|
140
|
+
rows_posits = '\n'.join((
|
|
141
|
+
rule.get_help_string_description()
|
|
142
|
+
for rule in self.rules_posit
|
|
143
|
+
))
|
|
144
|
+
return '\n'.join((
|
|
145
|
+
fy.Color.blue("positional arguments:", bright = False),
|
|
146
|
+
rows_posits, "",
|
|
147
|
+
))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# --------------------------------------------------------------------------
|
|
151
|
+
def _get_str_leaf_flags(self) -> str:
|
|
152
|
+
if not self.rules_flags: return ""
|
|
153
|
+
|
|
154
|
+
rows_flags = '\n'.join((
|
|
155
|
+
rule.get_help_string_description()
|
|
156
|
+
for rule in self.rules_flags
|
|
157
|
+
))
|
|
158
|
+
return '\n'.join((
|
|
159
|
+
fy.Color.green("options:", bright = False),
|
|
160
|
+
rows_flags, "",
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------------------
|
|
165
|
+
def _assert_unique_child(self, name: str):
|
|
166
|
+
if name in self.children:
|
|
167
|
+
raise fy.FreyaSyntaxError(f"Duplicate child branch '{name}' specified inside the same branch.")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
File without changes
|