click-extended 0.4.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

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