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.
@@ -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
- """Exceptions used in the `click_extended` library."""
1
+ """Error module for the `click_extended` library."""
2
2
 
3
- # pylint: disable=too-many-arguments
4
- # pylint: disable=too-many-positional-arguments
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.format import format_list
9
+ from click_extended.utils.humanize import humanize_iterable
15
10
 
16
11
 
17
12
  class ClickExtendedError(Exception):
18
- """Base exception for exceptions defined in the `click_extended` library."""
19
-
13
+ """Base exception for all click-extended errors."""
20
14
 
21
- # Catchable errors
22
- class CatchableError(ClickExtendedError):
23
- """Base exception for exceptions raised inside a child node.
15
+ def __init__(self, message: str, tip: str | None = None) -> None:
16
+ """
17
+ Initialize a ClickExtendedError.
24
18
 
25
- These exceptions are caught by the framework and reformatted with
26
- parameter context before being displayed to the user.
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 __init__(self, message: str) -> None:
29
+ def show(self, file: Any = None) -> None:
30
30
  """
31
- Initialize a CatchableError.
31
+ Display the error message.
32
+
33
+ Subclasses should override this to provide custom formatting.
32
34
 
33
35
  Args:
34
- message: The error message describing what went wrong.
36
+ file (Any, optional):
37
+ The file to write to (defaults to sys.stderr).
35
38
  """
36
- super().__init__(message)
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
- class ChildNodeProcessError(CatchableError):
47
+
48
+ class ContextAwareError(ClickExtendedError):
40
49
  """
41
- Base class for exceptions which must be raised inside
42
- the `process()` method of a `ChildNode`.
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
- def __init__(self, message: str, **kwargs: str) -> None:
58
+ context: click.Context | None
59
+
60
+ def __init__(self, message: str, tip: str | None = None) -> None:
46
61
  """
47
- Initialize a new `ChildNodeProcessError` instance.
62
+ Initialize a new `ContextAwareError` instance.
48
63
 
49
64
  Args:
50
65
  message (str):
51
- The error message. If child_name is found, it will be
52
- prefixed automatically. Subclasses can access child_name
53
- via self.child_name to format custom messages.
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
- Raises:
56
- RuntimeError:
57
- If raised outside a ChildNode context.
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.child_name: str | None = None
60
- frame = inspect.currentframe()
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
- formatted_message = message.format(name=self.child_name, **kwargs)
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
- class UnhandledValueError(ChildNodeProcessError):
91
- """Exception raised when a value in the `process()` method is unexpected."""
101
+ return "unknown"
92
102
 
93
- def __init__(self, value: Any) -> None:
103
+ def show(self, file: Any = None) -> None:
94
104
  """
95
- Initialize a new `UnhandledValueError` instance.
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
- value (Any):
99
- The unexpected value.
115
+ file (Any, optional):
116
+ The file to write to (defaults to `sys.stderr`).
100
117
  """
101
- message = "Received unexpected value for '{name}' of type '{type}'"
102
- super().__init__(message, type=type(value).__name__)
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
- class ValidationError(CatchableError):
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
- class TransformError(CatchableError):
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
- class UnknownError(ChildNodeProcessError):
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 ParameterError(ClickException):
121
- """Exception raised when parameter validation or transformation fails.
144
+ class MissingValueError(ContextAwareError):
145
+ """
146
+ Exception raised when a value is missing.
122
147
 
123
- This exception is raised by the framework after catching a CatchableError
124
- and adding parameter context information.
148
+ This exception is context-aware and can only be raised during `phase 3` or
149
+ `phase 4`.
125
150
  """
126
151
 
127
- exit_code = 2
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 __init__(
130
- self,
131
- message: str,
132
- param_hint: str | None = None,
133
- ctx: click.Context | None = None,
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
- Args:
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
- def format_message(self) -> str:
151
- """Format the error message with parameter context."""
152
- if self.param_hint:
153
- return f"({self.param_hint}): {self.message}"
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
- def show(self, file: IO[Any] | None = None) -> None:
157
- """Display the error with usage information (like Click does)."""
158
- if file is None:
159
- file = get_text_stderr()
171
+ return (
172
+ "Provide a value or set the default parameter to make it optional."
173
+ )
160
174
 
161
- color = None
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 self.ctx is not None:
164
- color = self.ctx.color
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
- if self.ctx.command.get_help_option(self.ctx) is not None:
169
- hint = (
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
- echo("", file=file)
206
+ def __init__(self, tip: str | None = None) -> None:
207
+ """
208
+ Initialize a new `NoRootError` instance.
176
209
 
177
- echo(f"Error {self.format_message()}", file=file, color=color)
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
- # Node errors
181
- class NoParentError(ClickExtendedError):
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, name: str) -> None:
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
- name (str):
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
- message = (
194
- f"Failed to register the child node '{name}' as no parent is "
195
- "defined. Ensure a parent node is registered before registering a "
196
- "child node."
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 ParentNodeExistsError(ClickExtendedError):
210
- """Exception raised when a parent node already exists with the same name."""
245
+ class RootExistsError(ContextAwareError):
246
+ """Exception raised when attempting to define multiple root nodes."""
211
247
 
212
- def __init__(self, name: str) -> None:
248
+ def __init__(self, tip: str | None = None) -> None:
213
249
  """
214
- Initialize a new `ParentNodeExistsError` instance.
250
+ Initialize a new `RootExistsError` instance.
215
251
 
216
252
  Args:
217
- name (str):
218
- The name of the parent node.
253
+ tip (str, optional):
254
+ Optional helpful guidance (defaults to standard tip).
219
255
  """
220
- message = (
221
- f"Cannot register parent node '{name}' as a parent node with this "
222
- "name already exists. "
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 RootNodeExistsError(ClickExtendedError):
229
- """Exception raised when a root node already exists for the tree."""
262
+ class ParentExistsError(ContextAwareError):
263
+ """Exception raised when attempting to register duplicate parent names."""
230
264
 
231
- def __init__(self) -> None:
232
- """Initialize a new `RootNodeExistsError` instance."""
233
- message = (
234
- "Cannot register root node as a root node has already been "
235
- "defined. Only one root node is allowed per tree instance."
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 InvalidChildOnTagError(ClickExtendedError):
241
- """Exception raised when a transformation child is attached to a tag."""
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
- def __init__(self, child_name: str, tag_name: str) -> None:
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 `InvalidChildOnTagError` instance.
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
- tag_name (str):
251
- The name of the tag.
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"Cannot attach transformation child '{child_name}' to tag "
255
- f"'{tag_name}'. Tags can only have validation-only children "
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
- class DuplicateNameError(ClickExtendedError):
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 `DuplicateNameError` instance.
329
+ Initialize a new `NameExistsError` instance.
269
330
 
270
331
  Args:
271
332
  name (str):
272
333
  The conflicting name.
273
- type1 (str):
274
- The type of the first node (e.g., "option", "tag").
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
- message = (
283
- f"The name '{name}' is used by both "
284
- f"{type1} {location1} and {type2} {location2}. "
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 TypeMismatchError(ClickExtendedError):
292
- """Exception raised when a child node doesn't support the parent's type."""
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
- name: str,
297
- parent_name: str,
298
- parent_type: type | None,
299
- supported_types: list[type],
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 `TypeMismatchError` instance.
357
+ Initialize a new `UnhandledTypeError` instance.
303
358
 
304
359
  Args:
305
- name (str):
306
- The name of the decorator.
307
- parent_name (str):
308
- The name of the parent node.
309
- parent_type (type | None):
310
- The actual type of the parent.
311
- supported_types (list[type]):
312
- List of types supported by the child node.
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
- def get_type_name(type_obj: type) -> str:
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
- type_names = [get_type_name(t) for t in supported_types]
322
- formatted_types = format_list(
323
- type_names,
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
- message = (
330
- f"Decorator '{name}' does not support "
331
- f"parent '{parent_name}' with type '{parent_type_name}'. "
332
- f"{formatted_types}"
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` which can be useful for users."""
1
+ """Types used in `click_extended`."""
2
2
 
3
- # pylint: disable=invalid-name
3
+ from typing import Any, Callable
4
4
 
5
- from typing import TYPE_CHECKING, Any, Callable
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
+ ![Banner](./assets/click-extended-banner.png)
65
+
66
+ # Click Extended
67
+
68
+ ![top language](https://img.shields.io/github/languages/top/marcusfrdk/click-extended)
69
+ ![code size](https://img.shields.io/github/languages/code-size/marcusfrdk/click-extended)
70
+ ![last commit](https://img.shields.io/github/last-commit/marcusfrdk/click-extended)
71
+ ![tests](https://github.com/marcusfrdk/click-extended/actions/workflows/tests.yml/badge.svg)
72
+ ![release](https://github.com/marcusfrdk/click-extended/actions/workflows/release.yml/badge.svg)
73
+ ![issues](https://img.shields.io/github/issues/marcusfrdk/click-extended)
74
+ ![contributors](https://img.shields.io/github/contributors/marcusfrdk/click-extended)
75
+ ![pypi](https://img.shields.io/pypi/v/click-extended)
76
+ ![license](https://img.shields.io/github/license/marcusfrdk/click-extended)
77
+ ![downloads](https://static.pepy.tech/badge/click-extended)
78
+ ![monthly downloads](https://static.pepy.tech/badge/click-extended/month)
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
- ![Banner](./assets/click-extended-banner.png)
65
-
66
- # Click Extended
67
-
68
- ![top language](https://img.shields.io/github/languages/top/marcusfrdk/click-extended)
69
- ![code size](https://img.shields.io/github/languages/code-size/marcusfrdk/click-extended)
70
- ![last commit](https://img.shields.io/github/last-commit/marcusfrdk/click-extended)
71
- ![issues](https://img.shields.io/github/issues/marcusfrdk/click-extended)
72
- ![contributors](https://img.shields.io/github/contributors/marcusfrdk/click-extended)
73
- ![PyPI](https://img.shields.io/pypi/v/click-extended)
74
- ![License](https://img.shields.io/github/license/marcusfrdk/click-extended)
75
- ![Downloads](https://static.pepy.tech/badge/click-extended)
76
- ![Monthly Downloads](https://static.pepy.tech/badge/click-extended/month)
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,,