wexample-cli 1.2.0__tar.gz → 1.3.1__tar.gz

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 (40) hide show
  1. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/PKG-INFO +8 -8
  2. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/README.md +4 -4
  3. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/pyproject.toml +4 -4
  4. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/command/extended_command.py +9 -8
  5. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/common/command_method_wrapper.py +3 -3
  6. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/context/execution_context.py +9 -7
  7. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/as_sudo.py +5 -4
  8. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/middleware.py +2 -3
  9. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/option_stop_on_failure.py +2 -2
  10. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/screenable.py +8 -12
  11. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/webhook.py +4 -4
  12. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/helpers/extra_args.py +4 -4
  13. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/middleware/abstract_middleware.py +13 -14
  14. wexample_cli-1.3.1/tests/unit/__init__.py +0 -0
  15. wexample_cli-1.3.1/tests/unit/helpers/__init__.py +0 -0
  16. wexample_cli-1.3.1/tests/unit/helpers/test_extra_args.py +4 -0
  17. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/__init__.py +0 -0
  18. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/command/__init__.py +0 -0
  19. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/common/__init__.py +0 -0
  20. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/const/__init__.py +0 -0
  21. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/const/middleware.py +0 -0
  22. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/const/tags.py +0 -0
  23. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/const/types.py +0 -0
  24. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/context/__init__.py +0 -0
  25. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/__init__.py +0 -0
  26. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/alias.py +0 -0
  27. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/command.py +0 -0
  28. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/decorator/option.py +0 -0
  29. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/exception/__init__.py +0 -0
  30. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/exception/abstract_command_option_exception.py +0 -0
  31. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/exception/command_option_missing_exception.py +0 -0
  32. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/exception/command_option_validation_exception.py +0 -0
  33. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/helpers/__init__.py +0 -0
  34. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/middleware/__init__.py +0 -0
  35. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/py.typed +0 -0
  36. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/testing/__init__.py +0 -0
  37. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/src/wexample_cli/testing/kernel.py +0 -0
  38. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/tests/__init__.py +0 -0
  39. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/tests/helpers/__init__.py +0 -0
  40. {wexample_cli-1.2.0 → wexample_cli-1.3.1}/tests/helpers/test_extra_args.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-cli
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework.
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -10,9 +10,9 @@ Classifier: Operating System :: OS Independent
10
10
  Project-URL: homepage, https://github.com/wexample/python-cli
11
11
  Requires-Python: >=3.10
12
12
  Requires-Dist: attrs>=23.1.0
13
- Requires-Dist: wexample-app>=18.1.0
14
- Requires-Dist: wexample-helpers>=16.2.0
15
- Requires-Dist: wexample-prompt>=13.2.0
13
+ Requires-Dist: wexample-app>=18.3.0
14
+ Requires-Dist: wexample-helpers>=17.2.0
15
+ Requires-Dist: wexample-prompt>=13.4.0
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest; extra == "dev"
18
18
  Requires-Dist: pytest-cov; extra == "dev"
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
20
20
 
21
21
  # cli
22
22
 
23
- Version: 1.2.0
23
+ Version: 1.3.1
24
24
 
25
25
  Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework.
26
26
 
@@ -100,9 +100,9 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
100
100
  ## Dependencies
101
101
 
102
102
  - attrs: >=23.1.0
103
- - wexample-app: >=18.1.0
104
- - wexample-helpers: >=16.2.0
105
- - wexample-prompt: >=13.2.0
103
+ - wexample-app: >=18.3.0
104
+ - wexample-helpers: >=17.2.0
105
+ - wexample-prompt: >=13.4.0
106
106
 
107
107
  ## Versioning & Compatibility Policy
108
108
 
@@ -1,6 +1,6 @@
1
1
  # cli
2
2
 
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
 
5
5
  Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework.
6
6
 
@@ -80,9 +80,9 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
80
80
  ## Dependencies
81
81
 
82
82
  - attrs: >=23.1.0
83
- - wexample-app: >=18.1.0
84
- - wexample-helpers: >=16.2.0
85
- - wexample-prompt: >=13.2.0
83
+ - wexample-app: >=18.3.0
84
+ - wexample-helpers: >=17.2.0
85
+ - wexample-prompt: >=13.4.0
86
86
 
87
87
  ## Versioning & Compatibility Policy
88
88
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-cli"
9
- version = "1.2.0"
9
+ version = "1.3.1"
10
10
  description = "Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework."
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -19,9 +19,9 @@ classifiers = [
19
19
  ]
20
20
  dependencies = [
21
21
  "attrs>=23.1.0",
22
- "wexample-app>=18.1.0",
23
- "wexample-helpers>=16.2.0",
24
- "wexample-prompt>=13.2.0",
22
+ "wexample-app>=18.3.0",
23
+ "wexample-helpers>=17.2.0",
24
+ "wexample-prompt>=13.4.0",
25
25
  ]
26
26
 
27
27
  [project.readme]
@@ -100,18 +100,18 @@ class ExtendedCommand(Command):
100
100
 
101
101
  # Validate the value if validators are defined and value is not None
102
102
  if value is not None and option.validators:
103
+ from wexample_cli.exception.command_option_validation_exception import (
104
+ CommandOptionValidationException,
105
+ )
106
+
103
107
  # For multiple values, validate each item individually
104
108
  values_to_validate = value if isinstance(value, list) else [value]
105
109
 
106
110
  for val in values_to_validate:
107
111
  for validator in option.validators:
108
112
  if not validator.validate(val):
109
- from wexample_cli.exception.command_option_validation_exception import (
110
- CommandOptionValidationException,
111
- )
112
-
113
113
  raise CommandOptionValidationException(
114
- option_name=option.name,
114
+ option_name=name,
115
115
  value=val,
116
116
  error_message=validator.get_error_message(val),
117
117
  )
@@ -159,10 +159,11 @@ class ExtendedCommand(Command):
159
159
  MIDDLEWARE_OPTION_VALUE_OPTIONAL,
160
160
  )
161
161
 
162
- if self.command_wrapper.middlewares:
162
+ middlewares = self.command_wrapper.middlewares
163
+ if middlewares:
163
164
  output = MultipleResponse(kernel=self.kernel)
164
165
 
165
- for middleware in self.command_wrapper.middlewares:
166
+ for middleware in middlewares:
166
167
  show_progress = (
167
168
  middleware.show_progress == MIDDLEWARE_OPTION_VALUE_ALLWAYS
168
169
  or (
@@ -306,7 +307,7 @@ class ExtendedCommand(Command):
306
307
  function_to_execute = execution_context.function or self.function
307
308
 
308
309
  # Run the function in a thread pool to avoid blocking the event loop
309
- loop = asyncio.get_event_loop()
310
+ loop = asyncio.get_running_loop()
310
311
  result = await loop.run_in_executor(
311
312
  executor,
312
313
  lambda: function_to_execute(**execution_context.function_kwargs),
@@ -114,9 +114,9 @@ class CommandMethodWrapper(BaseClass):
114
114
 
115
115
  def set_middleware(self, middleware: AbstractMiddleware) -> None:
116
116
  self.middlewares.append(middleware)
117
-
118
- for option in middleware.normalized_options:
119
- self.set_option(option)
117
+ # extend is a single C-level call; avoids N Python method-dispatch
118
+ # round-trips that self.set_option() would incur per option.
119
+ self.options.extend(middleware.normalized_options)
120
120
 
121
121
  def set_option(self, option: Option) -> None:
122
122
  self.options.append(option)
@@ -64,15 +64,17 @@ class ExecutionContext(
64
64
  return self._current_progress
65
65
 
66
66
  def finish_progress(self, **kwargs) -> ProgressHandle:
67
- self._current_progress.finish()
68
-
69
- self._current_progress = self._current_progress.parent
70
- self._current_progress.update(**kwargs)
71
- return self._current_progress
67
+ progress = self._current_progress
68
+ progress.finish()
69
+ parent = progress.parent
70
+ parent.update(**kwargs)
71
+ self._current_progress = parent
72
+ return parent
72
73
 
73
74
  def get_or_create_progress(self, **kwargs) -> ProgressHandle:
74
75
  if self._current_progress is None:
75
- self._current_progress = self.io.progress(
76
- **kwargs, context=self.io.create_context()
76
+ io = self.io
77
+ self._current_progress = io.progress(
78
+ **kwargs, context=io.create_context()
77
79
  ).get_handle()
78
80
  return self._current_progress
@@ -7,8 +7,9 @@ if TYPE_CHECKING:
7
7
 
8
8
 
9
9
  def as_sudo() -> CommandMethodWrapper:
10
- def decorator(wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
11
- wrapper.sudo = True
12
- return wrapper
10
+ return _sudo_decorator
13
11
 
14
- return decorator
12
+
13
+ def _sudo_decorator(wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
14
+ wrapper.sudo = True
15
+ return wrapper
@@ -2,10 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from wexample_cli.common.command_method_wrapper import CommandMethodWrapper
6
+
5
7
  if TYPE_CHECKING:
6
8
  from wexample_helpers.const.types import AnyCallable
7
9
 
8
- from wexample_cli.common.command_method_wrapper import CommandMethodWrapper
9
10
  from wexample_cli.middleware.abstract_middleware import AbstractMiddleware
10
11
 
11
12
 
@@ -16,8 +17,6 @@ def middleware(
16
17
  ) -> AnyCallable:
17
18
  def decorator(command_wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
18
19
  # Type safety check
19
- from wexample_cli.common.command_method_wrapper import CommandMethodWrapper
20
-
21
20
  if not isinstance(command_wrapper, CommandMethodWrapper):
22
21
  raise TypeError(
23
22
  f"Invalid middleware usage: @middleware must decorate a {CommandMethodWrapper.__name__} "
@@ -11,9 +11,9 @@ OPTION_NAME_STOP_ON_FAILURE: str = "stop_on_failure"
11
11
 
12
12
 
13
13
  def option_stop_on_failure() -> AnyCallable:
14
- def decorator(command_wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
15
- from wexample_app.command.option import Option
14
+ from wexample_app.command.option import Option
16
15
 
16
+ def decorator(command_wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
17
17
  command_wrapper.set_option(
18
18
  Option(
19
19
  name=OPTION_NAME_STOP_ON_FAILURE,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import time
3
4
  from typing import TYPE_CHECKING, Any
4
5
 
5
6
  if TYPE_CHECKING:
@@ -55,7 +56,7 @@ def screenable(interval: float = 1.0, height: int = 30) -> AnyCallable:
55
56
  )
56
57
 
57
58
  command_wrapper.register_pipeline_wrapper(
58
- _build_screen_wrapper(default_interval=float(interval), height=int(height))
59
+ _build_screen_wrapper(default_interval=interval, height=height)
59
60
  )
60
61
 
61
62
  return command_wrapper
@@ -73,31 +74,26 @@ def _build_screen_wrapper(default_interval: float, height: int) -> AnyCallable:
73
74
  ) -> Any:
74
75
  # Always strip screen kwargs so they cannot leak to the user function
75
76
  # or to inner pipeline wrappers, whether or not --screen was passed.
76
- enabled = bool(function_kwargs.pop(OPTION_NAME_SCREEN, False))
77
+ enabled = function_kwargs.pop(OPTION_NAME_SCREEN, False)
77
78
  interval_override = function_kwargs.pop(OPTION_NAME_SCREEN_INTERVAL, None)
78
79
 
79
80
  if not enabled:
80
81
  return next_dispatch(function_kwargs)
81
82
 
82
- import time
83
-
84
83
  from wexample_app.response.null_response import NullResponse
85
84
 
86
- interval = (
87
- float(interval_override)
88
- if interval_override is not None
89
- else default_interval
90
- )
85
+ interval = interval_override if interval_override is not None else default_interval
91
86
  kernel_io = request.kernel.io
92
87
  # Skip the inter-frame sleep on the very first frame so the user sees
93
88
  # output immediately instead of waiting `interval` seconds before the
94
89
  # first render.
95
- is_first_frame = [True]
90
+ is_first_frame = True
96
91
 
97
92
  def _frame(response) -> None:
98
- if not is_first_frame[0]:
93
+ nonlocal is_first_frame
94
+ if not is_first_frame:
99
95
  time.sleep(interval)
100
- is_first_frame[0] = False
96
+ is_first_frame = False
101
97
 
102
98
  response.clear()
103
99
  saved_output = kernel_io.output
@@ -19,9 +19,9 @@ def webhook() -> type[CommandMethodWrapper]:
19
19
  @command(type=COMMAND_TYPE_ADDON)
20
20
  def my__group__command(context): ...
21
21
  """
22
+ return _webhook_decorator # type: ignore[return-value]
22
23
 
23
- def decorator(wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
24
- wrapper.webhook = True
25
- return wrapper
26
24
 
27
- return decorator # type: ignore[return-value]
25
+ def _webhook_decorator(wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
26
+ wrapper.webhook = True
27
+ return wrapper
@@ -27,11 +27,11 @@ def resolve_shell_command(
27
27
  shell, which is the safer default. Use the legacy `--command "..."`
28
28
  form if you intentionally want remote expansion.
29
29
  """
30
- if extra_args and command:
31
- context.io.warning(
32
- "Both --command and `-- <args>` were given; using `-- <args>`."
33
- )
34
30
  if extra_args:
31
+ if command:
32
+ context.io.warning(
33
+ "Both --command and `-- <args>` were given; using `-- <args>`."
34
+ )
35
35
  return shlex.join(extra_args)
36
36
  if command:
37
37
  return command
@@ -72,17 +72,17 @@ class AbstractMiddleware(
72
72
  'Option "stop_on_failure" will be ignored due to parallelization'
73
73
  )
74
74
 
75
- if self.parallel == MIDDLEWARE_OPTION_VALUE_OPTIONAL:
76
- command_wrapper.set_option(
77
- Option(
78
- name="parallel",
79
- short_name="pll",
80
- type=bool,
81
- description="Execute async when possible",
82
- default=False,
83
- is_flag=True,
75
+ if self.parallel == MIDDLEWARE_OPTION_VALUE_OPTIONAL:
76
+ command_wrapper.set_option(
77
+ Option(
78
+ name="parallel",
79
+ short_name="pll",
80
+ type=bool,
81
+ description="Execute async when possible",
82
+ default=False,
83
+ is_flag=True,
84
+ )
84
85
  )
85
- )
86
86
 
87
87
  if self.show_progress == MIDDLEWARE_OPTION_VALUE_OPTIONAL:
88
88
  command_wrapper.set_option(
@@ -133,10 +133,9 @@ class AbstractMiddleware(
133
133
 
134
134
  def get_option_by_name(self, name: str) -> Option | None:
135
135
  """Get an option by its name from the normalized options."""
136
- for option in self.normalized_options:
137
- if option.name == name:
138
- return option
139
- return None
136
+ return next(
137
+ (opt for opt in self.normalized_options if opt.name == name), None
138
+ )
140
139
 
141
140
  def validate_options(
142
141
  self,
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ def test_resolve_shell_command() -> None:
4
+ pass