invoke-toolkit 0.0.63__tar.gz → 0.0.65b1__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.
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/.gitignore +1 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/PKG-INFO +1 -1
- invoke_toolkit-0.0.65b1/src/invoke_toolkit/context/async_tools.py +198 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/context/context.py +20 -5
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/context/types.py +7 -1
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/executor.py +42 -81
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/program/program.py +122 -25
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/tasks/cache.py +34 -9
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/tasks/tasks.py +125 -139
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/script/test_script.py +5 -4
- invoke_toolkit-0.0.65b1/tests/test_alias_self_reference.py +108 -0
- invoke_toolkit-0.0.65b1/tests/test_async_edge_cases.py +44 -0
- invoke_toolkit-0.0.65b1/tests/test_async_support.py +88 -0
- invoke_toolkit-0.0.65b1/tests/test_list_filter_flags.py +208 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_proctitle.py +16 -8
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/LICENSE.txt +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/README.md +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/pyproject.toml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/__main__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/collections.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/completion.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/config/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/config/config.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/config/registry.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/config/schema.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/config/status_helper.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/context/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/extensions/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/extensions/tasks/config.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/extensions/tasks/create.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/loader/entrypoint.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/log/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/log/logger.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/output/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/output/console.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/output/utils.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/parser.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/program/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/program/main.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/py.typed +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/runners/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/runners/rich.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/scripts/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/scripts/loader.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/tasks/autocomplete.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/tasks/types.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/testing.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/utils/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/utils/fzf.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/utils/inspection.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/utils/singleton.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/src/invoke_toolkit/utils/text.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/templates/package-template/.gitignore.jinja +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/templates/package-template/README.md.jinja +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/templates/package-template/copier.yml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/templates/package-template/pyproject.toml.jinja +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/conftest.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/examples/cached_completion/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/examples/completion_with_config/.gitignore +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/examples/completion_with_config/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/examples/config_schema/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/examples/enum_select_size/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/examples/fzf_selector/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/examples/literal_set_level/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/extensions/conftest.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/extensions/test_config_tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/extensions/test_create.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/extensions/test_package_template_entrypoint.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/extensions/test_shell_tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/collection.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/echo.yaml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/no-dedupe.yaml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/no-echo.yaml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/package/invoke.yml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/package/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/runtime.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/underscores/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/yaml/explicit.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/yaml/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/yml/explicit.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/configs/yml/tasks.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/_support/has_modules.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/conftest.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/original_invoke/test_config.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/program/main.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/program/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/program/tasks/coll1.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/tasks/test_cache.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/tasks/test_extensions_config.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_annotated_help.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_cofig_class.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_collection.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_collection_configure.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_completion_with_choices.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_config_helper.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_config_registry.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_config_schema.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_console.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_context_class.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_disable_status_cli.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_enum_arguments.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_executor.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_file_completion.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_global_context.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_help_flags.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_invoke_3_compat.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_invoke_compatibility.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_loader.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_parsing.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_program_with_collection.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/test_toplevel.py +0 -0
- {invoke_toolkit-0.0.63 → invoke_toolkit-0.0.65b1}/tests/utils/test_fzf.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invoke-toolkit
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.65b1
|
|
4
4
|
Summary: Extended APIs for PyInvoke: composable task plugins, typed CLI args, Rich output, shell completion, and PEP 723 script support
|
|
5
5
|
Project-URL: Documentation, https://github.com/D3f0/invoke-toolkit#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/D3f0/invoke-toolkit/issues
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Async execution helpers for :class:`ToolkitContext`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import asyncio.subprocess as async_subprocess
|
|
7
|
+
import contextvars
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from typing import Any, Awaitable, Iterator
|
|
12
|
+
|
|
13
|
+
from invoke.exceptions import CommandTimedOut, UnexpectedExit
|
|
14
|
+
from invoke.runners import Result, default_encoding, normalize_hide
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_async_task_context: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
|
18
|
+
"invoke_toolkit_async_task_context", default=False
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def async_task_context() -> Iterator[None]:
|
|
24
|
+
"""Mark synchronous context methods called from an async task."""
|
|
25
|
+
token = _async_task_context.set(True)
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
finally:
|
|
29
|
+
_async_task_context.reset(token)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def in_async_task_context() -> bool:
|
|
33
|
+
"""Return whether the current execution is an invoke-toolkit async task."""
|
|
34
|
+
return _async_task_context.get()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AsyncGatherScope:
|
|
38
|
+
"""Schedule awaitables and await them together when the scope exits.
|
|
39
|
+
|
|
40
|
+
Submitted awaitables start immediately. ``results`` contains their return
|
|
41
|
+
values in submission order after a successful scope exit.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._tasks: list[asyncio.Future[Any]] = []
|
|
46
|
+
self.results: tuple[Any, ...] = ()
|
|
47
|
+
|
|
48
|
+
async def __aenter__(self) -> "AsyncGatherScope":
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def __call__(self, awaitable: Awaitable[Any]) -> asyncio.Future[Any]:
|
|
52
|
+
"""Schedule *awaitable* and return its task handle."""
|
|
53
|
+
task = asyncio.ensure_future(awaitable)
|
|
54
|
+
self._tasks.append(task)
|
|
55
|
+
return task
|
|
56
|
+
|
|
57
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> bool:
|
|
58
|
+
if exc_type is not None:
|
|
59
|
+
await self._cancel_remaining()
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
self.results = tuple(await asyncio.gather(*self._tasks))
|
|
64
|
+
except BaseException:
|
|
65
|
+
await self._cancel_remaining()
|
|
66
|
+
raise
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
async def _cancel_remaining(self) -> None:
|
|
70
|
+
pending = [task for task in self._tasks if not task.done()]
|
|
71
|
+
for task in pending:
|
|
72
|
+
task.cancel()
|
|
73
|
+
if pending:
|
|
74
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def run_async_command( # pylint: disable=too-many-branches,too-many-locals
|
|
78
|
+
context: Any, command: str, **kwargs: Any
|
|
79
|
+
) -> Result:
|
|
80
|
+
"""Run a shell command without blocking the current event loop."""
|
|
81
|
+
options = dict(context.config.run.items())
|
|
82
|
+
for key, value in kwargs.items():
|
|
83
|
+
if key not in options and key != "timeout":
|
|
84
|
+
raise TypeError(f"run_async() got an unexpected keyword argument '{key}'")
|
|
85
|
+
options[key] = value
|
|
86
|
+
|
|
87
|
+
timeout = options.pop("timeout", context.config.timeouts.command)
|
|
88
|
+
if options.get("asynchronous") or options.get("disown"):
|
|
89
|
+
raise ValueError("run_async() cannot use asynchronous or disown")
|
|
90
|
+
if options.get("pty"):
|
|
91
|
+
raise ValueError("run_async() does not support pty=True")
|
|
92
|
+
if options.get("watchers"):
|
|
93
|
+
raise ValueError("run_async() does not support stream watchers")
|
|
94
|
+
if options.get("echo_stdin"):
|
|
95
|
+
raise ValueError("run_async() does not support echo_stdin")
|
|
96
|
+
if options.get("in_stream") not in (None, False):
|
|
97
|
+
raise ValueError("run_async() only supports disabled stdin")
|
|
98
|
+
|
|
99
|
+
command = context._prefix_commands(command) # pylint: disable=protected-access
|
|
100
|
+
shell = options.get("shell") or "bash"
|
|
101
|
+
encoding = options.get("encoding") or default_encoding()
|
|
102
|
+
hide = normalize_hide(
|
|
103
|
+
options.get("hide"), options.get("out_stream"), options.get("err_stream")
|
|
104
|
+
)
|
|
105
|
+
env_values = options.get("env") or {}
|
|
106
|
+
env = (
|
|
107
|
+
dict(env_values)
|
|
108
|
+
if options.get("replace_env")
|
|
109
|
+
else dict(os.environ, **env_values)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if options.get("echo"):
|
|
113
|
+
print(options.get("echo_format", "{command}").format(command=command))
|
|
114
|
+
if options.get("dry"):
|
|
115
|
+
return Result(
|
|
116
|
+
command=command,
|
|
117
|
+
shell=shell,
|
|
118
|
+
env=env,
|
|
119
|
+
encoding=encoding,
|
|
120
|
+
exited=0,
|
|
121
|
+
hide=hide,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
process = await asyncio.create_subprocess_shell(
|
|
125
|
+
command,
|
|
126
|
+
executable=shell,
|
|
127
|
+
stdout=async_subprocess.PIPE,
|
|
128
|
+
stderr=async_subprocess.PIPE,
|
|
129
|
+
stdin=async_subprocess.DEVNULL,
|
|
130
|
+
env=env,
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
communicate = process.communicate()
|
|
134
|
+
if timeout is None:
|
|
135
|
+
stdout, stderr = await communicate
|
|
136
|
+
else:
|
|
137
|
+
stdout, stderr = await asyncio.wait_for(communicate, timeout=timeout)
|
|
138
|
+
except asyncio.TimeoutError as exc:
|
|
139
|
+
await _terminate_process(process)
|
|
140
|
+
result = _result(command, shell, env, encoding, process, b"", b"", hide)
|
|
141
|
+
raise CommandTimedOut(result, timeout=timeout) from exc
|
|
142
|
+
except asyncio.CancelledError:
|
|
143
|
+
await _terminate_process(process)
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
result = _result(command, shell, env, encoding, process, stdout, stderr, hide)
|
|
147
|
+
_write_streams(result, options)
|
|
148
|
+
if not result.ok and not options.get("warn"):
|
|
149
|
+
raise UnexpectedExit(result)
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def _terminate_process(process: async_subprocess.Process) -> None:
|
|
154
|
+
if process.returncode is not None:
|
|
155
|
+
return
|
|
156
|
+
process.terminate()
|
|
157
|
+
try:
|
|
158
|
+
await asyncio.wait_for(process.wait(), timeout=1)
|
|
159
|
+
except asyncio.TimeoutError:
|
|
160
|
+
process.kill()
|
|
161
|
+
await process.wait()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _result(
|
|
165
|
+
command: str,
|
|
166
|
+
shell: str,
|
|
167
|
+
env: dict[str, str],
|
|
168
|
+
encoding: str,
|
|
169
|
+
process: async_subprocess.Process,
|
|
170
|
+
stdout: bytes,
|
|
171
|
+
stderr: bytes,
|
|
172
|
+
hide: tuple[str, ...],
|
|
173
|
+
) -> Result:
|
|
174
|
+
exited = process.returncode
|
|
175
|
+
if exited is None:
|
|
176
|
+
raise RuntimeError("Process completed without an exit code")
|
|
177
|
+
return Result(
|
|
178
|
+
stdout=stdout.decode(encoding, errors="replace"),
|
|
179
|
+
stderr=stderr.decode(encoding, errors="replace"),
|
|
180
|
+
command=command,
|
|
181
|
+
shell=shell,
|
|
182
|
+
env=env,
|
|
183
|
+
encoding=encoding,
|
|
184
|
+
exited=exited,
|
|
185
|
+
hide=hide,
|
|
186
|
+
pid=process.pid,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _write_streams(result: Result, options: dict[str, Any]) -> None:
|
|
191
|
+
out_stream = options.get("out_stream") or sys.stdout
|
|
192
|
+
err_stream = options.get("err_stream") or sys.stderr
|
|
193
|
+
if "out" not in result.hide and out_stream is not False:
|
|
194
|
+
out_stream.write(result.stdout)
|
|
195
|
+
out_stream.flush()
|
|
196
|
+
if "err" not in result.hide and err_stream is not False:
|
|
197
|
+
err_stream.write(result.stderr)
|
|
198
|
+
err_stream.flush()
|
|
@@ -29,7 +29,8 @@ from invoke_toolkit.config import ToolkitConfig
|
|
|
29
29
|
from invoke_toolkit.config.status_helper import StatusHelper
|
|
30
30
|
from invoke_toolkit.output.console import get_console
|
|
31
31
|
|
|
32
|
-
from .types import BoundPrintProtocol, ContextRunProtocol
|
|
32
|
+
from .types import AsyncContextRunProtocol, BoundPrintProtocol, ContextRunProtocol
|
|
33
|
+
from .async_tools import AsyncGatherScope, in_async_task_context, run_async_command
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from rich.console import Console
|
|
@@ -58,10 +59,10 @@ class ConfigProtocol(Protocol):
|
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
class ToolkitContext(Context, ConfigProtocol):
|
|
61
|
-
"""Type annotated override"""
|
|
62
|
+
"""Type annotated override with async task support."""
|
|
62
63
|
|
|
63
64
|
run: ContextRunProtocol
|
|
64
|
-
|
|
65
|
+
run_async: AsyncContextRunProtocol
|
|
65
66
|
_config: ToolkitConfig
|
|
66
67
|
_status_helper: StatusHelper
|
|
67
68
|
|
|
@@ -93,8 +94,22 @@ class ToolkitContext(Context, ConfigProtocol):
|
|
|
93
94
|
@property
|
|
94
95
|
def console(self) -> "Console":
|
|
95
96
|
"""A console instance to do rich output"""
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
return get_console()
|
|
98
|
+
|
|
99
|
+
def run(self, command: str, **kwargs: Any):
|
|
100
|
+
"""Run a command synchronously, or return a coroutine in async tasks."""
|
|
101
|
+
if in_async_task_context() and not kwargs.get("asynchronous", False):
|
|
102
|
+
return self.run_async(command, **kwargs)
|
|
103
|
+
runner = self.config.runners.local(self)
|
|
104
|
+
return self._run(runner, command, **kwargs)
|
|
105
|
+
|
|
106
|
+
async def run_async(self, command: str, **kwargs: Any):
|
|
107
|
+
"""Run a command without blocking the current asyncio event loop."""
|
|
108
|
+
return await run_async_command(self, command, **kwargs)
|
|
109
|
+
|
|
110
|
+
def gather(self) -> AsyncGatherScope:
|
|
111
|
+
"""Return an async context manager for explicit concurrent work."""
|
|
112
|
+
return AsyncGatherScope()
|
|
98
113
|
|
|
99
114
|
# @contextmanager
|
|
100
115
|
@property
|
|
@@ -3,7 +3,7 @@ Type annotations for finding out what's there in ctx.attribute
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
|
-
from typing import IO, Any, Optional, Union
|
|
6
|
+
from typing import IO, Any, Awaitable, Optional, Union
|
|
7
7
|
|
|
8
8
|
from invoke.runners import Result
|
|
9
9
|
from invoke.watchers import StreamWatcher
|
|
@@ -11,6 +11,12 @@ from rich.console import JustifyMethod, OverflowMethod, Style
|
|
|
11
11
|
from typing_extensions import Annotated, Protocol
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class AsyncContextRunProtocol(Protocol):
|
|
15
|
+
"""Coroutine API for native asyncio command execution."""
|
|
16
|
+
|
|
17
|
+
def __call__(self, command: str, **kwargs: Any) -> Awaitable[Result]: ...
|
|
18
|
+
|
|
19
|
+
|
|
14
20
|
class BoundPrintProtocol(Protocol):
|
|
15
21
|
def __call__(
|
|
16
22
|
self,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Custom executor class to for Syntax highlighted output
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import inspect
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
7
8
|
|
|
@@ -13,6 +14,7 @@ from invoke.util import debug
|
|
|
13
14
|
|
|
14
15
|
from invoke_toolkit.collections import ToolkitCollection
|
|
15
16
|
from invoke_toolkit.config import ToolkitConfig
|
|
17
|
+
from invoke_toolkit.context.async_tools import async_task_context
|
|
16
18
|
from invoke_toolkit.output import get_console
|
|
17
19
|
from invoke_toolkit.tasks.tasks import ToolkitCall, ToolkitTask
|
|
18
20
|
|
|
@@ -50,110 +52,69 @@ class ToolkitExecutor(Executor):
|
|
|
50
52
|
self.config = config if config is not None else ToolkitConfig()
|
|
51
53
|
self.core = core if core is not None else ParseResult()
|
|
52
54
|
|
|
55
|
+
async def execute_async(
|
|
56
|
+
self, *tasks: Union[str, Tuple[str, Dict[str, Any]], ParserContext]
|
|
57
|
+
) -> Dict["ToolkitTask", Any]:
|
|
58
|
+
"""Execute task calls in order, awaiting coroutine task results."""
|
|
59
|
+
calls = self.normalize(tasks)
|
|
60
|
+
direct = list(calls)
|
|
61
|
+
expanded = self.expand_calls(calls)
|
|
62
|
+
try:
|
|
63
|
+
dedupe = self.config.tasks.dedupe
|
|
64
|
+
except AttributeError:
|
|
65
|
+
dedupe = True
|
|
66
|
+
calls = self.dedupe(expanded) if dedupe else expanded
|
|
67
|
+
results: Dict["ToolkitTask", Any] = {}
|
|
68
|
+
for call in calls:
|
|
69
|
+
autoprint = call in direct and call.autoprint
|
|
70
|
+
config = self.config
|
|
71
|
+
collection_config = self.collection.configuration(call.called_as)
|
|
72
|
+
config.load_collection(collection_config)
|
|
73
|
+
config.load_shell_env()
|
|
74
|
+
context = call.make_context(config, core_parse_result=self.core)
|
|
75
|
+
args = (context, *call.args)
|
|
76
|
+
with async_task_context():
|
|
77
|
+
result = call.task(*args, **call.kwargs)
|
|
78
|
+
if inspect.isawaitable(result):
|
|
79
|
+
result = await result
|
|
80
|
+
if autoprint:
|
|
81
|
+
if isinstance(result, (str, Path)):
|
|
82
|
+
print(result)
|
|
83
|
+
else:
|
|
84
|
+
get_console("out").print(result)
|
|
85
|
+
results[call.task] = result # ty: ignore[invalid-assignment]
|
|
86
|
+
return results
|
|
87
|
+
|
|
53
88
|
def execute(
|
|
54
89
|
self, *tasks: Union[str, Tuple[str, Dict[str, Any]], ParserContext]
|
|
55
90
|
) -> Dict["ToolkitTask", "Result"]:
|
|
56
|
-
"""
|
|
57
|
-
Execute one or more ``tasks`` in sequence.
|
|
58
|
-
|
|
59
|
-
:param tasks:
|
|
60
|
-
An all-purpose iterable of "tasks to execute", each member of which
|
|
61
|
-
may take one of the following forms:
|
|
62
|
-
|
|
63
|
-
**A string** naming a task from the Executor's `.Collection`. This
|
|
64
|
-
name may contain dotted syntax appropriate for calling namespaced
|
|
65
|
-
tasks, e.g. ``subcollection.taskname``. Such tasks are executed
|
|
66
|
-
without arguments.
|
|
67
|
-
|
|
68
|
-
**A two-tuple** whose first element is a task name string (as
|
|
69
|
-
above) and whose second element is a dict suitable for use as
|
|
70
|
-
``**kwargs`` when calling the named task. E.g.::
|
|
71
|
-
|
|
72
|
-
[
|
|
73
|
-
('task1', {}),
|
|
74
|
-
('task2', {'arg1': 'val1'}),
|
|
75
|
-
...
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
is equivalent, roughly, to::
|
|
79
|
-
|
|
80
|
-
task1()
|
|
81
|
-
task2(arg1='val1')
|
|
82
|
-
|
|
83
|
-
**A `.ParserContext`** instance, whose ``.name`` attribute is used
|
|
84
|
-
as the task name and whose ``.as_kwargs`` attribute is used as the
|
|
85
|
-
task kwargs (again following the above specifications).
|
|
86
|
-
|
|
87
|
-
.. note::
|
|
88
|
-
When called without any arguments at all (i.e. when ``*tasks``
|
|
89
|
-
is empty), the default task from ``self.collection`` is used
|
|
90
|
-
instead, if defined.
|
|
91
|
-
|
|
92
|
-
:returns:
|
|
93
|
-
A dict mapping task objects to their return values.
|
|
94
|
-
|
|
95
|
-
This dict may include pre- and post-tasks if any were executed. For
|
|
96
|
-
example, in a collection with a ``build`` task depending on another
|
|
97
|
-
task named ``setup``, executing ``build`` will result in a dict
|
|
98
|
-
with two keys, one for ``build`` and one for ``setup``.
|
|
99
|
-
|
|
100
|
-
.. versionadded:: 1.0
|
|
101
|
-
"""
|
|
91
|
+
"""Execute one or more tasks in sequence."""
|
|
102
92
|
# Normalize input
|
|
103
|
-
debug("Examining top level tasks {!r}".format(list(tasks)))
|
|
93
|
+
debug("Examining top level tasks {!r}".format(list(tasks)))
|
|
104
94
|
calls = self.normalize(tasks)
|
|
105
|
-
debug("Tasks (now Calls) with kwargs: {!r}".format(calls)) # pylint: disable=W1202
|
|
106
|
-
# Obtain copy of directly-given tasks since they should sometimes
|
|
107
|
-
# behave differently
|
|
108
95
|
direct = list(calls)
|
|
109
|
-
# Expand pre/post tasks
|
|
110
|
-
# TODO: may make sense to bundle expansion & deduping now eh?
|
|
111
96
|
expanded = self.expand_calls(calls)
|
|
112
|
-
# Get some good value for dedupe option, even if config doesn't have
|
|
113
|
-
# the tree we expect. (This is a concession to testing.)
|
|
114
97
|
try:
|
|
115
98
|
dedupe = self.config.tasks.dedupe
|
|
116
99
|
except AttributeError:
|
|
117
100
|
dedupe = True
|
|
118
|
-
# Dedupe across entire run now that we know about all calls in order
|
|
119
101
|
calls = self.dedupe(expanded) if dedupe else expanded
|
|
120
|
-
# Execute
|
|
121
102
|
results = {}
|
|
122
|
-
# TODO: maybe clone initial config here? Probably not necessary,
|
|
123
|
-
# especially given Executor is not designed to execute() >1 time at the
|
|
124
|
-
# moment...
|
|
125
103
|
for call in calls:
|
|
126
104
|
autoprint = call in direct and call.autoprint
|
|
127
|
-
debug("Executing {!r}".format(call)) # pylint: disable=W1202
|
|
128
|
-
# Hand in reference to our config, which will preserve user
|
|
129
|
-
# modifications across the lifetime of the session.
|
|
130
105
|
config = self.config
|
|
131
|
-
|
|
132
|
-
# (collection & shell env)
|
|
133
|
-
# TODO: load_collection needs to be skipped if task is anonymous
|
|
134
|
-
# (Fabric 2 or other subclassing libs only)
|
|
135
|
-
collection_config = self.collection.configuration(call.called_as) # type: ignore[attr-defined]
|
|
106
|
+
collection_config = self.collection.configuration(call.called_as)
|
|
136
107
|
config.load_collection(collection_config)
|
|
137
108
|
config.load_shell_env()
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# being parameterized), handing in this config for use there.
|
|
142
|
-
context = call.make_context(config, core_parse_result=self.core) # type: ignore[attr-defined]
|
|
143
|
-
args = (context, *call.args) # type: ignore[attr-defined]
|
|
144
|
-
result = call.task(*args, **call.kwargs) # type: ignore[attr-defined]
|
|
109
|
+
context = call.make_context(config, core_parse_result=self.core)
|
|
110
|
+
args = (context, *call.args)
|
|
111
|
+
result = call.task(*args, **call.kwargs)
|
|
145
112
|
if autoprint:
|
|
146
|
-
# NOTE: Long strings will get wrapped when using autoprint in a console
|
|
147
|
-
# we will use print for strings, for the case of piping output
|
|
148
|
-
# and any non string type will be formatted by the console
|
|
149
113
|
if isinstance(result, (str, Path)):
|
|
150
114
|
print(result)
|
|
151
115
|
else:
|
|
152
116
|
get_console("out").print(result)
|
|
153
|
-
|
|
154
|
-
# TODO: handle the non-dedupe case / the same-task-different-args
|
|
155
|
-
# case, wherein one task obj maps to >1 result.
|
|
156
|
-
results[call.task] = result # type: ignore[attr-defined]
|
|
117
|
+
results[call.task] = result
|
|
157
118
|
return results
|
|
158
119
|
|
|
159
120
|
def normalize(
|
|
@@ -7,15 +7,16 @@ It allows three classes to be parametrized: Loader, Config and Executor
|
|
|
7
7
|
__all__ = ["ToolkitProgram"]
|
|
8
8
|
|
|
9
9
|
import ast
|
|
10
|
+
import asyncio
|
|
10
11
|
import inspect
|
|
11
12
|
import os
|
|
12
13
|
import re
|
|
13
14
|
import sys
|
|
14
|
-
from importlib import metadata
|
|
15
|
+
from importlib import import_module, metadata
|
|
15
16
|
from importlib.util import module_from_spec
|
|
16
17
|
from logging import getLogger
|
|
17
18
|
from pathlib import Path
|
|
18
|
-
from typing import Dict, List, Optional, Sequence, Tuple, Union
|
|
19
|
+
from typing import Dict, List, Literal, Optional, Sequence, Tuple, Union
|
|
19
20
|
|
|
20
21
|
from rich.table import Table
|
|
21
22
|
|
|
@@ -81,6 +82,9 @@ class ToolkitProgram(Program):
|
|
|
81
82
|
config_class,
|
|
82
83
|
binary_names,
|
|
83
84
|
)
|
|
85
|
+
# Names of sub-collections that were loaded from entry-point plugins.
|
|
86
|
+
# Populated in load_collection(); used by --list-tasks / --list-plugins.
|
|
87
|
+
self._plugin_collection_names: set[str] = set()
|
|
84
88
|
|
|
85
89
|
def get_version(self) -> str:
|
|
86
90
|
"""Compute version
|
|
@@ -126,13 +130,12 @@ class ToolkitProgram(Program):
|
|
|
126
130
|
self.parse_tasks()
|
|
127
131
|
# End of parsing (typically bailout stuff like --list, --help)
|
|
128
132
|
self.parse_cleanup()
|
|
129
|
-
#
|
|
130
|
-
# runtime config file contents and flag-derived overrides (e.g. for
|
|
131
|
-
# run()'s echo, warn, etc options.)
|
|
133
|
+
# Apply runtime config and CLI-derived overrides before execution.
|
|
132
134
|
self.update_config()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
if self._contains_async_task():
|
|
136
|
+
asyncio.run(self._execute_async())
|
|
137
|
+
else:
|
|
138
|
+
self.execute()
|
|
136
139
|
except (UnexpectedExit, Exit, ParseError) as e:
|
|
137
140
|
debug("Received a possibly-skippable exception: {!r}".format(e))
|
|
138
141
|
# Print error messages from parser, runner, etc if necessary;
|
|
@@ -159,6 +162,31 @@ class ToolkitProgram(Program):
|
|
|
159
162
|
except KeyboardInterrupt:
|
|
160
163
|
sys.exit(1) # Same behavior as Python itself outside of REPL
|
|
161
164
|
|
|
165
|
+
def _contains_async_task(self) -> bool:
|
|
166
|
+
"""Return whether the requested task graph contains coroutine work."""
|
|
167
|
+
for parser_context in self.tasks:
|
|
168
|
+
task = self.collection[parser_context.name]
|
|
169
|
+
calls = [task, *task.pre, *task.post]
|
|
170
|
+
if any(inspect.iscoroutinefunction(call.body) for call in calls):
|
|
171
|
+
return True
|
|
172
|
+
default = self.collection.default
|
|
173
|
+
return bool(
|
|
174
|
+
default and inspect.iscoroutinefunction(self.collection[default].body)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def _execute_async(self) -> None:
|
|
178
|
+
"""Execute parsed tasks through the async executor path."""
|
|
179
|
+
klass = self.executor_class
|
|
180
|
+
config_path = self.config.tasks.executor_class
|
|
181
|
+
if config_path is not None:
|
|
182
|
+
module_path, _, class_name = config_path.rpartition(".")
|
|
183
|
+
module = import_module(module_path)
|
|
184
|
+
klass = getattr(module, class_name)
|
|
185
|
+
executor = klass(self.collection, self.config, self.core)
|
|
186
|
+
if not hasattr(executor, "execute_async"):
|
|
187
|
+
raise TypeError("Async tasks require an executor with execute_async()")
|
|
188
|
+
await executor.execute_async(*self.tasks) # ty: ignore[call-non-callable]
|
|
189
|
+
|
|
162
190
|
def setup_consoles(self):
|
|
163
191
|
"""Pre-populate the console objects"""
|
|
164
192
|
patterns = self.args["redact_pattern"].value
|
|
@@ -368,10 +396,92 @@ class ToolkitProgram(Program):
|
|
|
368
396
|
default=False,
|
|
369
397
|
help="Initializes shell for runner via CLI flag",
|
|
370
398
|
),
|
|
399
|
+
ToolkitArgument(
|
|
400
|
+
names=("list-tasks", "lt"),
|
|
401
|
+
kind=bool,
|
|
402
|
+
default=False,
|
|
403
|
+
help="List only the main tasks discovered in the current directory (excludes plugins)",
|
|
404
|
+
),
|
|
405
|
+
ToolkitArgument(
|
|
406
|
+
names=("list-plugins", "lp"),
|
|
407
|
+
kind=bool,
|
|
408
|
+
default=False,
|
|
409
|
+
help="List only the plugin-provided task collections (excludes main tasks)",
|
|
410
|
+
),
|
|
371
411
|
]
|
|
372
412
|
args.extend(toolkit_program_arguments)
|
|
373
413
|
return args
|
|
374
414
|
|
|
415
|
+
def _handle_list_flags(self) -> None:
|
|
416
|
+
"""
|
|
417
|
+
Handle --list, --list-tasks, and --list-plugins flags.
|
|
418
|
+
|
|
419
|
+
Sets ``list_format`` / ``list_depth`` / ``scoped_collection`` as needed
|
|
420
|
+
and raises `Exit` when any listing flag is active.
|
|
421
|
+
"""
|
|
422
|
+
self.list_format = self.args["list-format"].value
|
|
423
|
+
self.list_depth = self.args["list-depth"].value
|
|
424
|
+
|
|
425
|
+
# --list [root]: standard task listing (optionally scoped to a sub-collection)
|
|
426
|
+
list_root = self.args.list.value # True or a string namespace
|
|
427
|
+
if list_root:
|
|
428
|
+
if isinstance(list_root, str):
|
|
429
|
+
self.list_root = list_root
|
|
430
|
+
try:
|
|
431
|
+
sub = self.collection.subcollection_from_path(list_root)
|
|
432
|
+
self.scoped_collection = sub
|
|
433
|
+
except KeyError:
|
|
434
|
+
msg = "Sub-collection '{}' not found!"
|
|
435
|
+
raise Exit(msg.format(list_root))
|
|
436
|
+
self.list_tasks()
|
|
437
|
+
raise Exit
|
|
438
|
+
|
|
439
|
+
# --list-tasks: show only main (non-plugin) tasks
|
|
440
|
+
if self.args["list-tasks"].value:
|
|
441
|
+
debug("Saw --list-tasks, listing main tasks only & exiting")
|
|
442
|
+
self.scoped_collection = self._make_filtered_collection("tasks")
|
|
443
|
+
self.list_tasks()
|
|
444
|
+
raise Exit
|
|
445
|
+
|
|
446
|
+
# --list-plugins: show only plugin-provided collections
|
|
447
|
+
if self.args["list-plugins"].value:
|
|
448
|
+
debug("Saw --list-plugins, listing plugin collections only & exiting")
|
|
449
|
+
if not self._plugin_collection_names:
|
|
450
|
+
raise Exit("No plugin collections found.")
|
|
451
|
+
self.scoped_collection = self._make_filtered_collection("plugins")
|
|
452
|
+
self.list_tasks()
|
|
453
|
+
raise Exit
|
|
454
|
+
|
|
455
|
+
def _make_filtered_collection(
|
|
456
|
+
self, mode: "Literal['tasks', 'plugins']"
|
|
457
|
+
) -> "ToolkitCollection":
|
|
458
|
+
"""
|
|
459
|
+
Return a shallow copy of ``self.scoped_collection`` containing only
|
|
460
|
+
the subset of tasks and sub-collections appropriate for *mode*.
|
|
461
|
+
|
|
462
|
+
- ``'tasks'`` – direct tasks + non-plugin sub-collections only
|
|
463
|
+
- ``'plugins'`` – plugin sub-collections only (no direct tasks)
|
|
464
|
+
"""
|
|
465
|
+
source = self.scoped_collection
|
|
466
|
+
filtered = ToolkitCollection(source.name or "")
|
|
467
|
+
if mode == "tasks":
|
|
468
|
+
# Include all direct tasks on the scoped collection
|
|
469
|
+
for task_name, task in source.tasks.items():
|
|
470
|
+
filtered.add_task(task, name=task_name)
|
|
471
|
+
# Include sub-collections that are NOT from plugins
|
|
472
|
+
for coll_name, coll in source.collections.items():
|
|
473
|
+
if coll_name not in self._plugin_collection_names:
|
|
474
|
+
filtered.add_collection(coll, name=coll_name)
|
|
475
|
+
else: # mode == "plugins"
|
|
476
|
+
# Include only the plugin sub-collections
|
|
477
|
+
for coll_name, coll in source.collections.items():
|
|
478
|
+
if coll_name in self._plugin_collection_names:
|
|
479
|
+
filtered.add_collection(coll, name=coll_name)
|
|
480
|
+
# Preserve the default task pointer when it still makes sense
|
|
481
|
+
if source.default and source.default in filtered.tasks:
|
|
482
|
+
filtered.default = source.default
|
|
483
|
+
return filtered
|
|
484
|
+
|
|
375
485
|
def _load_entry_points_collection(self, start: str) -> "ToolkitCollection | None":
|
|
376
486
|
"""
|
|
377
487
|
Load task collections from entry points.
|
|
@@ -453,6 +563,7 @@ class ToolkitProgram(Program):
|
|
|
453
563
|
debug("Merging entry point collections with tasks.py")
|
|
454
564
|
for name, collection in ep_collection.collections.items():
|
|
455
565
|
self.collection.add_collection(collection, name=name)
|
|
566
|
+
self._plugin_collection_names.add(name)
|
|
456
567
|
except CollectionNotFound as e:
|
|
457
568
|
start = self.args["search-root"].value or "."
|
|
458
569
|
# First try to load entry points (for packages without tasks.py)
|
|
@@ -460,6 +571,7 @@ class ToolkitProgram(Program):
|
|
|
460
571
|
if ep_collection is not None:
|
|
461
572
|
debug("Loading collections from entry points")
|
|
462
573
|
self.collection = ep_collection
|
|
574
|
+
self._plugin_collection_names.update(ep_collection.collections.keys())
|
|
463
575
|
else:
|
|
464
576
|
# If entry points weren't found, try local_tasks or fail
|
|
465
577
|
if not (Path(start) / "local_tasks.py").exists():
|
|
@@ -571,23 +683,8 @@ class ToolkitProgram(Program):
|
|
|
571
683
|
# should?
|
|
572
684
|
raise ParseError("No idea what '{}' is!".format(halp))
|
|
573
685
|
|
|
574
|
-
#
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
self.list_format = self.args["list-format"].value
|
|
578
|
-
self.list_depth = self.args["list-depth"].value
|
|
579
|
-
if list_root:
|
|
580
|
-
# Not just --list, but --list some-root - do moar work
|
|
581
|
-
if isinstance(list_root, str):
|
|
582
|
-
self.list_root = list_root
|
|
583
|
-
try:
|
|
584
|
-
sub = self.collection.subcollection_from_path(list_root)
|
|
585
|
-
self.scoped_collection = sub
|
|
586
|
-
except KeyError:
|
|
587
|
-
msg = "Sub-collection '{}' not found!"
|
|
588
|
-
raise Exit(msg.format(list_root))
|
|
589
|
-
self.list_tasks()
|
|
590
|
-
raise Exit
|
|
686
|
+
# Handle --list / --list-tasks / --list-plugins (raises Exit if triggered)
|
|
687
|
+
self._handle_list_flags()
|
|
591
688
|
|
|
592
689
|
# Print completion helpers if necessary
|
|
593
690
|
if self.args.complete.value:
|