click-extended 0.4.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/__init__.py +10 -6
- click_extended/classes.py +12 -8
- 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/types.py +1 -1
- 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-0.4.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +100 -29
- click_extended-1.0.1.dist-info/RECORD +149 -0
- click_extended-0.4.0.dist-info/RECORD +0 -10
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Child decorator to validate a value is one of the allowed choices."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
6
|
+
from click_extended.core.other.context import Context
|
|
7
|
+
from click_extended.types import Decorator
|
|
8
|
+
from click_extended.utils.humanize import humanize_iterable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Choice(ChildNode):
|
|
12
|
+
"""Child decorator to validate a value is one of the allowed choices."""
|
|
13
|
+
|
|
14
|
+
def handle_str(
|
|
15
|
+
self, value: str, context: Context, *args: Any, **kwargs: Any
|
|
16
|
+
) -> str:
|
|
17
|
+
values: tuple[str | int | float, ...] = kwargs["values"]
|
|
18
|
+
case_sensitive: bool = kwargs.get("case_sensitive", True)
|
|
19
|
+
|
|
20
|
+
if case_sensitive:
|
|
21
|
+
if value not in values:
|
|
22
|
+
choices_str = humanize_iterable(
|
|
23
|
+
[str(v) for v in values],
|
|
24
|
+
sep="or",
|
|
25
|
+
wrap="'",
|
|
26
|
+
prefix_plural="one of",
|
|
27
|
+
prefix_singular="",
|
|
28
|
+
)
|
|
29
|
+
raise ValueError(f"Value must be {choices_str}, got '{value}'.")
|
|
30
|
+
else:
|
|
31
|
+
value_lower = value.lower()
|
|
32
|
+
values_lower = [
|
|
33
|
+
v.lower() if isinstance(v, str) else v for v in values
|
|
34
|
+
]
|
|
35
|
+
if value_lower not in values_lower:
|
|
36
|
+
choices_str = humanize_iterable(
|
|
37
|
+
[str(v) for v in values],
|
|
38
|
+
sep="or",
|
|
39
|
+
wrap="'",
|
|
40
|
+
prefix_plural="one of",
|
|
41
|
+
prefix_singular="",
|
|
42
|
+
)
|
|
43
|
+
raise ValueError(f"Value must be {choices_str}, got '{value}'.")
|
|
44
|
+
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
def handle_int(
|
|
48
|
+
self, value: int, context: Context, *args: Any, **kwargs: Any
|
|
49
|
+
) -> int:
|
|
50
|
+
values: tuple[str | int | float, ...] = kwargs["values"]
|
|
51
|
+
|
|
52
|
+
if value not in values:
|
|
53
|
+
choices_str = humanize_iterable(
|
|
54
|
+
[str(v) for v in values],
|
|
55
|
+
sep="or",
|
|
56
|
+
wrap="'",
|
|
57
|
+
prefix_plural="one of",
|
|
58
|
+
prefix_singular="",
|
|
59
|
+
)
|
|
60
|
+
raise ValueError(f"Value must be {choices_str}, got '{value}'.")
|
|
61
|
+
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
def handle_float(
|
|
65
|
+
self, value: float, context: Context, *args: Any, **kwargs: Any
|
|
66
|
+
) -> float:
|
|
67
|
+
values: tuple[str | int | float, ...] = kwargs["values"]
|
|
68
|
+
|
|
69
|
+
if value not in values:
|
|
70
|
+
choices_str = humanize_iterable(
|
|
71
|
+
[str(v) for v in values],
|
|
72
|
+
sep="or",
|
|
73
|
+
wrap="'",
|
|
74
|
+
prefix_plural="one of",
|
|
75
|
+
prefix_singular="",
|
|
76
|
+
)
|
|
77
|
+
raise ValueError(f"Value must be {choices_str}, got '{value}'.")
|
|
78
|
+
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def choice(
|
|
83
|
+
*values: str | int | float, case_sensitive: bool = True
|
|
84
|
+
) -> Decorator:
|
|
85
|
+
"""
|
|
86
|
+
Validate that a value is one of the allowed choices.
|
|
87
|
+
|
|
88
|
+
Type: `ChildNode`
|
|
89
|
+
|
|
90
|
+
Supports: `str`, `int`, `float`
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
*values (str | int | float):
|
|
94
|
+
The allowed values to choose from. Must be strings, integers,
|
|
95
|
+
or floats.
|
|
96
|
+
case_sensitive (bool, optional):
|
|
97
|
+
Whether the comparison should be case-sensitive for strings.
|
|
98
|
+
Defaults to `True`.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Decorator:
|
|
102
|
+
The decorator function.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError:
|
|
106
|
+
If no values are provided.
|
|
107
|
+
TypeError:
|
|
108
|
+
If any value is not a string, integer, or float.
|
|
109
|
+
ValueError:
|
|
110
|
+
If the value is not one of the allowed choices.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
```python
|
|
114
|
+
@command()
|
|
115
|
+
@option("color")
|
|
116
|
+
@choice("red", "green", "blue")
|
|
117
|
+
def cmd(color: str) -> None:
|
|
118
|
+
click.echo(f"Color: {color}")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
@command()
|
|
123
|
+
@option("level")
|
|
124
|
+
@choice("DEBUG", "INFO", "WARNING", "ERROR", case_sensitive=False)
|
|
125
|
+
def cmd(level: str) -> None:
|
|
126
|
+
click.echo(f"Log level: {level}")
|
|
127
|
+
```
|
|
128
|
+
"""
|
|
129
|
+
if not values:
|
|
130
|
+
raise ValueError("At least one choice must be provided.")
|
|
131
|
+
|
|
132
|
+
for value in values:
|
|
133
|
+
if not isinstance(value, (str, int, float)):
|
|
134
|
+
raise TypeError(
|
|
135
|
+
f"All choice values must be str, int, or float, "
|
|
136
|
+
f"got {type(value).__name__}."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return Choice.as_decorator(values=values, case_sensitive=case_sensitive)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Child decorator to conditionally prompt for user confirmation."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-locals
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
12
|
+
from click_extended.core.other.context import Context
|
|
13
|
+
from click_extended.types import Decorator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfirmIf(ChildNode):
|
|
17
|
+
"""Child decorator to conditionally prompt for user confirmation."""
|
|
18
|
+
|
|
19
|
+
def handle_all(
|
|
20
|
+
self, value: Any, context: Context, *args: Any, **kwargs: Any
|
|
21
|
+
) -> Any:
|
|
22
|
+
fn: Callable[[Any], bool] | Callable[[Any, Context], bool] = kwargs[
|
|
23
|
+
"fn"
|
|
24
|
+
]
|
|
25
|
+
prompt_text: str = kwargs["prompt"]
|
|
26
|
+
truthy: list[str] = kwargs["truthy"]
|
|
27
|
+
|
|
28
|
+
sig = inspect.signature(fn)
|
|
29
|
+
accepts_context = len(sig.parameters) >= 2
|
|
30
|
+
|
|
31
|
+
if accepts_context:
|
|
32
|
+
should_confirm = fn(value, context) # type: ignore
|
|
33
|
+
else:
|
|
34
|
+
should_confirm = fn(value) # type: ignore
|
|
35
|
+
|
|
36
|
+
if not should_confirm:
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
if os.getenv("CLICK_EXTENDED_TESTING") == "1":
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
formatted_prompt = prompt_text.format(value=value).strip()
|
|
43
|
+
formatted_prompt = formatted_prompt.rstrip(":")
|
|
44
|
+
|
|
45
|
+
first_truthy = truthy[0] if truthy else "y"
|
|
46
|
+
formatted_prompt = f"{formatted_prompt} ({first_truthy}/n)"
|
|
47
|
+
|
|
48
|
+
response = click.prompt(formatted_prompt, type=str).strip()
|
|
49
|
+
|
|
50
|
+
response_lower = response.lower()
|
|
51
|
+
truthy_lower = [t.lower() for t in truthy]
|
|
52
|
+
|
|
53
|
+
if response_lower in truthy_lower:
|
|
54
|
+
return value
|
|
55
|
+
raise click.Abort()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def confirm_if(
|
|
59
|
+
prompt: str,
|
|
60
|
+
fn: Callable[[Any], bool] | Callable[[Any, Context], bool],
|
|
61
|
+
truthy: list[str] | None = None,
|
|
62
|
+
) -> Decorator:
|
|
63
|
+
"""
|
|
64
|
+
Conditionally prompt for user confirmation based on a predicate function.
|
|
65
|
+
|
|
66
|
+
Type: `ChildNode`
|
|
67
|
+
|
|
68
|
+
Supports: `Any`
|
|
69
|
+
|
|
70
|
+
The predicate function can accept either just the value `fn(value)` or
|
|
71
|
+
both the value and context `fn(value, context)`. The decorator automatically
|
|
72
|
+
detects the function signature and calls it appropriately.
|
|
73
|
+
|
|
74
|
+
When the predicate returns `True`, the user is prompted for confirmation.
|
|
75
|
+
The prompt text can include `{value}` placeholder which will be replaced
|
|
76
|
+
with the actual value. The prompt is automatically formatted to remove any
|
|
77
|
+
trailing colon and append ` (y/n):` where `y` is the first truthy value.
|
|
78
|
+
|
|
79
|
+
If the environment variable `CLICK_EXTENDED_TESTING=1` is set, confirmation
|
|
80
|
+
is automatically granted without prompting (useful for automated tests).
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
prompt (str):
|
|
84
|
+
The confirmation prompt text. Can include `{value}` placeholder
|
|
85
|
+
for the current value (e.g., "Delete {value}"). Trailing colons
|
|
86
|
+
are automatically removed and ` (first_truthy/n):` is appended.
|
|
87
|
+
fn (Callable[[Any], bool] | Callable[[Any, Context], bool]):
|
|
88
|
+
Predicate function that determines whether to prompt. Returns
|
|
89
|
+
`True` to prompt for confirmation, `False` to skip. Can accept
|
|
90
|
+
either `fn(value)` or `fn(value, context)`.
|
|
91
|
+
truthy (list[str] | None, optional):
|
|
92
|
+
List of accepted confirmation responses. Case-insensitive.
|
|
93
|
+
Any response not in this list will abort execution. The first
|
|
94
|
+
value in this list is shown in the prompt hint (e.g., `(ok/n):`).
|
|
95
|
+
Defaults to `["y", "yes", "ok", "1"]`.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
click.Abort:
|
|
99
|
+
If the user provides a non-truthy response.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Decorator:
|
|
103
|
+
The decorator function.
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
Basic usage with value-only predicate:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
@command()
|
|
110
|
+
@option("count", type=int)
|
|
111
|
+
@confirm_if("Are you sure?", lambda x: x > 100)
|
|
112
|
+
def process(count: int) -> None:
|
|
113
|
+
click.echo(f"Processing {count} items...")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Using context to check other parameters:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
@command()
|
|
120
|
+
@option("force", is_flag=True)
|
|
121
|
+
@option("file")
|
|
122
|
+
@confirm_if(
|
|
123
|
+
"Delete {value}?",
|
|
124
|
+
lambda val, ctx: not ctx.get_parent("force").get_value()
|
|
125
|
+
)
|
|
126
|
+
def delete(force: bool, file: str) -> None:
|
|
127
|
+
click.echo(f"Deleted {file}")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Custom truthy values:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
@command()
|
|
134
|
+
@option("path")
|
|
135
|
+
@confirm_if(
|
|
136
|
+
"Overwrite {value}?",
|
|
137
|
+
lambda x: os.path.exists(x),
|
|
138
|
+
truthy=["yes", "y", "overwrite", "ok"]
|
|
139
|
+
)
|
|
140
|
+
def save(path: str) -> None:
|
|
141
|
+
click.echo(f"Saved to {path}")
|
|
142
|
+
```
|
|
143
|
+
"""
|
|
144
|
+
if truthy is None:
|
|
145
|
+
truthy = ["y", "yes", "ok", "1"]
|
|
146
|
+
|
|
147
|
+
return ConfirmIf.as_decorator(prompt=prompt, fn=fn, truthy=truthy)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Child node to set a default value if a value is not provided."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
7
|
+
from click_extended.core.other.context import Context
|
|
8
|
+
from click_extended.types import Decorator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Default(ChildNode):
|
|
12
|
+
"""Child node to set a default value if a value is not provided."""
|
|
13
|
+
|
|
14
|
+
def handle_all(
|
|
15
|
+
self, value: Any, context: Context, *args: Any, **kwargs: Any
|
|
16
|
+
) -> Any:
|
|
17
|
+
parent = context.get_current_parent_as_parent()
|
|
18
|
+
|
|
19
|
+
if parent.was_provided:
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
from_value = kwargs.get("from_value")
|
|
23
|
+
from_env = kwargs.get("from_env")
|
|
24
|
+
from_param = kwargs.get("from_param")
|
|
25
|
+
|
|
26
|
+
if from_value is not None:
|
|
27
|
+
return from_value
|
|
28
|
+
|
|
29
|
+
if from_env is not None:
|
|
30
|
+
env_value = os.getenv(from_env)
|
|
31
|
+
if env_value is not None:
|
|
32
|
+
return env_value
|
|
33
|
+
|
|
34
|
+
if from_param is not None:
|
|
35
|
+
param_parent = context.get_parent(from_param)
|
|
36
|
+
if param_parent is not None and param_parent.was_provided:
|
|
37
|
+
return param_parent.get_value()
|
|
38
|
+
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default(
|
|
43
|
+
*,
|
|
44
|
+
from_value: Any = None,
|
|
45
|
+
from_env: str | None = None,
|
|
46
|
+
from_param: str | None = None,
|
|
47
|
+
) -> Decorator:
|
|
48
|
+
"""
|
|
49
|
+
If a value is not provided, set a default value.
|
|
50
|
+
|
|
51
|
+
Type: `ChildNode`
|
|
52
|
+
|
|
53
|
+
Supports: `Any`
|
|
54
|
+
|
|
55
|
+
Order: `from_value`, `from_env`, `from_param`
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
from_value (Any, optional):
|
|
59
|
+
A value to default to.
|
|
60
|
+
from_env (str | None, optional):
|
|
61
|
+
The environment variable to default to.
|
|
62
|
+
from_param (str | None, optional):
|
|
63
|
+
The name of the node which value is used to default to.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Decorator:
|
|
67
|
+
The decorated function.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError:
|
|
71
|
+
If more than one source is provided.
|
|
72
|
+
"""
|
|
73
|
+
sources = [
|
|
74
|
+
from_value is not None,
|
|
75
|
+
from_env is not None,
|
|
76
|
+
from_param is not None,
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
if sum(sources) > 1:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"Only one of 'from_value', 'from_env', or "
|
|
82
|
+
"'from_param' can be provided."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if sum(sources) == 0:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"At least one of 'from_value', 'from_env', or "
|
|
88
|
+
"'from_param' must be provided."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return Default.as_decorator(
|
|
92
|
+
from_value=from_value,
|
|
93
|
+
from_env=from_env,
|
|
94
|
+
from_param=from_param,
|
|
95
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Show a deprecation warning for a parent node."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from click import echo
|
|
6
|
+
|
|
7
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
8
|
+
from click_extended.core.other.context import Context
|
|
9
|
+
from click_extended.types import Decorator
|
|
10
|
+
from click_extended.utils import is_option
|
|
11
|
+
|
|
12
|
+
OLD_ONLY = "The parameter '{}' has been deprecated."
|
|
13
|
+
OLD_TO_NEW = "The parameter '{}' has been deprecated. Use '{}' instead."
|
|
14
|
+
OLD_SINCE = "The parameter '{}' has been deprecated since '{}'."
|
|
15
|
+
OLD_REMOVED = (
|
|
16
|
+
"The parameter '{}' has been deprecated and will be removed in '{}'."
|
|
17
|
+
)
|
|
18
|
+
OLD_TO_NEW_SINCE = (
|
|
19
|
+
"The parameter '{}' has been deprecated since '{}'. Use '{}' instead."
|
|
20
|
+
)
|
|
21
|
+
OLD_TO_NEW_REMOVED = (
|
|
22
|
+
"The parameter '{}' has been deprecated and "
|
|
23
|
+
"will be removed in '{}'. Use '{}' instead."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
OLD_SINCE_REMOVED = (
|
|
27
|
+
"The parameter '{}' was deprecated in '{}' and will be removed in '{}'."
|
|
28
|
+
)
|
|
29
|
+
OLD_TO_NEW_SINCE_REMOVED = (
|
|
30
|
+
"The parameter '{}' was deprecated in '{}' "
|
|
31
|
+
"and will be removed in '{}'. Use '{}' instead."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Deprecated(ChildNode):
|
|
36
|
+
"""Show a deprecation warning for a parent node."""
|
|
37
|
+
|
|
38
|
+
def handle_all(
|
|
39
|
+
self, value: Any, context: Context, *args: Any, **kwargs: Any
|
|
40
|
+
) -> Any:
|
|
41
|
+
parent = context.get_current_parent_as_parent()
|
|
42
|
+
|
|
43
|
+
if not parent.was_provided:
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
old_param = parent.name
|
|
47
|
+
if is_option(parent):
|
|
48
|
+
if parent.long_flags:
|
|
49
|
+
old_param = parent.long_flags[0]
|
|
50
|
+
else:
|
|
51
|
+
old_param = parent.name
|
|
52
|
+
|
|
53
|
+
new_name = kwargs["name"]
|
|
54
|
+
new_param = None
|
|
55
|
+
|
|
56
|
+
if new_name is not None:
|
|
57
|
+
new_parent = context.get_parent(new_name)
|
|
58
|
+
if new_parent is None:
|
|
59
|
+
raise RuntimeError(f"Parent '{new_name}' does not exist.")
|
|
60
|
+
|
|
61
|
+
new_param = new_parent.name
|
|
62
|
+
if is_option(new_parent):
|
|
63
|
+
new_param = (
|
|
64
|
+
new_parent.long_flags[0]
|
|
65
|
+
if new_parent.long_flags
|
|
66
|
+
else new_parent.name
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if parent == new_parent:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"The parent '{new_parent.name}' cannot replace itself."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
since = kwargs["since"]
|
|
75
|
+
removed = kwargs["removed"]
|
|
76
|
+
|
|
77
|
+
key = (new_name is not None, since is not None, removed is not None)
|
|
78
|
+
|
|
79
|
+
messages = {
|
|
80
|
+
(False, False, False): OLD_ONLY.format(old_param),
|
|
81
|
+
(True, False, False): OLD_TO_NEW.format(old_param, new_param),
|
|
82
|
+
(False, True, False): OLD_SINCE.format(old_param, since),
|
|
83
|
+
(False, False, True): OLD_REMOVED.format(old_param, removed),
|
|
84
|
+
(True, True, False): OLD_TO_NEW_SINCE.format(
|
|
85
|
+
old_param, since, new_param
|
|
86
|
+
),
|
|
87
|
+
(True, False, True): OLD_TO_NEW_REMOVED.format(
|
|
88
|
+
old_param, removed, new_param
|
|
89
|
+
),
|
|
90
|
+
(False, True, True): OLD_SINCE_REMOVED.format(
|
|
91
|
+
old_param, since, removed
|
|
92
|
+
),
|
|
93
|
+
(True, True, True): OLD_TO_NEW_SINCE_REMOVED.format(
|
|
94
|
+
old_param, since, removed, new_param
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
echo(f"DeprecationWarning: {messages[key]}", err=True)
|
|
99
|
+
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def deprecated(
|
|
104
|
+
name: str | None = None,
|
|
105
|
+
since: str | None = None,
|
|
106
|
+
removed: str | None = None,
|
|
107
|
+
) -> Decorator:
|
|
108
|
+
"""
|
|
109
|
+
Show a deprecation warning when using a parameter.
|
|
110
|
+
|
|
111
|
+
Type: `ChildNode`
|
|
112
|
+
|
|
113
|
+
Supports: `Any`
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name (str):
|
|
117
|
+
The name of the new parameter.
|
|
118
|
+
since (str):
|
|
119
|
+
The version in which the parameter was deprecated.
|
|
120
|
+
removed (str):
|
|
121
|
+
The version the parameter will be removed.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Decorator:
|
|
125
|
+
The decorated function.
|
|
126
|
+
"""
|
|
127
|
+
return Deprecated.as_decorator(
|
|
128
|
+
name=name,
|
|
129
|
+
since=since,
|
|
130
|
+
removed=removed,
|
|
131
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Child node for warning the user that a parent is experimental."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from click import echo
|
|
6
|
+
|
|
7
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
8
|
+
from click_extended.core.other.context import Context
|
|
9
|
+
from click_extended.types import Decorator
|
|
10
|
+
from click_extended.utils import is_option
|
|
11
|
+
|
|
12
|
+
EMPTY = "The parameter '{}' is experimental."
|
|
13
|
+
SINCE = "The parameter '{}' is experimental since '{}'."
|
|
14
|
+
STABLE = "The parameter '{}' is experimental and will stable in '{}'."
|
|
15
|
+
SINCE_STABLE = (
|
|
16
|
+
"The parameter '{}' is experimental since '{}' and will stable in '{}'."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Experimental(ChildNode):
|
|
21
|
+
"""Child node for warning the user that a parent is experimental."""
|
|
22
|
+
|
|
23
|
+
def handle_all(
|
|
24
|
+
self, value: Any, context: Context, *args: Any, **kwargs: Any
|
|
25
|
+
) -> Any:
|
|
26
|
+
parent = context.get_current_parent_as_parent()
|
|
27
|
+
|
|
28
|
+
if not parent.was_provided:
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
message = kwargs["message"]
|
|
32
|
+
since = kwargs["since"]
|
|
33
|
+
stable = kwargs["stable"]
|
|
34
|
+
|
|
35
|
+
if message is not None:
|
|
36
|
+
echo(f"ExperimentalWarning: {message}")
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
name = parent.name
|
|
40
|
+
|
|
41
|
+
if is_option(parent):
|
|
42
|
+
name = parent.long_flags[0] if parent.long_flags else parent.name
|
|
43
|
+
|
|
44
|
+
key = (since is not None, stable is not None)
|
|
45
|
+
|
|
46
|
+
messages = {
|
|
47
|
+
(False, False): EMPTY.format(name),
|
|
48
|
+
(True, False): SINCE.format(name, since),
|
|
49
|
+
(False, True): STABLE.format(name, stable),
|
|
50
|
+
(True, True): SINCE_STABLE.format(name, since, stable),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
echo(f"ExperimentalWarning: {messages[key]}", err=True)
|
|
54
|
+
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def experimental(
|
|
59
|
+
*,
|
|
60
|
+
message: str | None = None,
|
|
61
|
+
since: str | None = None,
|
|
62
|
+
stable: str | None = None,
|
|
63
|
+
) -> Decorator:
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
Type: `ChildNode`
|
|
68
|
+
|
|
69
|
+
Supports: `Any`
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Decorator:
|
|
73
|
+
The decorated function.
|
|
74
|
+
"""
|
|
75
|
+
return Experimental.as_decorator(
|
|
76
|
+
message=message,
|
|
77
|
+
since=since,
|
|
78
|
+
stable=stable,
|
|
79
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Parent node to return the current time."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
6
|
+
|
|
7
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
8
|
+
from click_extended.core.other.context import Context
|
|
9
|
+
from click_extended.types import Decorator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Now(ParentNode):
|
|
13
|
+
"""Parent node to return the current time."""
|
|
14
|
+
|
|
15
|
+
def load(self, context: Context, *args: Any, **kwargs: Any) -> Any:
|
|
16
|
+
tz_name = kwargs.get("tz", "UTC")
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
tz = ZoneInfo(tz_name)
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise ValueError(f"Invalid timezone '{tz_name}': {e}") from e
|
|
22
|
+
|
|
23
|
+
return datetime.now(tz)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def now(name: str, tz: str = "UTC") -> Decorator:
|
|
27
|
+
"""
|
|
28
|
+
Parent node to return the current time.
|
|
29
|
+
|
|
30
|
+
Type: `ParentNode`
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
name (str):
|
|
34
|
+
The name of the parameter.
|
|
35
|
+
tz (str, optional):
|
|
36
|
+
The timezone to use for the datetime. Defaults to "UTC".
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Decorator:
|
|
40
|
+
The decorated function.
|
|
41
|
+
"""
|
|
42
|
+
return Now.as_decorator(name=name, tz=tz)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Initialization file for the `click_extended.decorators.random` module."""
|
|
2
|
+
|
|
3
|
+
from click_extended.decorators.random.random_bool import random_bool
|
|
4
|
+
from click_extended.decorators.random.random_choice import random_choice
|
|
5
|
+
from click_extended.decorators.random.random_datetime import random_datetime
|
|
6
|
+
from click_extended.decorators.random.random_float import random_float
|
|
7
|
+
from click_extended.decorators.random.random_integer import random_integer
|
|
8
|
+
from click_extended.decorators.random.random_prime import random_prime
|
|
9
|
+
from click_extended.decorators.random.random_string import random_string
|
|
10
|
+
from click_extended.decorators.random.random_uuid import random_uuid
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"random_bool",
|
|
14
|
+
"random_choice",
|
|
15
|
+
"random_datetime",
|
|
16
|
+
"random_float",
|
|
17
|
+
"random_integer",
|
|
18
|
+
"random_prime",
|
|
19
|
+
"random_string",
|
|
20
|
+
"random_uuid",
|
|
21
|
+
]
|