click-extended 1.0.0__py3-none-any.whl → 1.0.2__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 +2 -0
- 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/context.py +56 -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.2.dist-info}/METADATA +2 -1
- click_extended-1.0.2.dist-info/RECORD +150 -0
- click_extended-1.0.0.dist-info/RECORD +0 -10
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/WHEEL +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Format utility functions."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_list(
|
|
7
|
+
value: list[Any],
|
|
8
|
+
prefix_singular: str | None = None,
|
|
9
|
+
prefix_plural: str | None = None,
|
|
10
|
+
wrap: str | tuple[str, str] | None = None,
|
|
11
|
+
) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Format a list of primitives to a human-readable format.
|
|
14
|
+
|
|
15
|
+
- **Single item**: "x" or "Prefix Singular: x"
|
|
16
|
+
- **Two items**: "x and y" or "Prefix Plural: x and y"
|
|
17
|
+
- **Three+ items**: "x, y and z" or "Prefix Plural: x, y and z"
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
value (list[Any]):
|
|
21
|
+
The list of primitives to format. All items must be
|
|
22
|
+
primitives (str, int, float, bool).
|
|
23
|
+
prefix_singular (str, optional):
|
|
24
|
+
A prefix to use if the list only contains a single element.
|
|
25
|
+
If this parameter is set, `prefix_plural` must also be set.
|
|
26
|
+
prefix_plural (str, optional):
|
|
27
|
+
A prefix to use if the list contains zero or multiple elements.
|
|
28
|
+
If this parameter is set, `prefix_singular` must also be set.
|
|
29
|
+
wrap (str | tuple[str, str], optional):
|
|
30
|
+
A string to wrap each element with (applied to both sides),
|
|
31
|
+
or a tuple of (left, right) strings for asymmetric wrapping.
|
|
32
|
+
Returns:
|
|
33
|
+
str:
|
|
34
|
+
The formatted string.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError:
|
|
38
|
+
If only one prefix is provided or if the list is empty.
|
|
39
|
+
TypeError:
|
|
40
|
+
If a value in the list is not a primitive.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
>>> format_list(["str"])
|
|
44
|
+
str
|
|
45
|
+
>>> format_list(["str", "int"])
|
|
46
|
+
str and int
|
|
47
|
+
>>> format_list(["str", "int", "float"])
|
|
48
|
+
str, int and float
|
|
49
|
+
>>> format_list(
|
|
50
|
+
>>> ["str"],
|
|
51
|
+
>>> prefix_singular="Type: ",
|
|
52
|
+
>>> prefix_plural="Types: ",
|
|
53
|
+
>>> )
|
|
54
|
+
Type: str
|
|
55
|
+
>>> format_list(
|
|
56
|
+
>>> ["str", "int"],
|
|
57
|
+
>>> prefix_singular="Type: ",
|
|
58
|
+
>>> prefix_plural="Types: ",
|
|
59
|
+
>>> )
|
|
60
|
+
Types: str and int
|
|
61
|
+
>>> format_list(["Alice", "Bob", "Charlie"], wrap="'")
|
|
62
|
+
'Alice', 'Bob' and 'Charlie'
|
|
63
|
+
>>> format_list(["str", "int"], wrap=("<", ">"))
|
|
64
|
+
<str> and <int>
|
|
65
|
+
"""
|
|
66
|
+
if (prefix_singular is None) != (prefix_plural is None):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"Both prefix_singular and prefix_plural must be provided together, "
|
|
69
|
+
"or neither should be provided."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not value:
|
|
73
|
+
raise ValueError("Cannot format an empty list.")
|
|
74
|
+
|
|
75
|
+
for item in value:
|
|
76
|
+
if not isinstance(item, (str, int, float, bool)):
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"All items must be primitives (str, int, float, bool). "
|
|
79
|
+
f"Got {type(item).__name__}: {item!r}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
str_items = [str(item) for item in value]
|
|
83
|
+
|
|
84
|
+
if wrap is not None:
|
|
85
|
+
if isinstance(wrap, tuple):
|
|
86
|
+
left, right = wrap
|
|
87
|
+
str_items = [f"{left}{item}{right}" for item in str_items]
|
|
88
|
+
else:
|
|
89
|
+
str_items = [f"{wrap}{item}{wrap}" for item in str_items]
|
|
90
|
+
|
|
91
|
+
if len(str_items) == 1:
|
|
92
|
+
formatted = str_items[0]
|
|
93
|
+
prefix = prefix_singular if prefix_singular is not None else ""
|
|
94
|
+
elif len(str_items) == 2:
|
|
95
|
+
formatted = f"{str_items[0]} and {str_items[1]}"
|
|
96
|
+
prefix = prefix_plural if prefix_plural is not None else ""
|
|
97
|
+
else:
|
|
98
|
+
formatted = ", ".join(str_items[:-1]) + f" and {str_items[-1]}"
|
|
99
|
+
prefix = prefix_plural if prefix_plural is not None else ""
|
|
100
|
+
|
|
101
|
+
return prefix + formatted
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Format utility functions."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-return-statements
|
|
4
|
+
# pylint: disable=too-many-arguments
|
|
5
|
+
# pylint: disable=too-many-positional-arguments
|
|
6
|
+
|
|
7
|
+
from types import UnionType
|
|
8
|
+
from typing import Any, Iterable, cast, get_args, get_origin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def humanize_iterable(
|
|
12
|
+
value: Iterable[str | int | float | bool],
|
|
13
|
+
prefix_singular: str | None = None,
|
|
14
|
+
prefix_plural: str | None = None,
|
|
15
|
+
suffix_singular: str | None = None,
|
|
16
|
+
suffix_plural: str | None = None,
|
|
17
|
+
wrap: str | tuple[str, str] | None = None,
|
|
18
|
+
sep: str = "and",
|
|
19
|
+
) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Format an iterable of primitives to a human-readable format.
|
|
22
|
+
|
|
23
|
+
- **Single item**: "x"
|
|
24
|
+
- **Two items**: "x and y"
|
|
25
|
+
- **Three+ items**: "x, y and z"
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
value (Iterable[str | int | float | bool]):
|
|
29
|
+
The iterable of primitives to format. All items must be
|
|
30
|
+
primitives (str, int, float, bool).
|
|
31
|
+
prefix_singular (str, optional):
|
|
32
|
+
A prefix to use if the list only contains a single element.
|
|
33
|
+
If this parameter is set, `prefix_plural` must also be set.
|
|
34
|
+
prefix_plural (str, optional):
|
|
35
|
+
A prefix to use if the list contains zero or multiple elements.
|
|
36
|
+
If this parameter is set, `prefix_singular` must also be set.
|
|
37
|
+
suffix_singular (str, optional):
|
|
38
|
+
A suffix to use if the list only contains a single element.
|
|
39
|
+
If this parameter is set, `suffix_plural` must also be set.
|
|
40
|
+
suffix_plural (str, optional):
|
|
41
|
+
A suffix to use if the list contains zero or multiple elements.
|
|
42
|
+
If this parameter is set, `prefix_singular` must also be set.
|
|
43
|
+
wrap (str | tuple[str, str], optional):
|
|
44
|
+
A string to wrap each element with (applied to both sides),
|
|
45
|
+
or a tuple of (left, right) strings for asymmetric wrapping.
|
|
46
|
+
sep (str, optional):
|
|
47
|
+
The sep when the length is greater than 2 (X, Y <sep> Z).
|
|
48
|
+
Returns:
|
|
49
|
+
str:
|
|
50
|
+
The formatted string.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError:
|
|
54
|
+
If only one prefix is provided or if the list is empty.
|
|
55
|
+
TypeError:
|
|
56
|
+
If a value in the list is not a primitive.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
>>> humanize_iterable(["str"])
|
|
60
|
+
str
|
|
61
|
+
>>> humanize_iterable(["str", "int"])
|
|
62
|
+
str and int
|
|
63
|
+
>>> humanize_iterable(["str", "int", "float"])
|
|
64
|
+
str, int and float
|
|
65
|
+
>>> humanize_iterable(
|
|
66
|
+
>>> ["str"],
|
|
67
|
+
>>> prefix_singular="Type: ",
|
|
68
|
+
>>> prefix_plural="Types: ",
|
|
69
|
+
>>> )
|
|
70
|
+
Type: str
|
|
71
|
+
>>> humanize_iterable(
|
|
72
|
+
>>> ["str", "int"],
|
|
73
|
+
>>> prefix_singular="Type: ",
|
|
74
|
+
>>> prefix_plural="Types: ",
|
|
75
|
+
>>> )
|
|
76
|
+
Types: str and int
|
|
77
|
+
>>> humanize_iterable(["Alice", "Bob", "Charlie"], wrap="'")
|
|
78
|
+
'Alice', 'Bob' and 'Charlie'
|
|
79
|
+
>>> humanize_iterable(["str", "int"], wrap=("<", ">"))
|
|
80
|
+
<str> and <int>
|
|
81
|
+
"""
|
|
82
|
+
if (prefix_singular is None) != (prefix_plural is None):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Both prefix_singular and prefix_plural must be provided together, "
|
|
85
|
+
"or neither should be provided."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if (suffix_singular is None) != (suffix_plural is None):
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"Both suffix_singular and suffix_plural must be provided together, "
|
|
91
|
+
"or neither should be provided."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not value:
|
|
95
|
+
raise ValueError("Cannot format an empty iterable.")
|
|
96
|
+
|
|
97
|
+
for item in value:
|
|
98
|
+
if not isinstance(item, (str, int, float, bool)):
|
|
99
|
+
raise TypeError(
|
|
100
|
+
f"All items must be primitives (str, int, float, bool). "
|
|
101
|
+
f"Got {type(item).__name__}: {item!r}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
str_items = [str(item) for item in value]
|
|
105
|
+
|
|
106
|
+
if wrap is not None:
|
|
107
|
+
if isinstance(wrap, tuple):
|
|
108
|
+
left, right = wrap
|
|
109
|
+
str_items = [f"{left}{item}{right}" for item in str_items]
|
|
110
|
+
else:
|
|
111
|
+
str_items = [f"{wrap}{item}{wrap}" for item in str_items]
|
|
112
|
+
|
|
113
|
+
if len(str_items) == 1:
|
|
114
|
+
formatted = str_items[0]
|
|
115
|
+
prefix = prefix_singular if prefix_singular is not None else ""
|
|
116
|
+
suffix = suffix_singular if suffix_singular is not None else ""
|
|
117
|
+
elif len(str_items) == 2:
|
|
118
|
+
formatted = f"{str_items[0]} {sep} {str_items[1]}"
|
|
119
|
+
prefix = prefix_plural if prefix_plural is not None else ""
|
|
120
|
+
suffix = suffix_plural if suffix_plural is not None else ""
|
|
121
|
+
else:
|
|
122
|
+
formatted = ", ".join(str_items[:-1]) + f" {sep} {str_items[-1]}"
|
|
123
|
+
prefix = prefix_plural if prefix_plural is not None else ""
|
|
124
|
+
suffix = suffix_plural if suffix_plural is not None else ""
|
|
125
|
+
|
|
126
|
+
return str(prefix.strip() + " " + formatted + " " + suffix.strip()).strip()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def humanize_type(value: Any) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Humanize the output of a `type` instance in the same format as
|
|
132
|
+
`humanize_iterable`.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
value (type):
|
|
136
|
+
The type to humanize. This can be simple types like `str` and more
|
|
137
|
+
complex types like `typing.Union` or `types.UnionType`.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
str:
|
|
141
|
+
The formatted string.
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
>>> humanize_type(str)
|
|
145
|
+
str
|
|
146
|
+
>>> humanize_type(int | float)
|
|
147
|
+
int and float
|
|
148
|
+
>>> humanize_type(int | float | bool)
|
|
149
|
+
int, float and bool
|
|
150
|
+
>>> humanize_type(list[int | float])
|
|
151
|
+
list[int | float]
|
|
152
|
+
>>> humanize_type(tuple[str] | list[str])
|
|
153
|
+
tuple[str] and list[str]
|
|
154
|
+
>>> humanize_type(str | list[list[str]])
|
|
155
|
+
str and list[list[str]]
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def format_type(t: Any, inside_generic: bool = False) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Format a type into a string.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
t (Any):
|
|
164
|
+
The type to format
|
|
165
|
+
inside_generic (bool):
|
|
166
|
+
If `True`, unions use `|` instead of `humanize_iterable`
|
|
167
|
+
"""
|
|
168
|
+
if t is type(None):
|
|
169
|
+
return "None"
|
|
170
|
+
|
|
171
|
+
origin = get_origin(t)
|
|
172
|
+
args = get_args(t)
|
|
173
|
+
|
|
174
|
+
is_union = origin is UnionType or str(origin).startswith("typing.Union")
|
|
175
|
+
if is_union:
|
|
176
|
+
members: list[str] = []
|
|
177
|
+
for arg in args:
|
|
178
|
+
members.append(format_type(arg, inside_generic=True))
|
|
179
|
+
|
|
180
|
+
if inside_generic:
|
|
181
|
+
return " | ".join(members)
|
|
182
|
+
|
|
183
|
+
return humanize_iterable(members)
|
|
184
|
+
|
|
185
|
+
if origin is not None and args:
|
|
186
|
+
origin_name = getattr(origin, "__name__", str(origin))
|
|
187
|
+
|
|
188
|
+
if args[-1] is Ellipsis:
|
|
189
|
+
inner_types: list[str] = []
|
|
190
|
+
for arg in args[:-1]:
|
|
191
|
+
inner_types.append(format_type(arg, inside_generic=True))
|
|
192
|
+
inner = ", ".join(inner_types)
|
|
193
|
+
return f"{origin_name}[{inner}, ...]"
|
|
194
|
+
|
|
195
|
+
inner_types = []
|
|
196
|
+
for arg in args:
|
|
197
|
+
inner_types.append(format_type(arg, inside_generic=True))
|
|
198
|
+
inner = ", ".join(inner_types)
|
|
199
|
+
return f"{origin_name}[{inner}]"
|
|
200
|
+
|
|
201
|
+
if hasattr(t, "__name__"):
|
|
202
|
+
return cast(str, t.__name__)
|
|
203
|
+
|
|
204
|
+
return str(t)
|
|
205
|
+
|
|
206
|
+
return format_type(value, inside_generic=False)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
__all__ = ["humanize_iterable", "humanize_type"]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Utilities for validating and parsing naming conventions."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from click_extended.utils.humanize import humanize_iterable
|
|
9
|
+
|
|
10
|
+
SNAKE_CASE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(_[a-z0-9]+)*$")
|
|
11
|
+
SCREAMING_SNAKE_CASE_PATTERN = re.compile(r"^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$")
|
|
12
|
+
KEBAB_CASE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
|
|
13
|
+
|
|
14
|
+
LONG_FLAG_PATTERN = re.compile(r"^--[a-z][a-z0-9-]*$")
|
|
15
|
+
SHORT_FLAG_PATTERN = re.compile(r"^-[a-zA-Z]$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_valid_name(name: str) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Check if a name is a valid name, in this case only `snake_case` formats
|
|
21
|
+
are considered valid.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name (str):
|
|
25
|
+
The name to validate.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
bool:
|
|
29
|
+
`True` if the name is valid, `False` otherwise.
|
|
30
|
+
"""
|
|
31
|
+
return bool(SNAKE_CASE_PATTERN.match(name))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_long_flag(value: str) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Check if a value is a long flag (e.g. --option).
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
value (str):
|
|
40
|
+
The value to check.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
`True` if the value is a long flag, `False` otherwise.
|
|
44
|
+
"""
|
|
45
|
+
return bool(LONG_FLAG_PATTERN.match(value))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_short_flag(value: str) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Check if a value is a short flag (e.g. -o).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
value (str):
|
|
54
|
+
The value to check.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
`True` if the value is a short flag, `False` otherwise.
|
|
58
|
+
"""
|
|
59
|
+
return bool(SHORT_FLAG_PATTERN.match(value))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_name(name: str, context: str = "name") -> None:
|
|
63
|
+
"""
|
|
64
|
+
Validate a name follows naming conventions.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
name (str):
|
|
68
|
+
The name to validate.
|
|
69
|
+
context (Context):
|
|
70
|
+
Context string for error messages
|
|
71
|
+
(e.g. "option name", "argument name").
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError:
|
|
75
|
+
If the name doesn't follow valid conventions.
|
|
76
|
+
"""
|
|
77
|
+
if not is_valid_name(name):
|
|
78
|
+
|
|
79
|
+
def _exit_program(message: str, tip: str) -> None:
|
|
80
|
+
click.echo(f"InvalidNameError: {message}", err=True)
|
|
81
|
+
click.echo(f"Tip: {tip}", err=True)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
# Empty name
|
|
85
|
+
if not name:
|
|
86
|
+
_exit_program(
|
|
87
|
+
"The name cannot be empty.",
|
|
88
|
+
"Provide a valid snake_case name (e.g. name, my_name)",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Starts with number
|
|
92
|
+
if re.match(r"^\d", name):
|
|
93
|
+
_exit_program(
|
|
94
|
+
f"The name '{name}' cannot start with a number.",
|
|
95
|
+
f"Try with a letter prefix (e.g. 'var_{name}' or 'opt_{name}')",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Starts with underscore
|
|
99
|
+
if name[0] == "_":
|
|
100
|
+
_exit_program(
|
|
101
|
+
f"The name '{name}' cannot start with an underscore.",
|
|
102
|
+
f"Try '{name[1:]}' without the leading underscore",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Contains uppercase
|
|
106
|
+
if re.search(r"[A-Z]", name):
|
|
107
|
+
suggested = re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
|
|
108
|
+
_exit_program(
|
|
109
|
+
f"The name '{name}' contains uppercase letters.",
|
|
110
|
+
f"Only use lowercase characters such as '{suggested}'",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Contains hyphens
|
|
114
|
+
if "-" in name:
|
|
115
|
+
_exit_program(
|
|
116
|
+
f"The name '{name}' contains hyphens.",
|
|
117
|
+
f"Use underscores instead like '{name.replace('-', '_')}'",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Contains spaces
|
|
121
|
+
if re.search(r"\s", name):
|
|
122
|
+
suggested_name = re.sub(r"\s+", "_", name).lower()
|
|
123
|
+
_exit_program(
|
|
124
|
+
f"The name '{name}' contains whitespace.",
|
|
125
|
+
f"Use underscores instead like '{suggested_name}'",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Contains consecutive underscores
|
|
129
|
+
if re.search(r"__+", name):
|
|
130
|
+
_exit_program(
|
|
131
|
+
f"The name '{name}' contains consecutive underscores.",
|
|
132
|
+
f"Use single underscores like '{re.sub(r'__+', '_', name)}'",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Contains invalid characters
|
|
136
|
+
invalid_match = re.search(r"[^a-z0-9_]", name)
|
|
137
|
+
if invalid_match:
|
|
138
|
+
invalid_chars = set(re.findall(r"[^a-z0-9_]", name))
|
|
139
|
+
contains_chars = humanize_iterable(
|
|
140
|
+
invalid_chars,
|
|
141
|
+
wrap="'",
|
|
142
|
+
prefix_singular="an invalid character",
|
|
143
|
+
prefix_plural="invalid characters",
|
|
144
|
+
)
|
|
145
|
+
remove_chars = humanize_iterable(
|
|
146
|
+
invalid_chars,
|
|
147
|
+
wrap="'",
|
|
148
|
+
prefix_singular="the character",
|
|
149
|
+
prefix_plural="the characters",
|
|
150
|
+
)
|
|
151
|
+
_exit_program(
|
|
152
|
+
f"The name '{name}' is invalid and contains {contains_chars}",
|
|
153
|
+
f"Remove {remove_chars}",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# General fallback
|
|
157
|
+
_exit_program(
|
|
158
|
+
f"The name '{name}' is invalid.",
|
|
159
|
+
"Names must be in snake_case (e.g. name, my_name, my_name1)",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_option_args(*args: str) -> tuple[str | None, str | None, str | None]:
|
|
164
|
+
"""
|
|
165
|
+
Parse option decorator arguments intelligently.
|
|
166
|
+
|
|
167
|
+
Supports three forms:
|
|
168
|
+
|
|
169
|
+
1. **@option("my_option")**: name only
|
|
170
|
+
2. **@option("--my-option")**: long flag (derives name)
|
|
171
|
+
3. **@option("-m", "--my-option")**: short and long flags
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
*args (str):
|
|
175
|
+
Positional arguments passed to `@option` decorator.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Tuple of (name, short, long):
|
|
179
|
+
|
|
180
|
+
- **name**: The parameter name (`None` if not provided)
|
|
181
|
+
- **short**: The short flag (`None` if not provided)
|
|
182
|
+
- **long**: The long flag (`None` if not provided)
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError:
|
|
186
|
+
If arguments are invalid or ambiguous.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
if len(args) == 0:
|
|
190
|
+
return None, None, None
|
|
191
|
+
|
|
192
|
+
if len(args) == 1:
|
|
193
|
+
arg = args[0]
|
|
194
|
+
|
|
195
|
+
# @option("--my-option")
|
|
196
|
+
if is_long_flag(arg):
|
|
197
|
+
return None, None, arg
|
|
198
|
+
|
|
199
|
+
# @option("-m")
|
|
200
|
+
if is_short_flag(arg):
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"Short flag '{arg}' provided without long flag or name. "
|
|
203
|
+
f"Use one of:\n"
|
|
204
|
+
f" @option('{arg}', '--flag') # with long flag\n"
|
|
205
|
+
f" @option('name', short='{arg}') # with name parameter"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# @option("my_option")
|
|
209
|
+
validate_name(arg, "option name")
|
|
210
|
+
return arg, None, None
|
|
211
|
+
|
|
212
|
+
if len(args) == 2:
|
|
213
|
+
first, second = args
|
|
214
|
+
|
|
215
|
+
# @option("-m", "--my-option")
|
|
216
|
+
if is_short_flag(first) and is_long_flag(second):
|
|
217
|
+
return None, first, second
|
|
218
|
+
|
|
219
|
+
# @option("--my-option", "-m")
|
|
220
|
+
if is_long_flag(first) and is_short_flag(second):
|
|
221
|
+
raise ValueError(
|
|
222
|
+
"Invalid argument order. "
|
|
223
|
+
"Short flag must come before long flag:\n"
|
|
224
|
+
f" Use: @option('{second}', '{first}')"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"Invalid arguments: '{first}', '{second}'. "
|
|
229
|
+
f"Expected one of:\n"
|
|
230
|
+
f" @option('name') # name only\n"
|
|
231
|
+
f" @option('--flag') # long flag only\n"
|
|
232
|
+
f" @option('-f', '--flag') # short and long flags"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"Too many positional arguments ({len(args)}). "
|
|
237
|
+
f"Maximum is 2 (short and long flags)."
|
|
238
|
+
)
|