click-extended 1.0.0__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. click_extended/__init__.py +2 -0
  2. click_extended/core/__init__.py +10 -0
  3. click_extended/core/decorators/__init__.py +21 -0
  4. click_extended/core/decorators/argument.py +227 -0
  5. click_extended/core/decorators/command.py +93 -0
  6. click_extended/core/decorators/context.py +56 -0
  7. click_extended/core/decorators/env.py +155 -0
  8. click_extended/core/decorators/group.py +96 -0
  9. click_extended/core/decorators/option.py +347 -0
  10. click_extended/core/decorators/prompt.py +69 -0
  11. click_extended/core/decorators/selection.py +155 -0
  12. click_extended/core/decorators/tag.py +109 -0
  13. click_extended/core/nodes/__init__.py +21 -0
  14. click_extended/core/nodes/_root_node.py +1012 -0
  15. click_extended/core/nodes/argument_node.py +165 -0
  16. click_extended/core/nodes/child_node.py +555 -0
  17. click_extended/core/nodes/child_validation_node.py +100 -0
  18. click_extended/core/nodes/node.py +55 -0
  19. click_extended/core/nodes/option_node.py +205 -0
  20. click_extended/core/nodes/parent_node.py +220 -0
  21. click_extended/core/nodes/validation_node.py +124 -0
  22. click_extended/core/other/__init__.py +7 -0
  23. click_extended/core/other/_click_command.py +60 -0
  24. click_extended/core/other/_click_group.py +246 -0
  25. click_extended/core/other/_tree.py +491 -0
  26. click_extended/core/other/context.py +496 -0
  27. click_extended/decorators/__init__.py +29 -0
  28. click_extended/decorators/check/__init__.py +57 -0
  29. click_extended/decorators/check/conflicts.py +149 -0
  30. click_extended/decorators/check/contains.py +69 -0
  31. click_extended/decorators/check/dependencies.py +115 -0
  32. click_extended/decorators/check/divisible_by.py +48 -0
  33. click_extended/decorators/check/ends_with.py +85 -0
  34. click_extended/decorators/check/exclusive.py +75 -0
  35. click_extended/decorators/check/falsy.py +37 -0
  36. click_extended/decorators/check/is_email.py +43 -0
  37. click_extended/decorators/check/is_hex_color.py +41 -0
  38. click_extended/decorators/check/is_hostname.py +47 -0
  39. click_extended/decorators/check/is_ipv4.py +46 -0
  40. click_extended/decorators/check/is_ipv6.py +46 -0
  41. click_extended/decorators/check/is_json.py +40 -0
  42. click_extended/decorators/check/is_mac_address.py +40 -0
  43. click_extended/decorators/check/is_negative.py +37 -0
  44. click_extended/decorators/check/is_non_zero.py +37 -0
  45. click_extended/decorators/check/is_port.py +39 -0
  46. click_extended/decorators/check/is_positive.py +37 -0
  47. click_extended/decorators/check/is_url.py +75 -0
  48. click_extended/decorators/check/is_uuid.py +40 -0
  49. click_extended/decorators/check/length.py +68 -0
  50. click_extended/decorators/check/not_empty.py +49 -0
  51. click_extended/decorators/check/regex.py +47 -0
  52. click_extended/decorators/check/requires.py +190 -0
  53. click_extended/decorators/check/starts_with.py +87 -0
  54. click_extended/decorators/check/truthy.py +37 -0
  55. click_extended/decorators/compare/__init__.py +15 -0
  56. click_extended/decorators/compare/at_least.py +57 -0
  57. click_extended/decorators/compare/at_most.py +57 -0
  58. click_extended/decorators/compare/between.py +119 -0
  59. click_extended/decorators/compare/greater_than.py +183 -0
  60. click_extended/decorators/compare/less_than.py +183 -0
  61. click_extended/decorators/convert/__init__.py +31 -0
  62. click_extended/decorators/convert/convert_angle.py +94 -0
  63. click_extended/decorators/convert/convert_area.py +123 -0
  64. click_extended/decorators/convert/convert_bits.py +211 -0
  65. click_extended/decorators/convert/convert_distance.py +154 -0
  66. click_extended/decorators/convert/convert_energy.py +155 -0
  67. click_extended/decorators/convert/convert_power.py +128 -0
  68. click_extended/decorators/convert/convert_pressure.py +131 -0
  69. click_extended/decorators/convert/convert_speed.py +122 -0
  70. click_extended/decorators/convert/convert_temperature.py +89 -0
  71. click_extended/decorators/convert/convert_time.py +108 -0
  72. click_extended/decorators/convert/convert_volume.py +218 -0
  73. click_extended/decorators/convert/convert_weight.py +158 -0
  74. click_extended/decorators/load/__init__.py +13 -0
  75. click_extended/decorators/load/load_csv.py +117 -0
  76. click_extended/decorators/load/load_json.py +61 -0
  77. click_extended/decorators/load/load_toml.py +47 -0
  78. click_extended/decorators/load/load_yaml.py +72 -0
  79. click_extended/decorators/math/__init__.py +37 -0
  80. click_extended/decorators/math/absolute.py +35 -0
  81. click_extended/decorators/math/add.py +48 -0
  82. click_extended/decorators/math/ceil.py +36 -0
  83. click_extended/decorators/math/clamp.py +51 -0
  84. click_extended/decorators/math/divide.py +42 -0
  85. click_extended/decorators/math/floor.py +36 -0
  86. click_extended/decorators/math/maximum.py +39 -0
  87. click_extended/decorators/math/minimum.py +39 -0
  88. click_extended/decorators/math/modulo.py +39 -0
  89. click_extended/decorators/math/multiply.py +51 -0
  90. click_extended/decorators/math/normalize.py +76 -0
  91. click_extended/decorators/math/power.py +39 -0
  92. click_extended/decorators/math/rounded.py +39 -0
  93. click_extended/decorators/math/sqrt.py +39 -0
  94. click_extended/decorators/math/subtract.py +39 -0
  95. click_extended/decorators/math/to_percent.py +63 -0
  96. click_extended/decorators/misc/__init__.py +17 -0
  97. click_extended/decorators/misc/choice.py +139 -0
  98. click_extended/decorators/misc/confirm_if.py +147 -0
  99. click_extended/decorators/misc/default.py +95 -0
  100. click_extended/decorators/misc/deprecated.py +131 -0
  101. click_extended/decorators/misc/experimental.py +79 -0
  102. click_extended/decorators/misc/now.py +42 -0
  103. click_extended/decorators/random/__init__.py +21 -0
  104. click_extended/decorators/random/random_bool.py +49 -0
  105. click_extended/decorators/random/random_choice.py +63 -0
  106. click_extended/decorators/random/random_datetime.py +140 -0
  107. click_extended/decorators/random/random_float.py +62 -0
  108. click_extended/decorators/random/random_integer.py +56 -0
  109. click_extended/decorators/random/random_prime.py +196 -0
  110. click_extended/decorators/random/random_string.py +77 -0
  111. click_extended/decorators/random/random_uuid.py +119 -0
  112. click_extended/decorators/transform/__init__.py +71 -0
  113. click_extended/decorators/transform/add_prefix.py +58 -0
  114. click_extended/decorators/transform/add_suffix.py +58 -0
  115. click_extended/decorators/transform/apply.py +35 -0
  116. click_extended/decorators/transform/basename.py +44 -0
  117. click_extended/decorators/transform/dirname.py +44 -0
  118. click_extended/decorators/transform/expand_vars.py +36 -0
  119. click_extended/decorators/transform/remove_prefix.py +57 -0
  120. click_extended/decorators/transform/remove_suffix.py +57 -0
  121. click_extended/decorators/transform/replace.py +46 -0
  122. click_extended/decorators/transform/slugify.py +45 -0
  123. click_extended/decorators/transform/split.py +43 -0
  124. click_extended/decorators/transform/strip.py +148 -0
  125. click_extended/decorators/transform/to_case.py +216 -0
  126. click_extended/decorators/transform/to_date.py +75 -0
  127. click_extended/decorators/transform/to_datetime.py +83 -0
  128. click_extended/decorators/transform/to_path.py +274 -0
  129. click_extended/decorators/transform/to_time.py +77 -0
  130. click_extended/decorators/transform/to_timestamp.py +114 -0
  131. click_extended/decorators/transform/truncate.py +47 -0
  132. click_extended/utils/__init__.py +13 -0
  133. click_extended/utils/casing.py +169 -0
  134. click_extended/utils/checks.py +48 -0
  135. click_extended/utils/dispatch.py +1016 -0
  136. click_extended/utils/format.py +101 -0
  137. click_extended/utils/humanize.py +209 -0
  138. click_extended/utils/naming.py +238 -0
  139. click_extended/utils/process.py +294 -0
  140. click_extended/utils/selection.py +267 -0
  141. click_extended/utils/time.py +46 -0
  142. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/METADATA +2 -1
  143. click_extended-1.0.2.dist-info/RECORD +150 -0
  144. click_extended-1.0.0.dist-info/RECORD +0 -10
  145. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/WHEEL +0 -0
  146. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/AUTHORS.md +0 -0
  147. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/LICENSE +0 -0
  148. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  from click_extended.core.decorators.argument import argument
4
4
  from click_extended.core.decorators.command import command
5
+ from click_extended.core.decorators.context import context
5
6
  from click_extended.core.decorators.env import env
6
7
  from click_extended.core.decorators.group import group
7
8
  from click_extended.core.decorators.option import option
@@ -12,6 +13,7 @@ from click_extended.core.decorators.tag import tag
12
13
  __all__ = [
13
14
  "argument",
14
15
  "command",
16
+ "context",
15
17
  "env",
16
18
  "group",
17
19
  "option",
@@ -0,0 +1,10 @@
1
+ """Initialization file for the 'click_extended.core' module."""
2
+
3
+ from click_extended.core.decorators import *
4
+ from click_extended.core.decorators import __all__ as decorators_all
5
+ from click_extended.core.nodes import *
6
+ from click_extended.core.nodes import __all__ as nodes_all
7
+ from click_extended.core.other import *
8
+ from click_extended.core.other import __all__ as other_all
9
+
10
+ __all__ = [*decorators_all, *nodes_all, *other_all] # type: ignore
@@ -0,0 +1,21 @@
1
+ """Initialization file for the `click_extended.core.decorators` module."""
2
+
3
+ from click_extended.core.decorators.argument import Argument
4
+ from click_extended.core.decorators.command import Command
5
+ from click_extended.core.decorators.env import Env
6
+ from click_extended.core.decorators.group import Group
7
+ from click_extended.core.decorators.option import Option
8
+ from click_extended.core.decorators.prompt import Prompt
9
+ from click_extended.core.decorators.selection import Selection
10
+ from click_extended.core.decorators.tag import Tag
11
+
12
+ __all__ = [
13
+ "Argument",
14
+ "Command",
15
+ "Env",
16
+ "Group",
17
+ "Option",
18
+ "Prompt",
19
+ "Selection",
20
+ "Tag",
21
+ ]
@@ -0,0 +1,227 @@
1
+ """Class to support arguments in the command line interface."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+ # pylint: disable=too-many-positional-arguments
5
+ # pylint: disable=redefined-builtin
6
+
7
+ from builtins import type as builtins_type
8
+ from typing import Any, Type, cast
9
+
10
+ from click_extended.core.nodes.argument_node import ArgumentNode
11
+ from click_extended.core.other.context import Context
12
+ from click_extended.types import Decorator
13
+ from click_extended.utils.casing import Casing
14
+ from click_extended.utils.humanize import humanize_type
15
+ from click_extended.utils.naming import validate_name
16
+
17
+ _MISSING = object()
18
+ SUPPORTED_TYPES = (str, int, float, bool)
19
+
20
+
21
+ class Argument(ArgumentNode):
22
+ """`ArgumentNode` that represents a Click argument."""
23
+
24
+ def __init__(
25
+ self,
26
+ name: str,
27
+ param: str | None = None,
28
+ nargs: int = 1,
29
+ type: Type[Any] | Any = None,
30
+ help: str | None = None,
31
+ required: bool = True,
32
+ default: Any = _MISSING,
33
+ tags: str | list[str] | None = None,
34
+ **kwargs: Any,
35
+ ):
36
+ """
37
+ Initialize a new `Argument` instance.
38
+
39
+ Args:
40
+ name (str):
41
+ The argument name in snake_case.
42
+ Examples: "filename", "input_file"
43
+ param (str, optional):
44
+ Custom parameter name for the function.
45
+ If not provided, uses the name directly.
46
+ nargs (int):
47
+ Number of arguments to accept. Use `-1` for unlimited.
48
+ Defaults to `1`.
49
+ type (Any, optional):
50
+ The type to convert the value to (`int`, `str`, `float`, etc.).
51
+ help (str, optional):
52
+ Help text for this argument.
53
+ required (bool):
54
+ Whether this argument is required. Defaults to `True` unless
55
+ `default` is provided, which makes it optional automatically.
56
+ default (Any):
57
+ Default value if not provided. When set, automatically makes
58
+ the argument optional (`required=False`). Defaults to `None`.
59
+ tags (str | list[str], optional):
60
+ Tag(s) to associate with this argument for grouping.
61
+ **kwargs (Any):
62
+ Additional keyword arguments.
63
+
64
+ Raises:
65
+ ValueError: If both `default` is provided and `required=True` is
66
+ explicitly set (detected via kwargs inspection in decorator).
67
+ """
68
+ validate_name(name, "argument name")
69
+
70
+ param_name = param if param is not None else name
71
+
72
+ validate_name(param_name, "parameter name")
73
+
74
+ if default is not _MISSING and required is True:
75
+ required = False
76
+
77
+ if default is _MISSING:
78
+ default = None
79
+
80
+ if type is None:
81
+ if default is not None:
82
+ type = cast(Type[Any], builtins_type(default)) # type: ignore
83
+ else:
84
+ type = str
85
+
86
+ if type not in SUPPORTED_TYPES:
87
+ types = humanize_type(
88
+ type.__name__ if hasattr(type, "__name__") else type
89
+ )
90
+ raise ValueError(
91
+ f"Argument '{name}' has unsupported type '{types}'. "
92
+ "Only basic primitives are supported: str, int, float, bool. "
93
+ "For complex types, use child decorators (e.g., @to_path, "
94
+ "@to_datetime, ..)."
95
+ )
96
+
97
+ super().__init__(
98
+ name=name,
99
+ param=param if param is not None else name,
100
+ nargs=nargs,
101
+ type=type,
102
+ help=help,
103
+ required=required,
104
+ default=default,
105
+ tags=tags,
106
+ )
107
+ self.extra_kwargs = kwargs
108
+
109
+ def get_display_name(self) -> str:
110
+ """
111
+ Get a formatted display name for error messages.
112
+
113
+ Returns:
114
+ str:
115
+ The argument name in SCREAMING_SNAKE_CASE.
116
+ """
117
+ return Casing.to_screaming_snake_case(self.name)
118
+
119
+ def load(
120
+ self,
121
+ value: str | int | float | bool | None,
122
+ context: Context,
123
+ *args: Any,
124
+ **kwargs: Any,
125
+ ) -> Any:
126
+ """
127
+ Load and return the CLI argument value.
128
+
129
+ Args:
130
+ value (str | int | float | bool | None):
131
+ The parsed CLI argument value from Click.
132
+ context (Context):
133
+ The current context instance.
134
+ *args (Any):
135
+ Optional positional arguments.
136
+ **kwargs (Any):
137
+ Optional keyword arguments.
138
+
139
+ Returns:
140
+ Any:
141
+ The argument value to inject into the function.
142
+ """
143
+ return value
144
+
145
+
146
+ def argument(
147
+ name: str,
148
+ param: str | None = None,
149
+ nargs: int = 1,
150
+ type: Type[str | int | float | bool] | None = None,
151
+ help: str | None = None,
152
+ required: bool = True,
153
+ default: Any = _MISSING,
154
+ tags: str | list[str] | None = None,
155
+ **kwargs: Any,
156
+ ) -> Decorator:
157
+ """
158
+ A `ParentNode` decorator to create a Click argument with value injection.
159
+
160
+ Args:
161
+ name (str):
162
+ The argument name in snake_case.
163
+ Examples: "filename", "input_file"
164
+ param (str, optional):
165
+ Custom parameter name for the function.
166
+ If not provided, uses the name directly.
167
+ nargs (int):
168
+ Number of arguments to accept. Use `-1` for unlimited.
169
+ Defaults to `1`.
170
+ type (Type[str | int | float | bool] | None, optional):
171
+ The type to convert the value to.
172
+ help (str, optional):
173
+ Help text for this argument.
174
+ required (bool):
175
+ Whether this argument is required. Defaults to `True` unless
176
+ `default` is provided, which automatically makes it optional.
177
+ default (Any):
178
+ Default value if not provided. When set, automatically makes
179
+ the argument optional (`required=False`). Defaults to `None`.
180
+ tags (str | list[str], optional):
181
+ Tag(s) to associate with this argument for grouping.
182
+ **kwargs (Any):
183
+ Additional Click argument parameters.
184
+
185
+ Returns:
186
+ Decorator:
187
+ A decorator function that registers the argument parent node.
188
+
189
+ Examples:
190
+
191
+ ```python
192
+ @argument("filename")
193
+ def my_func(filename):
194
+ print(f"File: {filename}")
195
+ ```
196
+
197
+ ```python
198
+ @argument("files", nargs=-1, help="Files to process")
199
+ def my_func(files):
200
+ for file in files:
201
+ print(f"Processing: {file}")
202
+ ```
203
+
204
+ ```python
205
+ @argument("port", type=int, default=8080)
206
+ def my_func(port):
207
+ print(f"Port: {port}")
208
+ ```
209
+
210
+ ```python
211
+ # Custom parameter name
212
+ @argument("input_file", param="infile")
213
+ def my_func(infile): # param: infile, CLI: INPUT_FILE
214
+ print(f"Input: {infile}")
215
+ ```
216
+ """
217
+ return Argument.as_decorator(
218
+ name=name,
219
+ param=param,
220
+ nargs=nargs,
221
+ type=type,
222
+ help=help,
223
+ required=required,
224
+ default=default,
225
+ tags=tags,
226
+ **kwargs,
227
+ )
@@ -0,0 +1,93 @@
1
+ """Command implementation for the `click_extended` library."""
2
+
3
+ # pylint: disable=redefined-builtin
4
+
5
+ from typing import Any, Callable
6
+
7
+ import click
8
+
9
+ from click_extended.core.nodes._root_node import RootNode
10
+ from click_extended.core.other._click_command import ClickCommand
11
+
12
+
13
+ class Command(RootNode):
14
+ """Command implementation for the `click_extended` library."""
15
+
16
+ @classmethod
17
+ def _get_click_decorator(cls) -> Callable[..., Any]:
18
+ """Return the click.command decorator (no longer used)."""
19
+ return click.command
20
+
21
+ @classmethod
22
+ def _get_click_cls(cls) -> type[click.Command]: # type: ignore[override]
23
+ """Return the ClickCommand class."""
24
+ return ClickCommand
25
+
26
+ @classmethod
27
+ def wrap(
28
+ cls,
29
+ wrapped_func: Callable[..., Any],
30
+ name: str,
31
+ instance: RootNode,
32
+ **kwargs: Any,
33
+ ) -> ClickCommand:
34
+ """Override to return proper ClickCommand type."""
35
+ return super().wrap(
36
+ wrapped_func,
37
+ name,
38
+ instance,
39
+ **kwargs,
40
+ ) # type: ignore[return-value]
41
+
42
+ @classmethod
43
+ def as_decorator(
44
+ cls, name: str | None = None, /, **kwargs: Any
45
+ ) -> Callable[[Callable[..., Any]], ClickCommand]:
46
+ """Override to return proper ClickCommand type."""
47
+ return super().as_decorator(
48
+ name,
49
+ **kwargs,
50
+ ) # type: ignore[return-value]
51
+
52
+
53
+ def command(
54
+ name: str | None = None,
55
+ *,
56
+ aliases: str | list[str] | None = None,
57
+ help: str | None = None,
58
+ **kwargs: Any,
59
+ ) -> Callable[[Callable[..., Any]], ClickCommand]:
60
+ """
61
+ A `ParentNode` decorator to create a click command with value injection
62
+ from parent nodes.
63
+
64
+ Args:
65
+ name (str, optional):
66
+ The name of the command. If `None`, uses the
67
+ decorated function's name.
68
+ aliases (str | list[str], optional):
69
+ Alternative name(s) for the command. Can be a single
70
+ string or a list of strings.
71
+ help (str, optional):
72
+ The help message for the command. If not provided,
73
+ uses the first line of the function's docstring.
74
+ **kwargs (Any):
75
+ Additional arguments to pass to `click.Command`.
76
+
77
+ Returns:
78
+ Callable:
79
+ A decorator function that returns a Click command.
80
+ """
81
+ if aliases is not None:
82
+ kwargs["aliases"] = aliases
83
+ if help is not None:
84
+ kwargs["help"] = help
85
+
86
+ def decorator(func: Callable[..., Any]) -> ClickCommand:
87
+ if help is None and func.__doc__:
88
+ first_line = func.__doc__.strip().split("\n")[0].strip()
89
+ if first_line:
90
+ kwargs["help"] = first_line
91
+ return Command.as_decorator(name, **kwargs)(func)
92
+
93
+ return decorator
@@ -0,0 +1,56 @@
1
+ """`ParentNode` to inject the context into the function."""
2
+
3
+ # pylint: disable=redefined-builtin
4
+ # pylint: disable=redefined-outer-name
5
+
6
+ from typing import Any
7
+
8
+ from click_extended.core.nodes.parent_node import ParentNode
9
+ from click_extended.core.other.context import Context as Ctx
10
+ from click_extended.types import Decorator
11
+
12
+
13
+ class Context(ParentNode):
14
+ """`ParentNode` to inject the context into the function."""
15
+
16
+ def load(self, context: Ctx, *args: Any, **kwargs: Any) -> Ctx:
17
+ return context
18
+
19
+
20
+ def context(
21
+ name: str = "ctx",
22
+ param: str | None = None,
23
+ help: str | None = None,
24
+ tags: str | list[str] | None = None,
25
+ **kwargs: Any,
26
+ ) -> Decorator:
27
+ """
28
+ A `ParentNode` to inject the context into the function.
29
+
30
+ Type: `ParentNode`
31
+
32
+ Args:
33
+ name (str, optional):
34
+ Internal node name (must be snake_case). Defaults
35
+ to "ctx".
36
+ param (str, optional):
37
+ The parameter name to inject into the function.
38
+ If not provided, uses `name` (or derived name).
39
+ help (str, optional):
40
+ Help text for this parameter.
41
+ tags (str | list[str], optional):
42
+ Tag(s) to associate with this parameter for grouping.
43
+ **kwargs (Any):
44
+ Additional keyword arguments.
45
+
46
+ Returns:
47
+ Callable:
48
+ A decorator function that registers the context parent node.
49
+ """
50
+ return Context.as_decorator(
51
+ name=name,
52
+ param=param,
53
+ help=help,
54
+ tags=tags,
55
+ **kwargs,
56
+ )
@@ -0,0 +1,155 @@
1
+ """`ParentNode` that loads a value from an environment variable."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+ # pylint: disable=too-many-positional-arguments
5
+ # pylint: disable=redefined-builtin
6
+
7
+ import os
8
+ from typing import Any, Callable, ParamSpec, TypeVar, cast
9
+
10
+ from dotenv import load_dotenv
11
+
12
+ from click_extended.core.nodes.parent_node import ParentNode
13
+ from click_extended.core.other.context import Context
14
+ from click_extended.utils.casing import Casing
15
+
16
+ load_dotenv()
17
+
18
+ P = ParamSpec("P")
19
+ T = TypeVar("T")
20
+
21
+
22
+ class Env(ParentNode):
23
+ """`ParentNode` that loads a value from an environment variable."""
24
+
25
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> Any:
26
+ """
27
+ Load and return the environment variable value.
28
+
29
+ Args:
30
+ context (Context):
31
+ The current context instance.
32
+ *args (Any):
33
+ Optional positional arguments.
34
+ **kwargs (Any):
35
+ Keyword arguments from the decorator, including:
36
+ - env_name (str): The environment variable name to read.
37
+
38
+ Returns:
39
+ Any:
40
+ The value of the environment variable, or the default value
41
+ if not required and not set.
42
+
43
+ Raises:
44
+ ValueError:
45
+ If the environment variable is required but not set.
46
+ """
47
+ env_name = kwargs.get("env_name")
48
+ if env_name is None:
49
+ raise ValueError("env_name must be provided")
50
+
51
+ value = os.getenv(env_name)
52
+
53
+ if value is None:
54
+ if self.required:
55
+ raise ValueError(
56
+ f"Required environment variable '{env_name}' " "is not set."
57
+ )
58
+ value = self.default
59
+
60
+ return value
61
+
62
+ def get_display_name(self) -> str:
63
+ """
64
+ Get a formatted display name for error messages.
65
+
66
+ Returns:
67
+ str:
68
+ The environment variable name in SCREAMING_SNAKE_CASE.
69
+ """
70
+ return Casing.to_screaming_snake_case(self.name)
71
+
72
+ def check_required(self) -> str | None:
73
+ """
74
+ Check if required environment variable is set.
75
+
76
+ Returns:
77
+ str | None:
78
+ The name of the missing environment variable if required
79
+ and not set, otherwise None.
80
+ """
81
+ env_name = self.decorator_kwargs.get("env_name")
82
+ if env_name and self.required and os.getenv(env_name) is None:
83
+ return cast(str, env_name)
84
+ return None
85
+
86
+
87
+ def env(
88
+ env_name: str,
89
+ name: str | None = None,
90
+ param: str | None = None,
91
+ help: str | None = None,
92
+ required: bool = False,
93
+ default: Any = None,
94
+ tags: str | list[str] | None = None,
95
+ **kwargs: Any,
96
+ ) -> Callable[[Callable[P, T]], Callable[P, T]]:
97
+ """
98
+ A `ParentNode` decorator to inject an environment variable value
99
+ into a command.
100
+
101
+ Type: `ParentNode`
102
+
103
+ Args:
104
+ env_name (str):
105
+ The name of the environment variable to read (e.g., "API_KEY").
106
+ Can be in any format, typically SCREAMING_SNAKE_CASE.
107
+ name (str, optional):
108
+ Internal node name (must be snake_case). If not provided,
109
+ uses env_name converted to snake_case.
110
+ param (str, optional):
111
+ The parameter name to inject into the function.
112
+ If not provided, uses name (or derived name).
113
+ help (str, optional):
114
+ Help text for this parameter.
115
+ required (bool):
116
+ Whether this parameter is required. Defaults to `False`.
117
+ If `True`, the environment variable must be set, even if
118
+ a default value is provided. The default is ignored when
119
+ `required=True`.
120
+ default (Any):
121
+ Default value if environment variable is not set and
122
+ `required=False`. Defaults to `None`.
123
+ tags (str | list[str], optional):
124
+ Tag(s) to associate with this parameter for grouping.
125
+ **kwargs (Any):
126
+ Additional keyword arguments.
127
+
128
+ Returns:
129
+ Callable:
130
+ A decorator function that registers the env parent node.
131
+
132
+ Examples:
133
+ >>> @env("API_KEY")
134
+ ... def my_func(api_key):
135
+ ... print(api_key)
136
+
137
+ >>> @env("DATABASE_URL", param="db", required=True)
138
+ ... def my_func(db):
139
+ ... print(db)
140
+
141
+ >>> @env("MY_ENV", param="api_key")
142
+ ... def my_func(api_key):
143
+ ... print(api_key)
144
+ """
145
+ node_name = name if name is not None else Casing.to_snake_case(env_name)
146
+ return Env.as_decorator(
147
+ name=node_name,
148
+ env_name=env_name,
149
+ param=param,
150
+ help=help,
151
+ required=required,
152
+ default=default,
153
+ tags=tags,
154
+ **kwargs,
155
+ )
@@ -0,0 +1,96 @@
1
+ """Group implementation for the `click_extended` library."""
2
+
3
+ # pylint: disable=protected-access
4
+ # pylint: disable=redefined-builtin
5
+ # pylint: disable=too-many-locals
6
+ # pylint: disable=too-many-branches
7
+
8
+ from typing import Any, Callable
9
+
10
+ import click
11
+
12
+ from click_extended.core.nodes._root_node import RootNode
13
+ from click_extended.core.other._click_group import ClickGroup
14
+
15
+
16
+ class Group(RootNode):
17
+ """Group implementation for the `click_extended` library."""
18
+
19
+ @classmethod
20
+ def _get_click_decorator(cls) -> Callable[..., Any]:
21
+ """Return the click.group decorator (no longer used)."""
22
+ return click.group
23
+
24
+ @classmethod
25
+ def _get_click_cls(cls) -> type[click.Command]: # type: ignore[override]
26
+ """Return the ClickGroup class."""
27
+ return ClickGroup
28
+
29
+ @classmethod
30
+ def wrap(
31
+ cls,
32
+ wrapped_func: Callable[..., Any],
33
+ name: str,
34
+ instance: RootNode,
35
+ **kwargs: Any,
36
+ ) -> ClickGroup:
37
+ """Override to return proper ClickGroup type."""
38
+ return super().wrap(
39
+ wrapped_func,
40
+ name,
41
+ instance,
42
+ **kwargs,
43
+ ) # type: ignore[return-value]
44
+
45
+ @classmethod
46
+ def as_decorator(
47
+ cls, name: str | None = None, /, **kwargs: Any
48
+ ) -> Callable[[Callable[..., Any]], ClickGroup]:
49
+ """Override to return proper ClickGroup type."""
50
+ return super().as_decorator(
51
+ name,
52
+ **kwargs,
53
+ ) # type: ignore[return-value]
54
+
55
+
56
+ def group(
57
+ name: str | None = None,
58
+ *,
59
+ aliases: str | list[str] | None = None,
60
+ help: str | None = None,
61
+ **kwargs: Any,
62
+ ) -> Callable[[Callable[..., Any]], ClickGroup]:
63
+ """
64
+ A `RootNode` decorator to create a click group with value injection
65
+ from parent nodes.
66
+
67
+ Args:
68
+ name (str, optional):
69
+ The name of the group. If `None`, uses the
70
+ decorated function's name.
71
+ aliases (str | list[str], optional):
72
+ Alternative name(s) for the group. Can be a single
73
+ string or a list of strings.
74
+ help (str, optional):
75
+ The help message for the group. If not provided,
76
+ uses the first line of the function's docstring.
77
+ **kwargs (Any):
78
+ Additional arguments to pass to `click.Group`.
79
+
80
+ Returns:
81
+ Callable:
82
+ A decorator function that returns a ClickGroup.
83
+ """
84
+ if aliases is not None:
85
+ kwargs["aliases"] = aliases
86
+ if help is not None:
87
+ kwargs["help"] = help
88
+
89
+ def decorator(func: Callable[..., Any]) -> ClickGroup:
90
+ if help is None and func.__doc__:
91
+ first_line = func.__doc__.strip().split("\n")[0].strip()
92
+ if first_line:
93
+ kwargs["help"] = first_line
94
+ return Group.as_decorator(name, **kwargs)(func)
95
+
96
+ return decorator