click-extended 1.0.0__py3-none-any.whl → 1.0.1__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.
- click_extended/core/__init__.py +10 -0
- click_extended/core/decorators/__init__.py +21 -0
- click_extended/core/decorators/argument.py +227 -0
- click_extended/core/decorators/command.py +93 -0
- click_extended/core/decorators/env.py +155 -0
- click_extended/core/decorators/group.py +96 -0
- click_extended/core/decorators/option.py +347 -0
- click_extended/core/decorators/prompt.py +69 -0
- click_extended/core/decorators/selection.py +155 -0
- click_extended/core/decorators/tag.py +109 -0
- click_extended/core/nodes/__init__.py +21 -0
- click_extended/core/nodes/_root_node.py +1012 -0
- click_extended/core/nodes/argument_node.py +165 -0
- click_extended/core/nodes/child_node.py +555 -0
- click_extended/core/nodes/child_validation_node.py +100 -0
- click_extended/core/nodes/node.py +55 -0
- click_extended/core/nodes/option_node.py +205 -0
- click_extended/core/nodes/parent_node.py +220 -0
- click_extended/core/nodes/validation_node.py +124 -0
- click_extended/core/other/__init__.py +7 -0
- click_extended/core/other/_click_command.py +60 -0
- click_extended/core/other/_click_group.py +246 -0
- click_extended/core/other/_tree.py +491 -0
- click_extended/core/other/context.py +496 -0
- click_extended/decorators/__init__.py +29 -0
- click_extended/decorators/check/__init__.py +57 -0
- click_extended/decorators/check/conflicts.py +149 -0
- click_extended/decorators/check/contains.py +69 -0
- click_extended/decorators/check/dependencies.py +115 -0
- click_extended/decorators/check/divisible_by.py +48 -0
- click_extended/decorators/check/ends_with.py +85 -0
- click_extended/decorators/check/exclusive.py +75 -0
- click_extended/decorators/check/falsy.py +37 -0
- click_extended/decorators/check/is_email.py +43 -0
- click_extended/decorators/check/is_hex_color.py +41 -0
- click_extended/decorators/check/is_hostname.py +47 -0
- click_extended/decorators/check/is_ipv4.py +46 -0
- click_extended/decorators/check/is_ipv6.py +46 -0
- click_extended/decorators/check/is_json.py +40 -0
- click_extended/decorators/check/is_mac_address.py +40 -0
- click_extended/decorators/check/is_negative.py +37 -0
- click_extended/decorators/check/is_non_zero.py +37 -0
- click_extended/decorators/check/is_port.py +39 -0
- click_extended/decorators/check/is_positive.py +37 -0
- click_extended/decorators/check/is_url.py +75 -0
- click_extended/decorators/check/is_uuid.py +40 -0
- click_extended/decorators/check/length.py +68 -0
- click_extended/decorators/check/not_empty.py +49 -0
- click_extended/decorators/check/regex.py +47 -0
- click_extended/decorators/check/requires.py +190 -0
- click_extended/decorators/check/starts_with.py +87 -0
- click_extended/decorators/check/truthy.py +37 -0
- click_extended/decorators/compare/__init__.py +15 -0
- click_extended/decorators/compare/at_least.py +57 -0
- click_extended/decorators/compare/at_most.py +57 -0
- click_extended/decorators/compare/between.py +119 -0
- click_extended/decorators/compare/greater_than.py +183 -0
- click_extended/decorators/compare/less_than.py +183 -0
- click_extended/decorators/convert/__init__.py +31 -0
- click_extended/decorators/convert/convert_angle.py +94 -0
- click_extended/decorators/convert/convert_area.py +123 -0
- click_extended/decorators/convert/convert_bits.py +211 -0
- click_extended/decorators/convert/convert_distance.py +154 -0
- click_extended/decorators/convert/convert_energy.py +155 -0
- click_extended/decorators/convert/convert_power.py +128 -0
- click_extended/decorators/convert/convert_pressure.py +131 -0
- click_extended/decorators/convert/convert_speed.py +122 -0
- click_extended/decorators/convert/convert_temperature.py +89 -0
- click_extended/decorators/convert/convert_time.py +108 -0
- click_extended/decorators/convert/convert_volume.py +218 -0
- click_extended/decorators/convert/convert_weight.py +158 -0
- click_extended/decorators/load/__init__.py +13 -0
- click_extended/decorators/load/load_csv.py +117 -0
- click_extended/decorators/load/load_json.py +61 -0
- click_extended/decorators/load/load_toml.py +47 -0
- click_extended/decorators/load/load_yaml.py +72 -0
- click_extended/decorators/math/__init__.py +37 -0
- click_extended/decorators/math/absolute.py +35 -0
- click_extended/decorators/math/add.py +48 -0
- click_extended/decorators/math/ceil.py +36 -0
- click_extended/decorators/math/clamp.py +51 -0
- click_extended/decorators/math/divide.py +42 -0
- click_extended/decorators/math/floor.py +36 -0
- click_extended/decorators/math/maximum.py +39 -0
- click_extended/decorators/math/minimum.py +39 -0
- click_extended/decorators/math/modulo.py +39 -0
- click_extended/decorators/math/multiply.py +51 -0
- click_extended/decorators/math/normalize.py +76 -0
- click_extended/decorators/math/power.py +39 -0
- click_extended/decorators/math/rounded.py +39 -0
- click_extended/decorators/math/sqrt.py +39 -0
- click_extended/decorators/math/subtract.py +39 -0
- click_extended/decorators/math/to_percent.py +63 -0
- click_extended/decorators/misc/__init__.py +17 -0
- click_extended/decorators/misc/choice.py +139 -0
- click_extended/decorators/misc/confirm_if.py +147 -0
- click_extended/decorators/misc/default.py +95 -0
- click_extended/decorators/misc/deprecated.py +131 -0
- click_extended/decorators/misc/experimental.py +79 -0
- click_extended/decorators/misc/now.py +42 -0
- click_extended/decorators/random/__init__.py +21 -0
- click_extended/decorators/random/random_bool.py +49 -0
- click_extended/decorators/random/random_choice.py +63 -0
- click_extended/decorators/random/random_datetime.py +140 -0
- click_extended/decorators/random/random_float.py +62 -0
- click_extended/decorators/random/random_integer.py +56 -0
- click_extended/decorators/random/random_prime.py +196 -0
- click_extended/decorators/random/random_string.py +77 -0
- click_extended/decorators/random/random_uuid.py +119 -0
- click_extended/decorators/transform/__init__.py +71 -0
- click_extended/decorators/transform/add_prefix.py +58 -0
- click_extended/decorators/transform/add_suffix.py +58 -0
- click_extended/decorators/transform/apply.py +35 -0
- click_extended/decorators/transform/basename.py +44 -0
- click_extended/decorators/transform/dirname.py +44 -0
- click_extended/decorators/transform/expand_vars.py +36 -0
- click_extended/decorators/transform/remove_prefix.py +57 -0
- click_extended/decorators/transform/remove_suffix.py +57 -0
- click_extended/decorators/transform/replace.py +46 -0
- click_extended/decorators/transform/slugify.py +45 -0
- click_extended/decorators/transform/split.py +43 -0
- click_extended/decorators/transform/strip.py +148 -0
- click_extended/decorators/transform/to_case.py +216 -0
- click_extended/decorators/transform/to_date.py +75 -0
- click_extended/decorators/transform/to_datetime.py +83 -0
- click_extended/decorators/transform/to_path.py +274 -0
- click_extended/decorators/transform/to_time.py +77 -0
- click_extended/decorators/transform/to_timestamp.py +114 -0
- click_extended/decorators/transform/truncate.py +47 -0
- click_extended/utils/__init__.py +13 -0
- click_extended/utils/casing.py +169 -0
- click_extended/utils/checks.py +48 -0
- click_extended/utils/dispatch.py +1016 -0
- click_extended/utils/format.py +101 -0
- click_extended/utils/humanize.py +209 -0
- click_extended/utils/naming.py +238 -0
- click_extended/utils/process.py +294 -0
- click_extended/utils/selection.py +267 -0
- click_extended/utils/time.py +46 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +2 -1
- click_extended-1.0.1.dist-info/RECORD +149 -0
- click_extended-1.0.0.dist-info/RECORD +0 -10
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""`ParentNode` that represents a Click option."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-arguments
|
|
4
|
+
# pylint: disable=too-many-positional-arguments
|
|
5
|
+
# pylint: disable=too-many-locals
|
|
6
|
+
# pylint: disable=too-many-branches
|
|
7
|
+
# pylint: disable=redefined-builtin
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from builtins import type as builtins_type
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from typing import Any, Callable, ParamSpec, Type, TypeVar, cast
|
|
13
|
+
|
|
14
|
+
from click_extended.core.nodes.option_node import OptionNode
|
|
15
|
+
from click_extended.core.other.context import Context
|
|
16
|
+
from click_extended.utils.humanize import humanize_type
|
|
17
|
+
from click_extended.utils.naming import (
|
|
18
|
+
is_long_flag,
|
|
19
|
+
is_short_flag,
|
|
20
|
+
is_valid_name,
|
|
21
|
+
validate_name,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
P = ParamSpec("P")
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
SUPPORTED_TYPES = (str, int, float, bool)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Option(OptionNode):
|
|
31
|
+
"""`OptionNode` that represents a Click option."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
name: str,
|
|
36
|
+
*flags: str,
|
|
37
|
+
param: str | None = None,
|
|
38
|
+
is_flag: bool = False,
|
|
39
|
+
type: Any = None,
|
|
40
|
+
nargs: int = 1,
|
|
41
|
+
multiple: bool = False,
|
|
42
|
+
help: str | None = None,
|
|
43
|
+
required: bool = False,
|
|
44
|
+
default: Any = None,
|
|
45
|
+
tags: str | list[str] | None = None,
|
|
46
|
+
**kwargs: Any,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize a new `Option` instance.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
name (str):
|
|
53
|
+
The option name (parameter name) in snake_case,
|
|
54
|
+
SCREAMING_SNAKE_CASE, or kebab-case. Examples: \"config_file\",
|
|
55
|
+
\"CONFIG_FILE\", \"config-file\"
|
|
56
|
+
*flags (str):
|
|
57
|
+
Optional flags for the option. Can include any number of short
|
|
58
|
+
flags (e.g., \"-p\", \"-P\") and long flags (e.g., \"--port\",
|
|
59
|
+
\"--p\"). If no long flags provided, auto-generates
|
|
60
|
+
"--kebab-case(name)".
|
|
61
|
+
param (str, optional):
|
|
62
|
+
Custom parameter name for the function.
|
|
63
|
+
If not provided, uses the name directly.
|
|
64
|
+
is_flag (bool):
|
|
65
|
+
Whether this is a boolean flag (no value needed).
|
|
66
|
+
Defaults to `False`.
|
|
67
|
+
type (Any, optional):
|
|
68
|
+
The type to convert the value to (int, str, float, etc.).
|
|
69
|
+
nargs (int):
|
|
70
|
+
Number of arguments each occurrence accepts. Defaults to `1`.
|
|
71
|
+
multiple (bool):
|
|
72
|
+
Whether the option can be provided multiple times.
|
|
73
|
+
Defaults to `False`.
|
|
74
|
+
help (str, optional):
|
|
75
|
+
Help text for this option.
|
|
76
|
+
required (bool):
|
|
77
|
+
Whether this option is required. Defaults to `False`.
|
|
78
|
+
default (Any):
|
|
79
|
+
Default value if not provided. Defaults to None.
|
|
80
|
+
tags (str | list[str], optional):
|
|
81
|
+
Tag(s) to associate with this option for grouping.
|
|
82
|
+
**kwargs (Any):
|
|
83
|
+
Additional Click option parameters.
|
|
84
|
+
"""
|
|
85
|
+
if name.startswith("--"):
|
|
86
|
+
if is_long_flag(name):
|
|
87
|
+
derived_name = name[2:]
|
|
88
|
+
if not is_valid_name(derived_name):
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Invalid option name '{name}'. When using a long "
|
|
91
|
+
"flag as name, it must be snake_case after removing "
|
|
92
|
+
f"'--'. Use '{name.replace('-', '_')}' or provide an "
|
|
93
|
+
"explicit snake_case name parameter."
|
|
94
|
+
)
|
|
95
|
+
if not flags or not any(f.startswith("--") for f in flags):
|
|
96
|
+
flags = (name,) + flags
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Invalid option name '{name}'. "
|
|
100
|
+
f"Must be snake_case (e.g., my_option, config_file)"
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
validate_name(name, "option name")
|
|
104
|
+
derived_name = name
|
|
105
|
+
|
|
106
|
+
short_flags_list: list[str] = []
|
|
107
|
+
long_flags_list: list[str] = []
|
|
108
|
+
|
|
109
|
+
for flag in flags:
|
|
110
|
+
if not flag.startswith("-"):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Invalid flag '{flag}'. Flags must start with '-' or '--'."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if flag.startswith("--"):
|
|
116
|
+
if not is_long_flag(flag):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Invalid long flag '{flag}'. "
|
|
119
|
+
"Must be format: --word "
|
|
120
|
+
"(lowercase, hyphens allowed, e.g., --port, "
|
|
121
|
+
"--config-file)"
|
|
122
|
+
)
|
|
123
|
+
long_flags_list.append(flag)
|
|
124
|
+
else:
|
|
125
|
+
if not is_short_flag(flag):
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Invalid short flag '{flag}'. Must be format: -X "
|
|
128
|
+
f"(single letter, e.g., -p, -v, -h)"
|
|
129
|
+
)
|
|
130
|
+
short_flags_list.append(flag)
|
|
131
|
+
|
|
132
|
+
param_name = param if param is not None else derived_name
|
|
133
|
+
|
|
134
|
+
validate_name(param_name, "parameter name")
|
|
135
|
+
|
|
136
|
+
if is_flag and type is not None and type != bool:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Cannot specify both is_flag=True and "
|
|
139
|
+
f"type={type.__name__ if hasattr(type, '__name__') else type}. "
|
|
140
|
+
f"Flags are always boolean."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if is_flag and default is None:
|
|
144
|
+
default = False
|
|
145
|
+
|
|
146
|
+
if type is None:
|
|
147
|
+
if is_flag:
|
|
148
|
+
type = bool
|
|
149
|
+
elif default is not None and not (multiple and default == ()):
|
|
150
|
+
type = cast(Type[Any], builtins_type(default)) # type: ignore
|
|
151
|
+
else:
|
|
152
|
+
type = str
|
|
153
|
+
|
|
154
|
+
if type not in SUPPORTED_TYPES:
|
|
155
|
+
types = humanize_type(
|
|
156
|
+
type.__name__ if hasattr(type, "__name__") else type
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Option '{derived_name}' has unsupported type '{types}'. "
|
|
161
|
+
"Only basic primitives are supported: str, int, float, bool. "
|
|
162
|
+
"For complex types, use child decorators (e.g., @to_path, "
|
|
163
|
+
"@to_datetime, ...)."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if multiple and default is None:
|
|
167
|
+
default = ()
|
|
168
|
+
|
|
169
|
+
super().__init__(
|
|
170
|
+
name=derived_name,
|
|
171
|
+
param=param_name,
|
|
172
|
+
short_flags=short_flags_list,
|
|
173
|
+
long_flags=long_flags_list,
|
|
174
|
+
is_flag=is_flag,
|
|
175
|
+
type=type,
|
|
176
|
+
nargs=nargs,
|
|
177
|
+
multiple=multiple,
|
|
178
|
+
help=help,
|
|
179
|
+
required=required,
|
|
180
|
+
default=default,
|
|
181
|
+
tags=tags,
|
|
182
|
+
)
|
|
183
|
+
self.extra_kwargs = kwargs
|
|
184
|
+
|
|
185
|
+
def get_display_name(self) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Get a formatted display name for error messages.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
str:
|
|
191
|
+
The first long flag if available, otherwise the first flag.
|
|
192
|
+
"""
|
|
193
|
+
if self.long_flags:
|
|
194
|
+
return self.long_flags[0]
|
|
195
|
+
if self.short_flags:
|
|
196
|
+
return self.short_flags[0]
|
|
197
|
+
return self.name
|
|
198
|
+
|
|
199
|
+
def load(
|
|
200
|
+
self,
|
|
201
|
+
value: str | int | float | bool | None,
|
|
202
|
+
context: Context,
|
|
203
|
+
*args: Any,
|
|
204
|
+
**kwargs: Any,
|
|
205
|
+
) -> Any:
|
|
206
|
+
"""
|
|
207
|
+
Load and return the CLI option value.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
value (str | int | float | bool | None):
|
|
211
|
+
The parsed CLI option value from Click.
|
|
212
|
+
context (Context):
|
|
213
|
+
The current context instance.
|
|
214
|
+
*args (Any):
|
|
215
|
+
Optional positional arguments.
|
|
216
|
+
**kwargs (Any):
|
|
217
|
+
Optional keyword arguments.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Any:
|
|
221
|
+
The option value to inject into the function.
|
|
222
|
+
"""
|
|
223
|
+
return value
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def option(
|
|
227
|
+
name: str,
|
|
228
|
+
*flags: str,
|
|
229
|
+
param: str | None = None,
|
|
230
|
+
is_flag: bool = False,
|
|
231
|
+
type: Type[str | int | float | bool] | None = None,
|
|
232
|
+
nargs: int = 1,
|
|
233
|
+
multiple: bool = False,
|
|
234
|
+
help: str | None = None,
|
|
235
|
+
required: bool = False,
|
|
236
|
+
default: Any = None,
|
|
237
|
+
tags: str | list[str] | None = None,
|
|
238
|
+
**kwargs: Any,
|
|
239
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
240
|
+
"""
|
|
241
|
+
A `ParentNode` decorator to create a Click option with value injection.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
name (str):
|
|
245
|
+
The option name (parameter name) in snake_case.
|
|
246
|
+
Examples: "verbose", "config_file"
|
|
247
|
+
*flags (str):
|
|
248
|
+
Optional flags for the option. Can include any number of short flags
|
|
249
|
+
(e.g., "-v", "-V") and long flags (e.g., "--verbose", "--verb").
|
|
250
|
+
If no long flags provided, auto-generates "--kebab-case(name)".
|
|
251
|
+
Examples:
|
|
252
|
+
@option("verbose", "-v")
|
|
253
|
+
@option("config", "-c", "--cfg", "--config")
|
|
254
|
+
@option("verbose", "-v", "-V", "--verbose", "--verb")
|
|
255
|
+
param (str, optional):
|
|
256
|
+
Custom parameter name for the function.
|
|
257
|
+
If not provided, uses the name directly.
|
|
258
|
+
is_flag (bool):
|
|
259
|
+
Whether this is a boolean flag (no value needed).
|
|
260
|
+
Defaults to `False`.
|
|
261
|
+
type (Type[str | int | float | bool] | None, optional):
|
|
262
|
+
The type to convert the value to.
|
|
263
|
+
nargs (int):
|
|
264
|
+
Number of arguments each occurrence accepts. Defaults to `1`.
|
|
265
|
+
multiple (bool):
|
|
266
|
+
Whether the option can be provided multiple times.
|
|
267
|
+
Defaults to `False`.
|
|
268
|
+
help (str, optional):
|
|
269
|
+
Help text for this option.
|
|
270
|
+
required (bool):
|
|
271
|
+
Whether this option is required. Defaults to `False`.
|
|
272
|
+
default (Any):
|
|
273
|
+
Default value if not provided. Defaults to None.
|
|
274
|
+
tags (str | list[str], optional):
|
|
275
|
+
Tag(s) to associate with this option for grouping.
|
|
276
|
+
**kwargs (Any):
|
|
277
|
+
Additional Click option parameters.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Callable:
|
|
281
|
+
A decorator function that registers the option parent node.
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
# Simple: name only (auto-generates --verbose)
|
|
287
|
+
@option("verbose", is_flag=True)
|
|
288
|
+
def my_func(verbose):
|
|
289
|
+
print(f"Verbose: {verbose}")
|
|
290
|
+
|
|
291
|
+
# Name with short flag
|
|
292
|
+
@option("port", "-p", type=int, default=8080)
|
|
293
|
+
def my_func(port):
|
|
294
|
+
print(f"Port: {port}")
|
|
295
|
+
|
|
296
|
+
# Multiple short and long flags
|
|
297
|
+
@option("verbose", "-v", "-V", "--verb", is_flag=True)
|
|
298
|
+
def my_func(verbose): # Accepts: -v, -V, --verbose, --verb
|
|
299
|
+
print("Verbose mode")
|
|
300
|
+
|
|
301
|
+
# Custom parameter name
|
|
302
|
+
@option("configuration_file", "-c", param="cfg")
|
|
303
|
+
def my_func(cfg): # param: cfg, CLI: -c, --configuration-file
|
|
304
|
+
print(f"Config: {cfg}")
|
|
305
|
+
```
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
309
|
+
"""The actual decorator that wraps the function."""
|
|
310
|
+
from click_extended.core.other._tree import Tree
|
|
311
|
+
|
|
312
|
+
instance = Option(
|
|
313
|
+
name,
|
|
314
|
+
*flags,
|
|
315
|
+
param=param,
|
|
316
|
+
is_flag=is_flag,
|
|
317
|
+
type=type,
|
|
318
|
+
nargs=nargs,
|
|
319
|
+
multiple=multiple,
|
|
320
|
+
help=help,
|
|
321
|
+
required=required,
|
|
322
|
+
default=default,
|
|
323
|
+
tags=tags,
|
|
324
|
+
**kwargs,
|
|
325
|
+
)
|
|
326
|
+
Tree.queue_parent(instance)
|
|
327
|
+
|
|
328
|
+
if asyncio.iscoroutinefunction(func):
|
|
329
|
+
|
|
330
|
+
@wraps(func)
|
|
331
|
+
async def async_wrapper(
|
|
332
|
+
*call_args: P.args, **call_kwargs: P.kwargs
|
|
333
|
+
) -> T:
|
|
334
|
+
"""Async wrapper that preserves the original function."""
|
|
335
|
+
result = await func(*call_args, **call_kwargs)
|
|
336
|
+
return cast(T, result)
|
|
337
|
+
|
|
338
|
+
return cast(Callable[P, T], async_wrapper)
|
|
339
|
+
|
|
340
|
+
@wraps(func)
|
|
341
|
+
def wrapper(*call_args: P.args, **call_kwargs: P.kwargs) -> T:
|
|
342
|
+
"""Wrapper that preserves the original function."""
|
|
343
|
+
return func(*call_args, **call_kwargs)
|
|
344
|
+
|
|
345
|
+
return wrapper
|
|
346
|
+
|
|
347
|
+
return decorator
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Parent decorator that prompts the user for a value."""
|
|
2
|
+
|
|
3
|
+
from getpass import getpass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
7
|
+
from click_extended.core.other.context import Context
|
|
8
|
+
from click_extended.types import Decorator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Prompt(ParentNode):
|
|
12
|
+
"""Parent decorator that prompts the user for a value."""
|
|
13
|
+
|
|
14
|
+
def get_input(self, text: str, hide: bool) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Get input from the user.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
text (str):
|
|
20
|
+
The prompt.
|
|
21
|
+
hide (bool):
|
|
22
|
+
Whether to hide the user input.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
str:
|
|
26
|
+
The value provided from the user.
|
|
27
|
+
"""
|
|
28
|
+
if hide:
|
|
29
|
+
return getpass(text)
|
|
30
|
+
return input(text)
|
|
31
|
+
|
|
32
|
+
def load(
|
|
33
|
+
self,
|
|
34
|
+
context: Context,
|
|
35
|
+
*args: Any,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
) -> str:
|
|
38
|
+
while True:
|
|
39
|
+
answer = self.get_input(kwargs["text"], kwargs["hide"])
|
|
40
|
+
if answer or kwargs["allow_empty"]:
|
|
41
|
+
return answer
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def prompt(
|
|
45
|
+
name: str, text: str = "", hide: bool = False, allow_empty: bool = False
|
|
46
|
+
) -> Decorator:
|
|
47
|
+
"""
|
|
48
|
+
A `ParentNode` decorator to prompt the user for input.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name (str):
|
|
52
|
+
The name of the parent node.
|
|
53
|
+
text (str):
|
|
54
|
+
The text to show.
|
|
55
|
+
hide (bool):
|
|
56
|
+
Whether to hide the input, defaults to `False`.
|
|
57
|
+
allow_empty (bool):
|
|
58
|
+
Whether to allow the input to be empty, defaults to `False`.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Decorator:
|
|
62
|
+
The decorator function.
|
|
63
|
+
"""
|
|
64
|
+
return Prompt.as_decorator(
|
|
65
|
+
name=name,
|
|
66
|
+
text=text,
|
|
67
|
+
hide=hide,
|
|
68
|
+
allow_empty=allow_empty,
|
|
69
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Choose one or more options from a list of selections."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-arguments
|
|
4
|
+
# pylint: disable=too-many-positional-arguments
|
|
5
|
+
# pylint: disable=redefined-builtin
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
10
|
+
from click_extended.core.other.context import Context
|
|
11
|
+
from click_extended.types import Decorator
|
|
12
|
+
from click_extended.utils.selection import selection as fn
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Selection(ParentNode):
|
|
16
|
+
"""Choose one or more options from a list of selections."""
|
|
17
|
+
|
|
18
|
+
def load(
|
|
19
|
+
self, context: Context, *args: Any, **kwargs: Any
|
|
20
|
+
) -> str | list[str]:
|
|
21
|
+
"""
|
|
22
|
+
Load the selection value by prompting the user.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
context (Context):
|
|
26
|
+
The current context.
|
|
27
|
+
*args (Any):
|
|
28
|
+
Additional positional arguments.
|
|
29
|
+
**kwargs (Any):
|
|
30
|
+
Additional keyword arguments
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str|list[str]:
|
|
34
|
+
Selected value(s). `str` for single mode, `list[str]`
|
|
35
|
+
for multiple.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError:
|
|
39
|
+
If selections is empty or invalid.
|
|
40
|
+
"""
|
|
41
|
+
return fn(
|
|
42
|
+
selections=kwargs.get("selections", []),
|
|
43
|
+
prompt=kwargs.get("prompt", "Select an option"),
|
|
44
|
+
multiple=kwargs.get("multiple", False),
|
|
45
|
+
default=kwargs.get("default"),
|
|
46
|
+
min_selections=kwargs.get("min_selections", 0),
|
|
47
|
+
max_selections=kwargs.get("max_selections"),
|
|
48
|
+
cursor_style=kwargs.get("cursor_style", ">"),
|
|
49
|
+
checkbox_style=kwargs.get("checkbox_style", ("◯", "◉")),
|
|
50
|
+
show_count=kwargs.get("show_count", False),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def selection(
|
|
55
|
+
name: str,
|
|
56
|
+
selections: list[str | tuple[str, str]],
|
|
57
|
+
multiple: bool = False,
|
|
58
|
+
default: str | list[str] | None = None,
|
|
59
|
+
prompt: str = "Select an option:",
|
|
60
|
+
min_selections: int = 0,
|
|
61
|
+
max_selections: int | None = None,
|
|
62
|
+
cursor_style: str = ">",
|
|
63
|
+
checkbox_style: tuple[str, str] = ("◯", "◉"),
|
|
64
|
+
show_count: bool = False,
|
|
65
|
+
param: str | None = None,
|
|
66
|
+
help: str | None = None,
|
|
67
|
+
required: bool = False,
|
|
68
|
+
tags: str | list[str] | None = None,
|
|
69
|
+
) -> Decorator:
|
|
70
|
+
"""
|
|
71
|
+
A `ParentNode` decorator for an interactive selection prompt
|
|
72
|
+
with arrow key navigation.
|
|
73
|
+
|
|
74
|
+
Creates an interactive terminal prompt that allows users to select one or
|
|
75
|
+
more options using arrow keys (or j/k vim-style keys). The list wraps around
|
|
76
|
+
(carousel behavior) when scrolling past the first or last item.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
name (str):
|
|
80
|
+
The name of the parameter for injection.
|
|
81
|
+
selections (list[str | tuple[str, str]]):
|
|
82
|
+
List of options. Each item can be:
|
|
83
|
+
- str: Used as both display text and value
|
|
84
|
+
- tuple[str, str]: (display_text, value)
|
|
85
|
+
multiple (bool):
|
|
86
|
+
If `True`, allows multiple selections with checkboxes.
|
|
87
|
+
If `False`, allows single selection only. Defaults to `False`.
|
|
88
|
+
default (str | list[str] | None):
|
|
89
|
+
Default selection(s). Should be a string for single mode,
|
|
90
|
+
or list of strings for multiple mode. Defaults to `None`.
|
|
91
|
+
prompt (str):
|
|
92
|
+
Text to display above the selection list.
|
|
93
|
+
Defaults to "Select an option:".
|
|
94
|
+
min_selections (int):
|
|
95
|
+
Minimum number of selections required (multiple mode only).
|
|
96
|
+
Defaults to `0`. User cannot confirm until minimum is met.
|
|
97
|
+
max_selections (int | None):
|
|
98
|
+
Maximum number of selections allowed (multiple mode only).
|
|
99
|
+
Defaults to `None` (unlimited). Prevents selecting more than max.
|
|
100
|
+
cursor_style (str):
|
|
101
|
+
The cursor indicator string. Defaults to ">".
|
|
102
|
+
Examples: ">", "→", "▶", "•"
|
|
103
|
+
checkbox_style (tuple[str, str]):
|
|
104
|
+
Tuple of (unselected, selected) checkbox indicators.
|
|
105
|
+
Defaults to ("◯", "◉").
|
|
106
|
+
Examples: ("☐", "☑"), ("○", "●"), ("[ ]", "[x]")
|
|
107
|
+
show_count (bool):
|
|
108
|
+
Whether to show selection count in the prompt.
|
|
109
|
+
Defaults to `False`. Shows "(X/Y selected)" when enabled.
|
|
110
|
+
param (str | None):
|
|
111
|
+
The parameter name to inject into the function.
|
|
112
|
+
If not provided, uses name.
|
|
113
|
+
help (str | None):
|
|
114
|
+
Help text for this parameter.
|
|
115
|
+
required (bool):
|
|
116
|
+
Whether this parameter is required. Defaults to `False`.
|
|
117
|
+
tags (str | list[str] | None):
|
|
118
|
+
Tag(s) to associate with this parameter for grouping.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Decorator:
|
|
122
|
+
The decorator function.
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
>>> @command()
|
|
126
|
+
>>> @selection("framework", ["React", "Vue", "Angular"])
|
|
127
|
+
>>> def setup(framework: str):
|
|
128
|
+
... print(f"Setting up {framework}...")
|
|
129
|
+
|
|
130
|
+
>>> @command()
|
|
131
|
+
>>> @selection(
|
|
132
|
+
... "features",
|
|
133
|
+
... [("TypeScript", "ts"), ("ESLint", "eslint")],
|
|
134
|
+
... multiple=True,
|
|
135
|
+
... default=["eslint"]
|
|
136
|
+
... )
|
|
137
|
+
>>> def configure(features: list[str]):
|
|
138
|
+
... print(f"Enabled: {features}")
|
|
139
|
+
"""
|
|
140
|
+
return Selection.as_decorator(
|
|
141
|
+
name=name,
|
|
142
|
+
param=param,
|
|
143
|
+
help=help,
|
|
144
|
+
required=required,
|
|
145
|
+
default=default,
|
|
146
|
+
tags=tags,
|
|
147
|
+
selections=selections,
|
|
148
|
+
multiple=multiple,
|
|
149
|
+
prompt=prompt,
|
|
150
|
+
min_selections=min_selections,
|
|
151
|
+
max_selections=max_selections,
|
|
152
|
+
cursor_style=cursor_style,
|
|
153
|
+
checkbox_style=checkbox_style,
|
|
154
|
+
show_count=show_count,
|
|
155
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Tag implementation for the `click_extended` library."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable, ParamSpec, TypeVar
|
|
4
|
+
|
|
5
|
+
from click_extended.core.nodes.node import Node
|
|
6
|
+
from click_extended.core.other._tree import Tree
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
10
|
+
|
|
11
|
+
P = ParamSpec("P")
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Tag(Node):
|
|
16
|
+
"""Tag class for grouping multiple ParentNodes together."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str):
|
|
19
|
+
"""
|
|
20
|
+
Initialize a new `Tag` instance.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
name (str):
|
|
24
|
+
The name of the tag for grouping ParentNodes.
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(name=name, children={})
|
|
27
|
+
self.parent_nodes: list["ParentNode"] = []
|
|
28
|
+
|
|
29
|
+
def get_provided_values(self) -> list[str]:
|
|
30
|
+
"""
|
|
31
|
+
Get the names of all ParentNodes in this tag
|
|
32
|
+
that were provided by the user.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
list[str]:
|
|
36
|
+
List of parameter names that were provided.
|
|
37
|
+
"""
|
|
38
|
+
return [node.name for node in self.parent_nodes if node.was_provided]
|
|
39
|
+
|
|
40
|
+
def get_value(self) -> dict[str, Any]:
|
|
41
|
+
"""
|
|
42
|
+
Get a dictionary of values from all parent nodes.
|
|
43
|
+
|
|
44
|
+
Returns a dictionary mapping parameter names to their values from all
|
|
45
|
+
parent nodes that reference this tag. Each key is the parent
|
|
46
|
+
node's name, and the value is `None` if not provided.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
dict[str, Any]:
|
|
50
|
+
Dictionary mapping parameter names to values.
|
|
51
|
+
"""
|
|
52
|
+
return {
|
|
53
|
+
parent_node.name: parent_node.get_value()
|
|
54
|
+
for parent_node in self.parent_nodes
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def as_decorator(
|
|
59
|
+
cls, name: str
|
|
60
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
61
|
+
"""
|
|
62
|
+
Return a decorator representation of the tag.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name (str):
|
|
66
|
+
The name of the tag.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Callable:
|
|
70
|
+
A decorator function that registers the tag.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
74
|
+
"""The actual decorator that registers the tag."""
|
|
75
|
+
instance = cls(name=name)
|
|
76
|
+
Tree.queue_tag(instance)
|
|
77
|
+
return func
|
|
78
|
+
|
|
79
|
+
return decorator
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def tag(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
83
|
+
"""
|
|
84
|
+
A special `Tag` decorator to create a tag for grouping parent nodes.
|
|
85
|
+
|
|
86
|
+
Tags allow you to apply child nodes to multiple parent nodes at once
|
|
87
|
+
and perform cross-node validation.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
name (str):
|
|
91
|
+
The name of the tag.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Callable:
|
|
95
|
+
A decorator function that registers the tag.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@command()
|
|
101
|
+
@option("--api-key", tags="api-config")
|
|
102
|
+
@option("--api-url", tags="api-config")
|
|
103
|
+
@tag("api-config")
|
|
104
|
+
@at_least(1)
|
|
105
|
+
def my_function(api_key: str, api_url: str):
|
|
106
|
+
print(f"API Key: {api_key}, URL: {api_url}")
|
|
107
|
+
```
|
|
108
|
+
"""
|
|
109
|
+
return Tag.as_decorator(name=name)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Initialization file for the `click_extended.core.nodes` module."""
|
|
2
|
+
|
|
3
|
+
from click_extended.core.nodes._root_node import RootNode
|
|
4
|
+
from click_extended.core.nodes.argument_node import ArgumentNode
|
|
5
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
6
|
+
from click_extended.core.nodes.child_validation_node import ChildValidationNode
|
|
7
|
+
from click_extended.core.nodes.node import Node
|
|
8
|
+
from click_extended.core.nodes.option_node import OptionNode
|
|
9
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
10
|
+
from click_extended.core.nodes.validation_node import ValidationNode
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"RootNode",
|
|
14
|
+
"ArgumentNode",
|
|
15
|
+
"ChildNode",
|
|
16
|
+
"ChildValidationNode",
|
|
17
|
+
"Node",
|
|
18
|
+
"OptionNode",
|
|
19
|
+
"ParentNode",
|
|
20
|
+
"ValidationNode",
|
|
21
|
+
]
|