click-extended 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- click_extended/__init__.py +0 -13
- click_extended/classes.py +21 -0
- click_extended/errors.py +336 -220
- click_extended/types.py +4 -25
- click_extended-0.4.0.dist-info/METADATA +261 -0
- click_extended-0.4.0.dist-info/RECORD +10 -0
- click_extended-0.3.1.dist-info/METADATA +0 -257
- click_extended-0.3.1.dist-info/RECORD +0 -9
- {click_extended-0.3.1.dist-info → click_extended-0.4.0.dist-info}/WHEEL +0 -0
- {click_extended-0.3.1.dist-info → click_extended-0.4.0.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-0.3.1.dist-info → click_extended-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {click_extended-0.3.1.dist-info → click_extended-0.4.0.dist-info}/top_level.txt +0 -0
click_extended/__init__.py
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
"""Initialization file for the 'click_extended' module."""
|
|
2
2
|
|
|
3
|
-
from click_extended.core._child_node import ChildNode, ProcessContext
|
|
4
|
-
from click_extended.core._global_node import GlobalNode
|
|
5
|
-
from click_extended.core._node import Node
|
|
6
|
-
from click_extended.core._parent_node import ParentNode
|
|
7
|
-
from click_extended.core._root_node import RootNode
|
|
8
|
-
from click_extended.core._tree import Tree
|
|
9
3
|
from click_extended.core.argument import argument
|
|
10
4
|
from click_extended.core.command import command
|
|
11
5
|
from click_extended.core.env import env
|
|
@@ -14,13 +8,6 @@ from click_extended.core.option import option
|
|
|
14
8
|
from click_extended.core.tag import tag
|
|
15
9
|
|
|
16
10
|
__all__ = [
|
|
17
|
-
"ChildNode",
|
|
18
|
-
"ProcessContext",
|
|
19
|
-
"GlobalNode",
|
|
20
|
-
"Node",
|
|
21
|
-
"ParentNode",
|
|
22
|
-
"RootNode",
|
|
23
|
-
"Tree",
|
|
24
11
|
"argument",
|
|
25
12
|
"command",
|
|
26
13
|
"env",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Classes used in `click_extended`."""
|
|
2
|
+
|
|
3
|
+
from click_extended.core.argument_node import ArgumentNode
|
|
4
|
+
from click_extended.core.child_node import ChildNode
|
|
5
|
+
from click_extended.core.command import Command
|
|
6
|
+
from click_extended.core.group import Group
|
|
7
|
+
from click_extended.core.node import Node
|
|
8
|
+
from click_extended.core.option_node import OptionNode
|
|
9
|
+
from click_extended.core.parent_node import ParentNode
|
|
10
|
+
from click_extended.core.tag import Tag
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Node",
|
|
14
|
+
"ChildNode",
|
|
15
|
+
"ParentNode",
|
|
16
|
+
"ArgumentNode",
|
|
17
|
+
"OptionNode",
|
|
18
|
+
"Command",
|
|
19
|
+
"Group",
|
|
20
|
+
"Tag",
|
|
21
|
+
]
|
click_extended/errors.py
CHANGED
|
@@ -1,334 +1,450 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Error module for the `click_extended` library."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import inspect
|
|
7
|
-
from typing import IO, Any
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
8
5
|
|
|
9
6
|
import click
|
|
10
|
-
from click import ClickException
|
|
11
|
-
from click._compat import get_text_stderr
|
|
12
7
|
from click.utils import echo
|
|
13
8
|
|
|
14
|
-
from click_extended.utils.
|
|
9
|
+
from click_extended.utils.humanize import humanize_iterable
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
class ClickExtendedError(Exception):
|
|
18
|
-
"""Base exception for
|
|
19
|
-
|
|
13
|
+
"""Base exception for all click-extended errors."""
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
def __init__(self, message: str, tip: str | None = None) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initialize a ClickExtendedError.
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
Args:
|
|
20
|
+
message (str):
|
|
21
|
+
The error message describing what went wrong.
|
|
22
|
+
tip (str):
|
|
23
|
+
Optional helpful guidance for resolving the error.
|
|
24
|
+
"""
|
|
25
|
+
self.message = message
|
|
26
|
+
self.tip = tip
|
|
27
|
+
super().__init__(message)
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def show(self, file: Any = None) -> None:
|
|
30
30
|
"""
|
|
31
|
-
|
|
31
|
+
Display the error message.
|
|
32
|
+
|
|
33
|
+
Subclasses should override this to provide custom formatting.
|
|
32
34
|
|
|
33
35
|
Args:
|
|
34
|
-
|
|
36
|
+
file (Any, optional):
|
|
37
|
+
The file to write to (defaults to sys.stderr).
|
|
35
38
|
"""
|
|
36
|
-
|
|
39
|
+
if file is None:
|
|
40
|
+
file = sys.stderr
|
|
41
|
+
|
|
42
|
+
echo(f"Error: {self.message}", file=file)
|
|
37
43
|
|
|
44
|
+
if self.tip:
|
|
45
|
+
echo(f"\nTip: {self.tip}", file=file)
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
|
|
48
|
+
class ContextAwareError(ClickExtendedError):
|
|
40
49
|
"""
|
|
41
|
-
Base
|
|
42
|
-
|
|
50
|
+
Base exception for errors that occur within Click context.
|
|
51
|
+
|
|
52
|
+
These errors have access to the full node hierarchy and are formatted
|
|
53
|
+
with Click-style usage information and node context.
|
|
54
|
+
|
|
55
|
+
It can only be raised during `phase 3` or `phase 4`.
|
|
43
56
|
"""
|
|
44
57
|
|
|
45
|
-
|
|
58
|
+
context: click.Context | None
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str, tip: str | None = None) -> None:
|
|
46
61
|
"""
|
|
47
|
-
Initialize a new `
|
|
62
|
+
Initialize a new `ContextAwareError` instance.
|
|
48
63
|
|
|
49
64
|
Args:
|
|
50
65
|
message (str):
|
|
51
|
-
The error message
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
The error message describing what went wrong.
|
|
67
|
+
tip (str):
|
|
68
|
+
Optional helpful guidance for resolving the error.
|
|
69
|
+
"""
|
|
70
|
+
super().__init__(message, tip)
|
|
71
|
+
try:
|
|
72
|
+
self.context = click.get_current_context()
|
|
73
|
+
self._node_name = self._resolve_node_name()
|
|
74
|
+
except RuntimeError:
|
|
75
|
+
self.context = None
|
|
76
|
+
self._node_name = "unknown"
|
|
77
|
+
|
|
78
|
+
def _resolve_node_name(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Get the most specific node name from context.
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
If inside a child node, that will be used, otherwise it checks if a
|
|
83
|
+
parent is defined, and if not that, the root node will be used.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
str:
|
|
87
|
+
The name of the most specific node in the current scope.
|
|
58
88
|
"""
|
|
59
|
-
self.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if frame:
|
|
63
|
-
current_frame = frame.f_back
|
|
64
|
-
depth = 0
|
|
65
|
-
max_depth = 10
|
|
66
|
-
|
|
67
|
-
while current_frame and depth < max_depth:
|
|
68
|
-
caller_locals = current_frame.f_locals
|
|
69
|
-
if "self" in caller_locals:
|
|
70
|
-
self_obj = caller_locals["self"]
|
|
71
|
-
if hasattr(self_obj, "name") and hasattr(
|
|
72
|
-
self_obj, "process"
|
|
73
|
-
):
|
|
74
|
-
self.child_name = self_obj.name
|
|
75
|
-
break
|
|
76
|
-
current_frame = current_frame.f_back
|
|
77
|
-
depth += 1
|
|
78
|
-
|
|
79
|
-
if not self.child_name:
|
|
80
|
-
raise RuntimeError(
|
|
81
|
-
f"{self.__class__.__name__} must be raised from within a "
|
|
82
|
-
f"ChildNode.process() method. The exception was raised outside "
|
|
83
|
-
f"a valid ChildNode context."
|
|
84
|
-
)
|
|
89
|
+
if self.context is None:
|
|
90
|
+
return "unknown"
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
super().__init__(formatted_message)
|
|
92
|
+
meta = self.context.meta.get("click_extended", {})
|
|
88
93
|
|
|
94
|
+
if meta.get("child_node"):
|
|
95
|
+
return str(meta["child_node"].name)
|
|
96
|
+
if meta.get("parent_node"):
|
|
97
|
+
return str(meta["parent_node"].name)
|
|
98
|
+
if meta.get("root_node"):
|
|
99
|
+
return str(meta["root_node"].name)
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
"""Exception raised when a value in the `process()` method is unexpected."""
|
|
101
|
+
return "unknown"
|
|
92
102
|
|
|
93
|
-
def
|
|
103
|
+
def show(self, file: Any = None) -> None:
|
|
94
104
|
"""
|
|
95
|
-
|
|
105
|
+
Display the error with Click-style formatting.
|
|
106
|
+
|
|
107
|
+
Format:
|
|
108
|
+
Usage: cli [OPTIONS] COMMAND [ARGS]...
|
|
109
|
+
Try 'cli --help' for help.
|
|
110
|
+
|
|
111
|
+
Error (node_name): message
|
|
112
|
+
Tip: helpful guidance
|
|
96
113
|
|
|
97
114
|
Args:
|
|
98
|
-
|
|
99
|
-
The
|
|
115
|
+
file (Any, optional):
|
|
116
|
+
The file to write to (defaults to `sys.stderr`).
|
|
100
117
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
if file is None:
|
|
119
|
+
file = sys.stderr
|
|
103
120
|
|
|
121
|
+
if self.context is None:
|
|
122
|
+
super().show(file)
|
|
123
|
+
return
|
|
104
124
|
|
|
105
|
-
|
|
106
|
-
"""Exception raised when validation fails in a child node."""
|
|
125
|
+
echo(self.context.get_usage(), file=file, color=self.context.color)
|
|
107
126
|
|
|
127
|
+
if self.context.command.get_help_option(self.context) is not None:
|
|
128
|
+
hint = f"Try '{self.context.command_path} --help' for help."
|
|
129
|
+
echo(hint, file=file, color=self.context.color)
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
"""Exception raised when transformation fails in a child node."""
|
|
131
|
+
echo("", file=file)
|
|
111
132
|
|
|
133
|
+
exception_name = self.__class__.__name__
|
|
134
|
+
echo(
|
|
135
|
+
f"{exception_name} ({self._node_name}): {self.message}",
|
|
136
|
+
file=file,
|
|
137
|
+
color=self.context.color,
|
|
138
|
+
)
|
|
112
139
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Exception raised when an unexpected error occurs or parts of code
|
|
116
|
-
which is unhandled.
|
|
117
|
-
"""
|
|
140
|
+
if self.tip:
|
|
141
|
+
echo(f"Tip: {self.tip}", file=file, color=self.context.color)
|
|
118
142
|
|
|
119
143
|
|
|
120
|
-
class
|
|
121
|
-
"""
|
|
144
|
+
class MissingValueError(ContextAwareError):
|
|
145
|
+
"""
|
|
146
|
+
Exception raised when a value is missing.
|
|
122
147
|
|
|
123
|
-
This exception is
|
|
124
|
-
|
|
148
|
+
This exception is context-aware and can only be raised during `phase 3` or
|
|
149
|
+
`phase 4`.
|
|
125
150
|
"""
|
|
126
151
|
|
|
127
|
-
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
"""Initialize a new `MissingValueError` instance."""
|
|
154
|
+
super().__init__(
|
|
155
|
+
message="Value not provided.", tip=self._generate_tip()
|
|
156
|
+
)
|
|
128
157
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
) -> None:
|
|
135
|
-
"""
|
|
136
|
-
Initialize a ParameterError.
|
|
158
|
+
def _generate_tip(self) -> str:
|
|
159
|
+
"""Generate a context-aware tip based on the parent node type."""
|
|
160
|
+
try:
|
|
161
|
+
ctx = click.get_current_context()
|
|
162
|
+
meta = ctx.meta.get("click_extended", {})
|
|
137
163
|
|
|
138
|
-
|
|
139
|
-
message (str):
|
|
140
|
-
The error message from the validator/transformer.
|
|
141
|
-
param_hint (str, optional):
|
|
142
|
-
The parameter name (e.g., '--config', 'PATH').
|
|
143
|
-
ctx (click.Context, optional):
|
|
144
|
-
The Click context for displaying usage information.
|
|
145
|
-
"""
|
|
146
|
-
super().__init__(message)
|
|
147
|
-
self.param_hint = param_hint
|
|
148
|
-
self.ctx = ctx
|
|
164
|
+
parent = meta.get("parent_node")
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return self.message
|
|
166
|
+
if parent is not None:
|
|
167
|
+
return self._tip_for_parent(parent)
|
|
168
|
+
except (RuntimeError, AttributeError):
|
|
169
|
+
pass
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
file = get_text_stderr()
|
|
171
|
+
return (
|
|
172
|
+
"Provide a value or set the default parameter to make it optional."
|
|
173
|
+
)
|
|
160
174
|
|
|
161
|
-
|
|
175
|
+
# pylint: disable=too-many-return-statements
|
|
176
|
+
def _tip_for_parent(self, parent: Any) -> str:
|
|
177
|
+
"""Generate tip based on parent type."""
|
|
178
|
+
parent_type = parent.__class__.__name__
|
|
179
|
+
parent_name = parent.name
|
|
162
180
|
|
|
163
|
-
if
|
|
164
|
-
|
|
181
|
+
if parent_type == "Option":
|
|
182
|
+
return "".join(
|
|
183
|
+
f"Use --{parent_name.replace('_', '-')} to specify a value, "
|
|
184
|
+
"or set the default parameter to make it optional."
|
|
185
|
+
)
|
|
186
|
+
if parent_type == "Argument":
|
|
187
|
+
return "".join(
|
|
188
|
+
f"Provide the {parent_name} argument, or set the default "
|
|
189
|
+
"parameter to make it optional."
|
|
190
|
+
)
|
|
191
|
+
if parent_type == "Env":
|
|
192
|
+
env_var = getattr(parent, "env_name", parent_name.upper())
|
|
193
|
+
return "".join(
|
|
194
|
+
f"Set the {env_var} environment variable, or set the "
|
|
195
|
+
f"default parameter to make it optional."
|
|
196
|
+
)
|
|
197
|
+
return "".join(
|
|
198
|
+
"Provide a value or set the default parameter to "
|
|
199
|
+
"make it optional."
|
|
200
|
+
)
|
|
165
201
|
|
|
166
|
-
echo(self.ctx.get_usage(), file=file, color=color)
|
|
167
202
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
f"Try '{self.ctx.command_path} "
|
|
171
|
-
f"{self.ctx.help_option_names[0]}' for help."
|
|
172
|
-
)
|
|
173
|
-
echo(hint, file=file, color=color)
|
|
203
|
+
class NoRootError(ContextAwareError):
|
|
204
|
+
"""Exception raised when no root node has been defined."""
|
|
174
205
|
|
|
175
|
-
|
|
206
|
+
def __init__(self, tip: str | None = None) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Initialize a new `NoRootError` instance.
|
|
176
209
|
|
|
177
|
-
|
|
210
|
+
Args:
|
|
211
|
+
tip (str):
|
|
212
|
+
Optional helpful guidance (defaults to standard tip).
|
|
213
|
+
"""
|
|
214
|
+
super().__init__(
|
|
215
|
+
"No root node has been defined",
|
|
216
|
+
tip=tip or "Use @click_extended.root() decorator first",
|
|
217
|
+
)
|
|
178
218
|
|
|
179
219
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"""Exception raised when no `ParentNode` has been defined."""
|
|
220
|
+
class NoParentError(ContextAwareError):
|
|
221
|
+
"""Exception raised when a child node has no parent to attach to."""
|
|
183
222
|
|
|
184
|
-
def __init__(self,
|
|
223
|
+
def __init__(self, child_name: str, tip: str | None = None) -> None:
|
|
185
224
|
"""
|
|
186
225
|
Initialize a new `NoParentError` instance.
|
|
187
226
|
|
|
188
227
|
Args:
|
|
189
|
-
|
|
228
|
+
child_name (str):
|
|
190
229
|
The name of the child node.
|
|
230
|
+
tip (str):
|
|
231
|
+
Optional helpful guidance (defaults to standard tip).
|
|
191
232
|
"""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
"
|
|
196
|
-
|
|
233
|
+
tip_msg = (
|
|
234
|
+
tip
|
|
235
|
+
or "Ensure a parent node (option/argument) is defined "
|
|
236
|
+
"before child nodes"
|
|
237
|
+
)
|
|
238
|
+
super().__init__(
|
|
239
|
+
f"Cannot register child node '{child_name}' "
|
|
240
|
+
f"as no parent is defined",
|
|
241
|
+
tip=tip_msg,
|
|
197
242
|
)
|
|
198
|
-
super().__init__(message)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
class NoRootError(ClickExtendedError):
|
|
202
|
-
"""Exception raised when there is no `RootNode` defined."""
|
|
203
|
-
|
|
204
|
-
def __init__(self, message: str | None = None) -> None:
|
|
205
|
-
"""Initialize a new `NoRootError` instance."""
|
|
206
|
-
super().__init__(message or "No root node is defined in the tree.")
|
|
207
243
|
|
|
208
244
|
|
|
209
|
-
class
|
|
210
|
-
"""Exception raised when
|
|
245
|
+
class RootExistsError(ContextAwareError):
|
|
246
|
+
"""Exception raised when attempting to define multiple root nodes."""
|
|
211
247
|
|
|
212
|
-
def __init__(self,
|
|
248
|
+
def __init__(self, tip: str | None = None) -> None:
|
|
213
249
|
"""
|
|
214
|
-
Initialize a new `
|
|
250
|
+
Initialize a new `RootExistsError` instance.
|
|
215
251
|
|
|
216
252
|
Args:
|
|
217
|
-
|
|
218
|
-
|
|
253
|
+
tip (str, optional):
|
|
254
|
+
Optional helpful guidance (defaults to standard tip).
|
|
219
255
|
"""
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"
|
|
223
|
-
f"Parent node names must be unique within the tree."
|
|
256
|
+
super().__init__(
|
|
257
|
+
"A root node has already been defined",
|
|
258
|
+
tip=tip or "Only one @root() decorator is allowed per command",
|
|
224
259
|
)
|
|
225
|
-
super().__init__(message)
|
|
226
260
|
|
|
227
261
|
|
|
228
|
-
class
|
|
229
|
-
"""Exception raised when
|
|
262
|
+
class ParentExistsError(ContextAwareError):
|
|
263
|
+
"""Exception raised when attempting to register duplicate parent names."""
|
|
230
264
|
|
|
231
|
-
def __init__(self) -> None:
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
265
|
+
def __init__(self, name: str, tip: str | None = None) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Initialize a new `ParentExistsError` instance.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name (str):
|
|
271
|
+
The name of the duplicate parent node.
|
|
272
|
+
tip (str | None, optional):
|
|
273
|
+
Optional helpful guidance (defaults to standard tip).
|
|
274
|
+
"""
|
|
275
|
+
super().__init__(
|
|
276
|
+
f"Parent node '{name}' already exists",
|
|
277
|
+
tip=tip or "Parent node names must be unique within a command",
|
|
236
278
|
)
|
|
237
|
-
super().__init__(message)
|
|
238
279
|
|
|
239
280
|
|
|
240
|
-
class
|
|
241
|
-
"""
|
|
281
|
+
class TypeMismatchError(ContextAwareError):
|
|
282
|
+
"""
|
|
283
|
+
Exception raised when a child's process() signature is incompatible
|
|
284
|
+
with the parent's type.
|
|
285
|
+
"""
|
|
242
286
|
|
|
243
|
-
|
|
287
|
+
# pylint: disable=too-many-arguments
|
|
288
|
+
# pylint: disable=too-many-positional-arguments
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
child_name: str,
|
|
292
|
+
parent_name: str,
|
|
293
|
+
parent_type: str,
|
|
294
|
+
supported_types: list[str],
|
|
295
|
+
tip: str | None = None,
|
|
296
|
+
) -> None:
|
|
244
297
|
"""
|
|
245
|
-
Initialize a new `
|
|
298
|
+
Initialize a new `TypeMismatchError` instance.
|
|
246
299
|
|
|
247
300
|
Args:
|
|
248
301
|
child_name (str):
|
|
249
302
|
The name of the child node.
|
|
250
|
-
|
|
251
|
-
The name of the
|
|
303
|
+
parent_name (str):
|
|
304
|
+
The name of the parent node.
|
|
305
|
+
parent_type (str):
|
|
306
|
+
The type of the parent (as string).
|
|
307
|
+
supported_types (list[str]):
|
|
308
|
+
List of supported type names.
|
|
309
|
+
tip (str | None, optional):
|
|
310
|
+
Optional helpful guidance (defaults to supported types).
|
|
252
311
|
"""
|
|
253
312
|
message = (
|
|
254
|
-
f"
|
|
255
|
-
f"'{
|
|
256
|
-
"(no return statement or return None)."
|
|
313
|
+
f"Child '{child_name}' does not support parent '{parent_name}' "
|
|
314
|
+
f"with type '{parent_type}'"
|
|
257
315
|
)
|
|
258
|
-
super().__init__(message)
|
|
259
316
|
|
|
317
|
+
if tip is None:
|
|
318
|
+
types_str = ", ".join(f"<{t}>" for t in supported_types)
|
|
319
|
+
tip = f"Supported types: {types_str}"
|
|
260
320
|
|
|
261
|
-
|
|
321
|
+
super().__init__(message, tip=tip)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class NameExistsError(ContextAwareError):
|
|
262
325
|
"""Exception raised when a name collision is detected."""
|
|
263
326
|
|
|
264
|
-
def __init__(
|
|
265
|
-
self, name: str, type1: str, type2: str, location1: str, location2: str
|
|
266
|
-
) -> None:
|
|
327
|
+
def __init__(self, name: str, tip: str | None = None) -> None:
|
|
267
328
|
"""
|
|
268
|
-
Initialize a new `
|
|
329
|
+
Initialize a new `NameExistsError` instance.
|
|
269
330
|
|
|
270
331
|
Args:
|
|
271
332
|
name (str):
|
|
272
333
|
The conflicting name.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
type2 (str):
|
|
276
|
-
The type of the second node.
|
|
277
|
-
location1 (str):
|
|
278
|
-
Description of where the first node is defined.
|
|
279
|
-
location2 (str):
|
|
280
|
-
Description of where the second node is defined.
|
|
334
|
+
tip (str | None, optional):
|
|
335
|
+
Optional helpful guidance (defaults to standard tip).
|
|
281
336
|
"""
|
|
282
|
-
|
|
283
|
-
f"The name '{name}' is used
|
|
284
|
-
|
|
285
|
-
f"All names (options, arguments, environment variables, and tags) "
|
|
286
|
-
f"must be unique within a command."
|
|
337
|
+
super().__init__(
|
|
338
|
+
f"The name '{name}' is already used",
|
|
339
|
+
tip=tip or "All names must be unique within a command",
|
|
287
340
|
)
|
|
288
|
-
super().__init__(message)
|
|
289
341
|
|
|
290
342
|
|
|
291
|
-
class
|
|
292
|
-
"""
|
|
343
|
+
class UnhandledTypeError(ContextAwareError):
|
|
344
|
+
"""
|
|
345
|
+
Exception raised when a child node doesn't implement a handler
|
|
346
|
+
for the value type.
|
|
347
|
+
"""
|
|
293
348
|
|
|
294
349
|
def __init__(
|
|
295
350
|
self,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
351
|
+
child_name: str,
|
|
352
|
+
value_type: str,
|
|
353
|
+
implemented_handlers: list[str],
|
|
354
|
+
tip: str | None = None,
|
|
300
355
|
) -> None:
|
|
301
356
|
"""
|
|
302
|
-
Initialize a new `
|
|
357
|
+
Initialize a new `UnhandledTypeError` instance.
|
|
303
358
|
|
|
304
359
|
Args:
|
|
305
|
-
|
|
306
|
-
The name of the
|
|
307
|
-
|
|
308
|
-
The
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
360
|
+
child_name (str):
|
|
361
|
+
The name of the child node.
|
|
362
|
+
value_type (str):
|
|
363
|
+
The type of value that couldn't be handled.
|
|
364
|
+
implemented_handlers (list[str]):
|
|
365
|
+
List of handler names that are implemented.
|
|
366
|
+
tip (str, optional):
|
|
367
|
+
Optional helpful guidance (defaults to list of handlers).
|
|
313
368
|
"""
|
|
369
|
+
message = "Child '{}' does not handle values of type '{}'."
|
|
370
|
+
message = message.format(child_name, value_type)
|
|
371
|
+
|
|
372
|
+
if tip is None:
|
|
373
|
+
if implemented_handlers:
|
|
374
|
+
tip = (
|
|
375
|
+
f"Missing handler for '{value_type}', only "
|
|
376
|
+
+ humanize_iterable(
|
|
377
|
+
implemented_handlers,
|
|
378
|
+
wrap="'",
|
|
379
|
+
suffix_singular=" is supported.",
|
|
380
|
+
suffix_plural=" are supported.",
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
else:
|
|
384
|
+
tip = "".join(
|
|
385
|
+
"No handlers are implemented. Override handle_all() "
|
|
386
|
+
"or a specific handler method."
|
|
387
|
+
)
|
|
314
388
|
|
|
315
|
-
|
|
316
|
-
"""Get type name, handling both regular types and UnionType."""
|
|
317
|
-
return getattr(type_obj, "__name__", str(type_obj))
|
|
389
|
+
super().__init__(message, tip=tip)
|
|
318
390
|
|
|
319
|
-
parent_type_name = get_type_name(parent_type) if parent_type else "None"
|
|
320
391
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
prefix_singular="Supported type is ",
|
|
325
|
-
prefix_plural="Supported types are ",
|
|
326
|
-
wrap=("<", ">"),
|
|
327
|
-
)
|
|
392
|
+
class ProcessError(ContextAwareError):
|
|
393
|
+
"""
|
|
394
|
+
Exception raised when user code in `child.process()` raises an exception.
|
|
328
395
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
396
|
+
This error wraps standard Python exceptions (ValueError, TypeError, etc.)
|
|
397
|
+
raised by user code and adds node context for better error messages.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def __init__(self, message: str, tip: str | None = None) -> None:
|
|
401
|
+
"""
|
|
402
|
+
Initialize a new `ProcessError` instance.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
message (str):
|
|
406
|
+
The error message from the wrapped exception.
|
|
407
|
+
tip (str | None, optional):
|
|
408
|
+
Optional helpful guidance for resolving the error.
|
|
409
|
+
"""
|
|
410
|
+
super().__init__(message, tip=tip)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class InvalidHandlerError(ContextAwareError):
|
|
414
|
+
"""Exception raised when a handler returns an invalid value."""
|
|
415
|
+
|
|
416
|
+
def __init__(self, message: str, tip: str | None = None) -> None:
|
|
417
|
+
"""
|
|
418
|
+
Initialize an new `InvalidHandlerError` instance.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
message (str):
|
|
422
|
+
Description of the invalid handler behavior.
|
|
423
|
+
tip (str | None, optional):
|
|
424
|
+
Optional helpful guidance for correcting the handler.
|
|
425
|
+
"""
|
|
426
|
+
super().__init__(message, tip=tip)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class InternalError(ContextAwareError):
|
|
430
|
+
"""
|
|
431
|
+
Exception raised for unexpected errors in framework code.
|
|
432
|
+
|
|
433
|
+
This indicates a bug in `click-extended` or an unreachable code path.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def __init__(self, message: str, tip: str | None = None) -> None:
|
|
437
|
+
"""
|
|
438
|
+
Initialize a new `InternalError` instance.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
message (str):
|
|
442
|
+
Description of the internal error.
|
|
443
|
+
tip (str | None, optional):
|
|
444
|
+
Optional helpful guidance (defaults to bug report message).
|
|
445
|
+
"""
|
|
446
|
+
super().__init__(
|
|
447
|
+
message,
|
|
448
|
+
tip=tip
|
|
449
|
+
or "This is likely a bug in click-extended. Please report it.",
|
|
333
450
|
)
|
|
334
|
-
super().__init__(message)
|
click_extended/types.py
CHANGED
|
@@ -1,33 +1,12 @@
|
|
|
1
|
-
"""Types used in `click_extended
|
|
1
|
+
"""Types used in `click_extended`."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from typing import Any, Callable
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from click_extended.core.context import Context
|
|
6
6
|
|
|
7
|
-
from click_extended.core.argument import Argument
|
|
8
|
-
from click_extended.core.command import Command
|
|
9
|
-
from click_extended.core.env import Env
|
|
10
|
-
from click_extended.core.group import Group
|
|
11
|
-
from click_extended.core.option import Option
|
|
12
|
-
from click_extended.core.tag import Tag
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from click_extended.core._parent_node import ParentNode
|
|
16
|
-
|
|
17
|
-
Tags = dict[str, Tag]
|
|
18
|
-
Siblings = list[str]
|
|
19
|
-
Parent = "ParentNode | Tag"
|
|
20
7
|
Decorator = Callable[[Callable[..., Any]], Callable[..., Any]]
|
|
21
8
|
|
|
22
9
|
__all__ = [
|
|
10
|
+
"Context",
|
|
23
11
|
"Decorator",
|
|
24
|
-
"Parent",
|
|
25
|
-
"Argument",
|
|
26
|
-
"Command",
|
|
27
|
-
"Env",
|
|
28
|
-
"Group",
|
|
29
|
-
"Option",
|
|
30
|
-
"Siblings",
|
|
31
|
-
"Tag",
|
|
32
|
-
"Tags",
|
|
33
12
|
]
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: click_extended
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: An extension to Click with additional features like automatic async support, aliasing and a modular decorator system.
|
|
5
|
+
Author-email: Marcus Fredriksson <marcus@marcusfredriksson.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Marcus Fredriksson
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/marcusfrdk/click-extended
|
|
29
|
+
Project-URL: Repository, https://github.com/marcusfrdk/click-extended
|
|
30
|
+
Project-URL: Issues, https://github.com/marcusfrdk/click-extended/issues
|
|
31
|
+
Keywords: click,cli,command-line,alias,aliasing,command,group,decorator,terminal,console
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
41
|
+
Classifier: Topic :: System :: Shells
|
|
42
|
+
Classifier: Topic :: Utilities
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
License-File: AUTHORS.md
|
|
48
|
+
Requires-Dist: click>=8.3.0
|
|
49
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
50
|
+
Provides-Extra: build
|
|
51
|
+
Requires-Dist: build; extra == "build"
|
|
52
|
+
Requires-Dist: twine; extra == "build"
|
|
53
|
+
Provides-Extra: dev
|
|
54
|
+
Requires-Dist: pytest>=8.4.2; extra == "dev"
|
|
55
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
|
|
56
|
+
Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
|
|
57
|
+
Requires-Dist: mypy>=1.18.2; extra == "dev"
|
|
58
|
+
Requires-Dist: pylint>=3.0.0; extra == "dev"
|
|
59
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
60
|
+
Requires-Dist: black>=25.9.0; extra == "dev"
|
|
61
|
+
Requires-Dist: pre-commit>=4.3.0; extra == "dev"
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
# Click Extended
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+

|
|
70
|
+

|
|
71
|
+

|
|
72
|
+

|
|
73
|
+

|
|
74
|
+

|
|
75
|
+

|
|
76
|
+

|
|
77
|
+

|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
An extension of the [Click](https://github.com/pallets/click) library with additional features like aliasing, asynchronous support, an extended decorator API and more.
|
|
81
|
+
|
|
82
|
+
## Features
|
|
83
|
+
|
|
84
|
+
- **Decorator API**: Extend the functionality your command line by adding custom data sources, data processing pipelines, and more.
|
|
85
|
+
- **Aliasing**: Use aliases for groups and commands to reduce boilerplate and code repetition.
|
|
86
|
+
- **Tags**: Use tags to group several data sources together to apply batch processing.
|
|
87
|
+
- **Async Support**: Native support for declaring functions and methods asynchronous.
|
|
88
|
+
- **Environment Variables**: Built-in support for loading and using environment variables as a data source.
|
|
89
|
+
- **Full Type Support**: Built with type-hinting from the ground up, meaning everything is fully typed.
|
|
90
|
+
- **Improved Errors**: Improved error output like tips, debugging, and more.
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install click-extended
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Requirements
|
|
99
|
+
|
|
100
|
+
- **Python**: 3.10 or higher
|
|
101
|
+
|
|
102
|
+
## Quick Start
|
|
103
|
+
|
|
104
|
+
### Basic Command
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from click_extended import command, argument, option
|
|
108
|
+
|
|
109
|
+
@command(aliases="ping")
|
|
110
|
+
@argument("value")
|
|
111
|
+
@option("--count", "-c", default=1)
|
|
112
|
+
def my_function(value: str, count: int):
|
|
113
|
+
"""This is the help message for my_function."""
|
|
114
|
+
if _ in range(count):
|
|
115
|
+
print(value)
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
my_function()
|
|
119
|
+
|
|
120
|
+
# $ python cli.py "Hello world"
|
|
121
|
+
# Hello world
|
|
122
|
+
|
|
123
|
+
# $ python cli.py "Hello world" --count 3
|
|
124
|
+
# Hello world
|
|
125
|
+
# Hello world
|
|
126
|
+
# Hello world
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Basic Command Line Interface
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from click_extended import group, argument, option
|
|
133
|
+
|
|
134
|
+
@group()
|
|
135
|
+
def my_group():
|
|
136
|
+
"""This is the help message for my_group."""
|
|
137
|
+
print("Running initialization code...")
|
|
138
|
+
|
|
139
|
+
@my_group.command(aliases=["ping", "repeat"])
|
|
140
|
+
@argument("value")
|
|
141
|
+
@option("--count", "-c", default=1)
|
|
142
|
+
def my_function(value: str, count: int):
|
|
143
|
+
"""This is the help message for my_function."""
|
|
144
|
+
if _ in range(count):
|
|
145
|
+
print(value)
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
my_group()
|
|
149
|
+
|
|
150
|
+
# $ python cli.py my_function "Hello world"
|
|
151
|
+
# Running initialization code...
|
|
152
|
+
# Hello world
|
|
153
|
+
|
|
154
|
+
# $ python cli.py my_function "Hello world" --count 3
|
|
155
|
+
# Running initialization code...
|
|
156
|
+
# Hello world
|
|
157
|
+
# Hello world
|
|
158
|
+
# Hello world
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Using Environment Variables
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from click_extended import group, command, env
|
|
165
|
+
|
|
166
|
+
@group()
|
|
167
|
+
def my_group():
|
|
168
|
+
"""This is the help message for my_group."""
|
|
169
|
+
|
|
170
|
+
@my_group.command()
|
|
171
|
+
@env("API_KEY")
|
|
172
|
+
def my_function_1(api_key: str | None):
|
|
173
|
+
"""This is the help message for my_function."""
|
|
174
|
+
print(f"The API key is: {api_key}")
|
|
175
|
+
|
|
176
|
+
@my_group.command()
|
|
177
|
+
@env("API_KEY", required=True)
|
|
178
|
+
def my_function_2(api_key: str):
|
|
179
|
+
"""This is the help message for my_function."""
|
|
180
|
+
print(f"The API key is: {api_key}")
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
my_group()
|
|
184
|
+
|
|
185
|
+
# $ python cli.py my_function_1
|
|
186
|
+
# The API key is: None
|
|
187
|
+
|
|
188
|
+
# $ API_KEY=api-key python cli.py my_function_1
|
|
189
|
+
# The API key is: api-key
|
|
190
|
+
|
|
191
|
+
# $ python cli.py my_function_2
|
|
192
|
+
# ProcessError (my_function_2): Required environment variable 'API_KEY' is not set.
|
|
193
|
+
|
|
194
|
+
# $ API_KEY=api-key python cli.py my_function_2
|
|
195
|
+
# The API key is: api-key
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Custom Children
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from typing import Any
|
|
202
|
+
|
|
203
|
+
from click_extended import group, argument, option
|
|
204
|
+
from click_extended.classes import ChildNode
|
|
205
|
+
from click_extended.types import Context, Decorator
|
|
206
|
+
|
|
207
|
+
class MyCustomChild(ChildNode):
|
|
208
|
+
def handle_primitive(
|
|
209
|
+
self,
|
|
210
|
+
value: str,
|
|
211
|
+
context: Context,
|
|
212
|
+
*args: Any,
|
|
213
|
+
**kwargs: Any,
|
|
214
|
+
) -> str:
|
|
215
|
+
if value == "invalid":
|
|
216
|
+
raise ValueError("The value 'invalid' is not valid")
|
|
217
|
+
|
|
218
|
+
return value.upper()
|
|
219
|
+
|
|
220
|
+
def my_custom_child() -> Decorator:
|
|
221
|
+
"""Checks if the value is invalid and converts it to uppercase."""
|
|
222
|
+
return MyCustomChild.as_decorator()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@group()
|
|
226
|
+
def my_group():
|
|
227
|
+
"""This is the help message for my_group."""
|
|
228
|
+
print("Running initialization code...")
|
|
229
|
+
|
|
230
|
+
@my_group.command(aliases=["ping", "repeat"])
|
|
231
|
+
@argument("value")
|
|
232
|
+
@my_custom_child()
|
|
233
|
+
def my_function(value: str):
|
|
234
|
+
"""This is the help message for my_function."""
|
|
235
|
+
print(f"The value '{value}' should be uppercase.")
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
my_group()
|
|
239
|
+
|
|
240
|
+
# $ python cli.py my_function valid
|
|
241
|
+
# The value 'VALID' should be uppercase.
|
|
242
|
+
|
|
243
|
+
# $ python cli.py my_function invalid
|
|
244
|
+
# ValueError (my_function): "The value 'invalid' is not valid"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Documentation
|
|
248
|
+
|
|
249
|
+
The full documentation is [available here](./docs/README.md) and goes through the full library, from explaining design choices, how to use the library, and much more.
|
|
250
|
+
|
|
251
|
+
## Contributing
|
|
252
|
+
|
|
253
|
+
Contributors are more than welcome to work on this project. Read the [contribution documentation](./CONTRIBUTING.md) to learn more.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
This project is licensed under the MIT License, see the [license file](./LICENSE) for details.
|
|
258
|
+
|
|
259
|
+
## Acknowledgements
|
|
260
|
+
|
|
261
|
+
This project is built on top of the [Click](https://github.com/pallets/click) library.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
click_extended/__init__.py,sha256=WC8SbG5_xnIKiEgdnTNWBR03hbXlJNF0FEz5gZyNfag,423
|
|
2
|
+
click_extended/classes.py,sha256=6qc3cY0iS4WS5jQkKwcQCOYn_jl7t4j8DzykqzRCURw,576
|
|
3
|
+
click_extended/errors.py,sha256=tkvAXs4oUZ8SFgX37KhYrHAlsxmOGrSFLIezCH9NDQI,13897
|
|
4
|
+
click_extended/types.py,sha256=ZYRHjA_wWiLcZ80AYwiVw-kEaHTKSWHKozfPEVay_WE,232
|
|
5
|
+
click_extended-0.4.0.dist-info/licenses/AUTHORS.md,sha256=NkShPinjqtnRDQVRyVnfJuOGM56sejauE3WRoYCcbtw,132
|
|
6
|
+
click_extended-0.4.0.dist-info/licenses/LICENSE,sha256=gjO8hzM4mFSBXFikktaXVSgmXGcre91_GPJ-E_yP56E,1075
|
|
7
|
+
click_extended-0.4.0.dist-info/METADATA,sha256=ffz3zK1urIlLLYS10HMgXAy4_-kkqOLPWXmwmu-roIU,8860
|
|
8
|
+
click_extended-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
click_extended-0.4.0.dist-info/top_level.txt,sha256=2G3bm6tCNv80okRm773jKTk-_z1ElY-seaozZrn_TxA,15
|
|
10
|
+
click_extended-0.4.0.dist-info/RECORD,,
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: click_extended
|
|
3
|
-
Version: 0.3.1
|
|
4
|
-
Summary: An extension to Click with additional features like automatic async support, aliasing and a modular decorator system.
|
|
5
|
-
Author-email: Marcus Fredriksson <marcus@marcusfredriksson.com>
|
|
6
|
-
License: MIT License
|
|
7
|
-
|
|
8
|
-
Copyright (c) 2025 Marcus Fredriksson
|
|
9
|
-
|
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
-
in the Software without restriction, including without limitation the rights
|
|
13
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
-
furnished to do so, subject to the following conditions:
|
|
16
|
-
|
|
17
|
-
The above copyright notice and this permission notice shall be included in all
|
|
18
|
-
copies or substantial portions of the Software.
|
|
19
|
-
|
|
20
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
-
SOFTWARE.
|
|
27
|
-
|
|
28
|
-
Project-URL: Homepage, https://github.com/marcusfrdk/click-extended
|
|
29
|
-
Project-URL: Repository, https://github.com/marcusfrdk/click-extended
|
|
30
|
-
Project-URL: Issues, https://github.com/marcusfrdk/click-extended/issues
|
|
31
|
-
Keywords: click,cli,command-line,alias,aliasing,command,group,decorator,terminal,console
|
|
32
|
-
Classifier: Intended Audience :: Developers
|
|
33
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
-
Classifier: Operating System :: OS Independent
|
|
35
|
-
Classifier: Programming Language :: Python :: 3
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
41
|
-
Classifier: Topic :: System :: Shells
|
|
42
|
-
Classifier: Topic :: Utilities
|
|
43
|
-
Classifier: Typing :: Typed
|
|
44
|
-
Requires-Python: >=3.10
|
|
45
|
-
Description-Content-Type: text/markdown
|
|
46
|
-
License-File: LICENSE
|
|
47
|
-
License-File: AUTHORS.md
|
|
48
|
-
Requires-Dist: click>=8.3.0
|
|
49
|
-
Requires-Dist: python-dotenv>=1.2.1
|
|
50
|
-
Provides-Extra: build
|
|
51
|
-
Requires-Dist: build; extra == "build"
|
|
52
|
-
Requires-Dist: twine; extra == "build"
|
|
53
|
-
Provides-Extra: dev
|
|
54
|
-
Requires-Dist: pytest>=8.4.2; extra == "dev"
|
|
55
|
-
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
|
|
56
|
-
Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
|
|
57
|
-
Requires-Dist: mypy>=1.18.2; extra == "dev"
|
|
58
|
-
Requires-Dist: pylint>=3.0.0; extra == "dev"
|
|
59
|
-
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
60
|
-
Requires-Dist: black>=25.9.0; extra == "dev"
|
|
61
|
-
Requires-Dist: pre-commit>=4.3.0; extra == "dev"
|
|
62
|
-
Dynamic: license-file
|
|
63
|
-
|
|
64
|
-

|
|
65
|
-
|
|
66
|
-
# Click Extended
|
|
67
|
-
|
|
68
|
-

|
|
69
|
-

|
|
70
|
-

|
|
71
|
-

|
|
72
|
-

|
|
73
|
-

|
|
74
|
-

|
|
75
|
-

|
|
76
|
-

|
|
77
|
-
|
|
78
|
-
An extension of the [Click](https://github.com/pallets/click) library with additional features like aliasing, asynchronous support, an extended decorator API and more.
|
|
79
|
-
|
|
80
|
-
## Features
|
|
81
|
-
|
|
82
|
-
- **Aliasing**: Add multiple aliases to a group or command.
|
|
83
|
-
- **Async support**: Automatically run both synchronous and asynchronous functions.
|
|
84
|
-
- **Extensible decorator API**: Extend the Click decorator API with custom decorators like validators, transformers, and more.
|
|
85
|
-
- **Type-hint First**: Built using the type-hinting system to it's full potential.
|
|
86
|
-
- **Environment variables**: Automatically validate and inject environment variables into the function.
|
|
87
|
-
- **Help alias**: The `-h` and `--help` automatically show the help menu unless overridden.
|
|
88
|
-
|
|
89
|
-
## Installation
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
pip install click-extended
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Requirements
|
|
96
|
-
|
|
97
|
-
- **Python**: 3.10 or higher
|
|
98
|
-
|
|
99
|
-
## Quick Start
|
|
100
|
-
|
|
101
|
-
### Basic Command
|
|
102
|
-
|
|
103
|
-
```python
|
|
104
|
-
from click_extended import command, option
|
|
105
|
-
|
|
106
|
-
@command()
|
|
107
|
-
@option("--name", default="World", help="Name to greet")
|
|
108
|
-
@option("--count", type=int, default=1, help="Number of greetings")
|
|
109
|
-
def greet(name: str, count: int):
|
|
110
|
-
"""Greet someone multiple times."""
|
|
111
|
-
for _ in range(count):
|
|
112
|
-
print(f"Hello, {name}!")
|
|
113
|
-
|
|
114
|
-
if __name__ == "__main__":
|
|
115
|
-
greet()
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
$ python app.py --name Alice --count 3
|
|
120
|
-
Hello, Alice!
|
|
121
|
-
Hello, Alice!
|
|
122
|
-
Hello, Alice!
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Async Support
|
|
126
|
-
|
|
127
|
-
```python
|
|
128
|
-
import asyncio
|
|
129
|
-
|
|
130
|
-
from click_extended import command, option
|
|
131
|
-
|
|
132
|
-
@command()
|
|
133
|
-
@option("--url", required=True, help="URL to fetch")
|
|
134
|
-
async def fetch(url: str):
|
|
135
|
-
"""Fetch data from a URL asynchronously."""
|
|
136
|
-
await asyncio.sleep(1)
|
|
137
|
-
print(f"Fetched data from {url}")
|
|
138
|
-
|
|
139
|
-
if __name__ == "__main__":
|
|
140
|
-
fetch()
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Command Aliases
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
from click_extended import command, option
|
|
147
|
-
|
|
148
|
-
@command(aliases=["hi", "hello"])
|
|
149
|
-
@option("--name", default="World")
|
|
150
|
-
def greet(name: str):
|
|
151
|
-
"""Greet someone."""
|
|
152
|
-
print(f"Hello, {name}!")
|
|
153
|
-
|
|
154
|
-
if __name__ == "__main__":
|
|
155
|
-
greet()
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
$ python app.py greet --name Alice
|
|
160
|
-
Hello, Alice!
|
|
161
|
-
$ python app.py hi --name Bob
|
|
162
|
-
Hello, Bob!
|
|
163
|
-
$ python app.py hello --name Charlie
|
|
164
|
-
Hello, Charlie!
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Environment Variables
|
|
168
|
-
|
|
169
|
-
Environment variables are automatically loaded from the `.env` file, but as long as the variable is defined in your system environment, it will work.
|
|
170
|
-
|
|
171
|
-
```txt
|
|
172
|
-
API_TOKEN=secret123
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
```python
|
|
176
|
-
from click_extended import command, option, env
|
|
177
|
-
|
|
178
|
-
@command()
|
|
179
|
-
@option("--token", help="API token")
|
|
180
|
-
@env("API_TOKEN", name="token", required=True)
|
|
181
|
-
def api_call(token: str):
|
|
182
|
-
"""Make an API call with authentication."""
|
|
183
|
-
print(f"Using token: {token[:8]}...")
|
|
184
|
-
|
|
185
|
-
if __name__ == "__main__":
|
|
186
|
-
api_call()
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
```bash
|
|
190
|
-
$ python app.py
|
|
191
|
-
Using token: secret12...
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Custom Validators
|
|
195
|
-
|
|
196
|
-
```python
|
|
197
|
-
from click_extended import command, option, ChildNode, ProcessContext
|
|
198
|
-
from click_extended.errors import ValidationError
|
|
199
|
-
|
|
200
|
-
class IsPositive(ChildNode):
|
|
201
|
-
"""Validate that a number is positive."""
|
|
202
|
-
|
|
203
|
-
def process(self, value: float | int, context: ProcessContext):
|
|
204
|
-
if value <= 0:
|
|
205
|
-
raise ValidationError(f"{value} is not positive")
|
|
206
|
-
|
|
207
|
-
def is_positive(*args, **kwargs):
|
|
208
|
-
"""Validate positive numbers."""
|
|
209
|
-
return IsPositive.as_decorator(*args, **kwargs)
|
|
210
|
-
|
|
211
|
-
@command()
|
|
212
|
-
@option("--count", type=int, required=True)
|
|
213
|
-
@is_positive()
|
|
214
|
-
def process(count: int):
|
|
215
|
-
"""Process a positive number of items."""
|
|
216
|
-
print(f"Processing {count} items")
|
|
217
|
-
|
|
218
|
-
if __name__ == "__main__":
|
|
219
|
-
process()
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
```bash
|
|
223
|
-
$ python app.py --count 5
|
|
224
|
-
Processing 5 items
|
|
225
|
-
$ python app.py --count -1
|
|
226
|
-
Usage: app.py [OPTIONS]
|
|
227
|
-
Try 'app.py --help' for help.
|
|
228
|
-
|
|
229
|
-
Error (--count): -1 is not positive
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## Documentation
|
|
233
|
-
|
|
234
|
-
### Core Concepts
|
|
235
|
-
|
|
236
|
-
- [Commands and Groups](./docs/ROOT_NODE.md) - CLI entry points
|
|
237
|
-
- [Options, Arguments, and Environment Variables](./docs/PARENT_NODE.md) - Parameter sources
|
|
238
|
-
- [Validators and Transformers](./docs/CHILD_NODE.md) - Value processing
|
|
239
|
-
- [Global Nodes](./docs/GLOBAL_NODE.md) - Tree-level operations
|
|
240
|
-
- [Tags](./docs/TAG.md) - Cross-parameter validation
|
|
241
|
-
- [Tree Architecture](./docs/TREE.md) - Internal structure
|
|
242
|
-
|
|
243
|
-
### Guides
|
|
244
|
-
|
|
245
|
-
- [Migrating from Click](./docs/MIGRATING_FROM_CLICK.md) - Upgrade guide
|
|
246
|
-
|
|
247
|
-
## Contributing
|
|
248
|
-
|
|
249
|
-
Contributors are more than welcome to work on this project. Read the [contribution documentation](./CONTRIBUTING.md) to learn more.
|
|
250
|
-
|
|
251
|
-
## License
|
|
252
|
-
|
|
253
|
-
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|
|
254
|
-
|
|
255
|
-
## Acknowledgements
|
|
256
|
-
|
|
257
|
-
This project is built on top of the [Click](https://github.com/pallets/click) library.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
click_extended/__init__.py,sha256=wBUV7-CmgDBKSnDj3_MbYCjHv2zAaATQ795Y9Xs-0NE,858
|
|
2
|
-
click_extended/errors.py,sha256=ITtVGZ1GosrrGnUbUuJQueoJqFCtJvj83dKJrQqkCmU,10500
|
|
3
|
-
click_extended/types.py,sha256=tiz-toTYABwFTsxUMIDRhPiq3H_V3nhrAcV8VGEKyzA,786
|
|
4
|
-
click_extended-0.3.1.dist-info/licenses/AUTHORS.md,sha256=NkShPinjqtnRDQVRyVnfJuOGM56sejauE3WRoYCcbtw,132
|
|
5
|
-
click_extended-0.3.1.dist-info/licenses/LICENSE,sha256=gjO8hzM4mFSBXFikktaXVSgmXGcre91_GPJ-E_yP56E,1075
|
|
6
|
-
click_extended-0.3.1.dist-info/METADATA,sha256=ONgNHY5XXy9sOrlf0NH2ASwQcPgeagLUmYxCyKxzauY,8295
|
|
7
|
-
click_extended-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
click_extended-0.3.1.dist-info/top_level.txt,sha256=2G3bm6tCNv80okRm773jKTk-_z1ElY-seaozZrn_TxA,15
|
|
9
|
-
click_extended-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|