invoke-toolkit 0.0.61__tar.gz → 0.0.63__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.61 → invoke_toolkit-0.0.63}/.gitignore +3 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/PKG-INFO +15 -7
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/pyproject.toml +27 -9
- invoke_toolkit-0.0.63/src/invoke_toolkit/config/status_helper.py +246 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/context/context.py +12 -3
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/executor.py +8 -3
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/config.py +81 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/program/program.py +11 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/runners/rich.py +61 -1
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/tasks.py +11 -4
- invoke_toolkit-0.0.63/tests/program/tasks/__init__.py +0 -0
- invoke_toolkit-0.0.63/tests/test_invoke_3_compat.py +184 -0
- invoke_toolkit-0.0.61/src/invoke_toolkit/config/status_helper.py +0 -121
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/LICENSE.txt +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/README.md +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/__main__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/collections.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/completion.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/config.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/registry.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/schema.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/context/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/context/types.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/create.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/loader/entrypoint.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/log/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/log/logger.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/output/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/output/console.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/output/utils.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/parser.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/program/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/program/main.py +0 -0
- /invoke_toolkit-0.0.61/src/invoke_toolkit/runners/__init__.py → /invoke_toolkit-0.0.63/src/invoke_toolkit/py.typed +0 -0
- {invoke_toolkit-0.0.61/src/invoke_toolkit/scripts → invoke_toolkit-0.0.63/src/invoke_toolkit/runners}/__init__.py +0 -0
- {invoke_toolkit-0.0.61/src/invoke_toolkit/utils → invoke_toolkit-0.0.63/src/invoke_toolkit/scripts}/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/scripts/loader.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/autocomplete.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/cache.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/types.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/testing.py +0 -0
- {invoke_toolkit-0.0.61/tests/program/tasks → invoke_toolkit-0.0.63/src/invoke_toolkit/utils}/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/fzf.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/inspection.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/singleton.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/text.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/.gitignore.jinja +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/README.md.jinja +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/copier.yml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/pyproject.toml.jinja +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/conftest.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/cached_completion/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/completion_with_config/.gitignore +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/completion_with_config/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/config_schema/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/enum_select_size/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/fzf_selector/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/literal_set_level/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/conftest.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_config_tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_create.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_package_template_entrypoint.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_shell_tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/collection.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/echo.yaml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/no-dedupe.yaml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/no-echo.yaml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/package/invoke.yml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/package/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/runtime.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/underscores/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yaml/explicit.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yaml/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yml/explicit.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yml/tasks.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/has_modules.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/conftest.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/test_config.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/program/main.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/program/tasks/coll1.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/script/test_script.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/tasks/test_cache.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/tasks/test_extensions_config.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_annotated_help.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_cofig_class.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_collection.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_collection_configure.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_completion_with_choices.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_config_helper.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_config_registry.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_config_schema.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_console.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_context_class.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_disable_status_cli.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_enum_arguments.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_executor.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_file_completion.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_global_context.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_help_flags.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_invoke_compatibility.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_loader.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_parsing.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_proctitle.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_program_with_collection.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_toplevel.py +0 -0
- {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/utils/test_fzf.py +0 -0
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invoke-toolkit
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.0.63
|
|
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
|
|
7
7
|
Project-URL: Source, https://github.com/D3f0/invoke-toolkit
|
|
8
8
|
Author-email: Nahuel Defossé <D3f0@users.noreply.github.com>
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE.txt
|
|
11
|
+
Keywords: automation,cli,completion,devtools,fzf,invoke,plugin,pyinvoke,rich,task
|
|
11
12
|
Classifier: Development Status :: 4 - Beta
|
|
12
13
|
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
13
15
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
14
16
|
Classifier: Operating System :: POSIX
|
|
15
17
|
Classifier: Operating System :: Unix
|
|
16
18
|
Classifier: Programming Language :: Python
|
|
17
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
24
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
25
|
Classifier: Topic :: Software Development
|
|
20
26
|
Classifier: Topic :: Software Development :: Build Tools
|
|
@@ -22,13 +28,15 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
22
28
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
29
|
Classifier: Topic :: System :: Software Distribution
|
|
24
30
|
Classifier: Topic :: System :: Systems Administration
|
|
31
|
+
Classifier: Topic :: Terminals
|
|
32
|
+
Classifier: Topic :: Utilities
|
|
33
|
+
Classifier: Typing :: Typed
|
|
25
34
|
Requires-Python: >=3.10
|
|
26
35
|
Requires-Dist: appdirs
|
|
27
36
|
Requires-Dist: attrs>=24.1.0
|
|
28
37
|
Requires-Dist: cattrs>=24.1.0
|
|
29
38
|
Requires-Dist: httpx>=0.28.1
|
|
30
|
-
Requires-Dist: invoke>=
|
|
31
|
-
Requires-Dist: pdbpp>=0.11.7
|
|
39
|
+
Requires-Dist: invoke>=3.0.3
|
|
32
40
|
Requires-Dist: platformdirs>=4.5.0
|
|
33
41
|
Requires-Dist: pyyaml>=6.0.3
|
|
34
42
|
Requires-Dist: rich>=13
|
|
@@ -38,17 +46,17 @@ Requires-Dist: typing-extensions>=4.13.2
|
|
|
38
46
|
Provides-Extra: cache
|
|
39
47
|
Requires-Dist: diskcache>=5.6.0; extra == 'cache'
|
|
40
48
|
Provides-Extra: copier
|
|
41
|
-
Requires-Dist: copier>=9.
|
|
49
|
+
Requires-Dist: copier>=9.16.0; extra == 'copier'
|
|
42
50
|
Provides-Extra: debug
|
|
43
51
|
Requires-Dist: hunter>=3.7.0; extra == 'debug'
|
|
44
52
|
Requires-Dist: pdbpp>=0.11.7; extra == 'debug'
|
|
45
53
|
Provides-Extra: full
|
|
46
|
-
Requires-Dist: copier>=9.
|
|
54
|
+
Requires-Dist: copier>=9.16.0; extra == 'full'
|
|
47
55
|
Requires-Dist: diskcache>=5.6.0; extra == 'full'
|
|
48
56
|
Requires-Dist: hunter>=3.7.0; extra == 'full'
|
|
49
57
|
Requires-Dist: pdbpp>=0.11.7; extra == 'full'
|
|
50
58
|
Provides-Extra: templating
|
|
51
|
-
Requires-Dist: copier>=9.
|
|
59
|
+
Requires-Dist: copier>=9.16.0; extra == 'templating'
|
|
52
60
|
Description-Content-Type: text/markdown
|
|
53
61
|
|
|
54
62
|
# invoke-toolkit
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "invoke-toolkit"
|
|
3
3
|
dynamic = ["version"]
|
|
4
|
-
description = "
|
|
4
|
+
description = "Extended APIs for PyInvoke: composable task plugins, typed CLI args, Rich output, shell completion, and PEP 723 script support"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = "MIT"
|
|
8
|
-
keywords = [
|
|
8
|
+
keywords = [
|
|
9
|
+
"invoke",
|
|
10
|
+
"task",
|
|
11
|
+
"cli",
|
|
12
|
+
"automation",
|
|
13
|
+
"plugin",
|
|
14
|
+
"completion",
|
|
15
|
+
"rich",
|
|
16
|
+
"pyinvoke",
|
|
17
|
+
"devtools",
|
|
18
|
+
"fzf",
|
|
19
|
+
]
|
|
9
20
|
authors = [
|
|
10
21
|
{ name = "Nahuel Defossé", email = "D3f0@users.noreply.github.com" },
|
|
11
22
|
]
|
|
12
23
|
classifiers = [
|
|
13
24
|
"Development Status :: 4 - Beta",
|
|
14
25
|
"Environment :: Console",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
15
27
|
"Programming Language :: Python",
|
|
16
28
|
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
17
33
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
18
34
|
"Operating System :: POSIX",
|
|
19
35
|
"Operating System :: Unix",
|
|
@@ -24,15 +40,17 @@ classifiers = [
|
|
|
24
40
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
41
|
"Topic :: System :: Software Distribution",
|
|
26
42
|
"Topic :: System :: Systems Administration",
|
|
43
|
+
"Topic :: Terminals",
|
|
44
|
+
"Topic :: Utilities",
|
|
45
|
+
"Typing :: Typed",
|
|
27
46
|
]
|
|
28
47
|
dependencies = [
|
|
29
|
-
"invoke>=
|
|
48
|
+
"invoke>=3.0.3",
|
|
30
49
|
"rich>=13",
|
|
31
50
|
"typing-extensions>=4.13.2",
|
|
32
51
|
"appdirs",
|
|
33
52
|
"tomlkit>=0.13.3",
|
|
34
53
|
"pyyaml>=6.0.3",
|
|
35
|
-
"pdbpp >=0.11.7",
|
|
36
54
|
"platformdirs>=4.5.0",
|
|
37
55
|
"httpx>=0.28.1",
|
|
38
56
|
"setproctitle>=1.3.7",
|
|
@@ -119,17 +137,17 @@ cache = [
|
|
|
119
137
|
"diskcache>=5.6.0",
|
|
120
138
|
]
|
|
121
139
|
copier = [
|
|
122
|
-
"copier>=9.
|
|
140
|
+
"copier>=9.16.0",
|
|
123
141
|
]
|
|
124
142
|
templating = [
|
|
125
|
-
"copier>=9.
|
|
143
|
+
"copier>=9.16.0",
|
|
126
144
|
]
|
|
127
145
|
debug = [
|
|
128
146
|
"hunter>=3.7.0",
|
|
129
147
|
"pdbpp>=0.11.7",
|
|
130
148
|
]
|
|
131
149
|
full = [
|
|
132
|
-
"copier>=9.
|
|
150
|
+
"copier>=9.16.0",
|
|
133
151
|
"diskcache>=5.6.0",
|
|
134
152
|
"hunter>=3.7.0",
|
|
135
153
|
"pdbpp>=0.11.7",
|
|
@@ -141,7 +159,7 @@ ci = [
|
|
|
141
159
|
]
|
|
142
160
|
dev = [
|
|
143
161
|
|
|
144
|
-
"copier>=9.
|
|
162
|
+
"copier>=9.16.0",
|
|
145
163
|
"diskcache>=5.6.0",
|
|
146
164
|
"hunter>=3.7.0",
|
|
147
165
|
"pdbpp>=0.11.7",
|
|
@@ -149,7 +167,7 @@ dev = [
|
|
|
149
167
|
"pre-commit-uv>=4.2.0",
|
|
150
168
|
"pylint>=3.2.7",
|
|
151
169
|
"pylint-per-file-ignores>=1.0.0",
|
|
152
|
-
"pytest>=
|
|
170
|
+
"pytest>=9.1.1",
|
|
153
171
|
"pytest-cov>=5.0.0",
|
|
154
172
|
"pytest-html>=4.1.1",
|
|
155
173
|
"pytest-picked>=0.5.1",
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class that implements the ctx.status through the config class
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Generator, Optional, Union
|
|
8
|
+
|
|
9
|
+
from rich.console import Console, Group, RenderableType
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.spinner import Spinner
|
|
13
|
+
from rich.status import Status
|
|
14
|
+
from rich.style import StyleType
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
from invoke_toolkit.output import get_console
|
|
18
|
+
|
|
19
|
+
MAX_OUTPUT_LINES = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NoOpStatus:
|
|
23
|
+
"""A no-op status object that prints messages on enter and exit"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
status: Optional[RenderableType] = None,
|
|
28
|
+
console: Optional[Console] = None,
|
|
29
|
+
):
|
|
30
|
+
self.status = status
|
|
31
|
+
self.console = console or get_console()
|
|
32
|
+
|
|
33
|
+
def __enter__(self) -> "NoOpStatus":
|
|
34
|
+
"""Print status message on enter"""
|
|
35
|
+
if self.status:
|
|
36
|
+
self.console.print(f"[bold blue]→[/bold blue] {self.status}")
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
40
|
+
"""Print status message on exit"""
|
|
41
|
+
if self.status:
|
|
42
|
+
self.console.print(f"[bold green]✓[/bold green] {self.status}")
|
|
43
|
+
|
|
44
|
+
def update(
|
|
45
|
+
self,
|
|
46
|
+
status: Optional[RenderableType] = None,
|
|
47
|
+
*,
|
|
48
|
+
spinner: Optional[str] = None,
|
|
49
|
+
spinner_style: Optional[StyleType] = None,
|
|
50
|
+
speed: Optional[float] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""No-op update"""
|
|
53
|
+
|
|
54
|
+
def stop(self) -> None:
|
|
55
|
+
"""No-op stop"""
|
|
56
|
+
|
|
57
|
+
def capture_line(self, line: str) -> None:
|
|
58
|
+
"""No-op capture"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class VerboseStatus: # pylint: disable=too-many-instance-attributes
|
|
62
|
+
"""Status that shows the last N lines of command output below the spinner."""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
status: RenderableType,
|
|
67
|
+
*,
|
|
68
|
+
console: Optional[Console] = None,
|
|
69
|
+
spinner: str = "dots",
|
|
70
|
+
spinner_style: StyleType = "status.spinner",
|
|
71
|
+
speed: float = 1.0,
|
|
72
|
+
refresh_per_second: float = 12.5,
|
|
73
|
+
max_lines: int = MAX_OUTPUT_LINES,
|
|
74
|
+
):
|
|
75
|
+
self.status = status
|
|
76
|
+
self.spinner_style = spinner_style
|
|
77
|
+
self.speed = speed
|
|
78
|
+
self._lines: deque[str] = deque(maxlen=max_lines)
|
|
79
|
+
self._spinner = Spinner(spinner, text=status, style=spinner_style, speed=speed)
|
|
80
|
+
self._console = console or get_console()
|
|
81
|
+
self._live = Live(
|
|
82
|
+
self._build_renderable(),
|
|
83
|
+
console=self._console,
|
|
84
|
+
refresh_per_second=refresh_per_second,
|
|
85
|
+
transient=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _build_renderable(self) -> RenderableType:
|
|
89
|
+
"""Build the composite renderable: spinner + output panel."""
|
|
90
|
+
if not self._lines:
|
|
91
|
+
return self._spinner
|
|
92
|
+
output_text = Text("\n".join(self._lines), style="dim")
|
|
93
|
+
panel = Panel(
|
|
94
|
+
output_text,
|
|
95
|
+
border_style="dim",
|
|
96
|
+
padding=(0, 1),
|
|
97
|
+
expand=True,
|
|
98
|
+
)
|
|
99
|
+
return Group(self._spinner, panel)
|
|
100
|
+
|
|
101
|
+
def capture_line(self, line: str) -> None:
|
|
102
|
+
"""Append a line of output and refresh the live display."""
|
|
103
|
+
self._lines.append(line.rstrip("\n\r"))
|
|
104
|
+
self._live.update(self._build_renderable())
|
|
105
|
+
|
|
106
|
+
def update(
|
|
107
|
+
self,
|
|
108
|
+
status: Optional[RenderableType] = None,
|
|
109
|
+
*,
|
|
110
|
+
spinner: Optional[str] = None,
|
|
111
|
+
spinner_style: Optional[StyleType] = None,
|
|
112
|
+
speed: Optional[float] = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Update the status text or spinner parameters."""
|
|
115
|
+
if status is not None:
|
|
116
|
+
self.status = status
|
|
117
|
+
if spinner_style is not None:
|
|
118
|
+
self.spinner_style = spinner_style
|
|
119
|
+
if speed is not None:
|
|
120
|
+
self.speed = speed
|
|
121
|
+
if spinner is not None:
|
|
122
|
+
self._spinner = Spinner(
|
|
123
|
+
spinner, text=self.status, style=self.spinner_style, speed=self.speed
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
self._spinner.update(
|
|
127
|
+
text=self.status, style=self.spinner_style, speed=self.speed
|
|
128
|
+
)
|
|
129
|
+
self._live.update(self._build_renderable())
|
|
130
|
+
|
|
131
|
+
def start(self) -> None:
|
|
132
|
+
"""Start the live display."""
|
|
133
|
+
self._live.start()
|
|
134
|
+
|
|
135
|
+
def stop(self) -> None:
|
|
136
|
+
"""Stop the live display."""
|
|
137
|
+
self._live.stop()
|
|
138
|
+
|
|
139
|
+
def __enter__(self) -> "VerboseStatus":
|
|
140
|
+
self.start()
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
144
|
+
self.stop()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class StatusHelper:
|
|
148
|
+
"""
|
|
149
|
+
A bridge to insert rich's status bound to a console into a invoke config, so
|
|
150
|
+
it can be accessed from the task's context
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
_current_status: Optional[Union[Status, VerboseStatus]]
|
|
154
|
+
_disabled: bool
|
|
155
|
+
_show_command_output: bool
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
console: Console,
|
|
160
|
+
disabled: bool = False,
|
|
161
|
+
show_command_output: bool = False,
|
|
162
|
+
):
|
|
163
|
+
self.console = console or get_console()
|
|
164
|
+
self._current_status = None
|
|
165
|
+
self._disabled = disabled
|
|
166
|
+
self._show_command_output = show_command_output
|
|
167
|
+
|
|
168
|
+
@contextmanager
|
|
169
|
+
def status(
|
|
170
|
+
self,
|
|
171
|
+
status: RenderableType,
|
|
172
|
+
*,
|
|
173
|
+
spinner: str = "dots",
|
|
174
|
+
spinner_style: StyleType = "status.spinner",
|
|
175
|
+
speed: float = 1.0,
|
|
176
|
+
refresh_per_second: float = 12.5,
|
|
177
|
+
) -> Generator[Union[Status, VerboseStatus], None, None]:
|
|
178
|
+
"""Context manager for status management"""
|
|
179
|
+
if self._disabled:
|
|
180
|
+
with NoOpStatus(status=status, console=self.console) as noop:
|
|
181
|
+
yield noop # type: ignore[misc]
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if self._current_status is not None:
|
|
185
|
+
# Nested status: update the existing one
|
|
186
|
+
self._current_status.update(
|
|
187
|
+
status, spinner=spinner, spinner_style=spinner_style, speed=speed
|
|
188
|
+
)
|
|
189
|
+
yield self._current_status
|
|
190
|
+
elif self._show_command_output:
|
|
191
|
+
verbose = VerboseStatus(
|
|
192
|
+
status=status,
|
|
193
|
+
console=self.console,
|
|
194
|
+
spinner=spinner,
|
|
195
|
+
spinner_style=spinner_style,
|
|
196
|
+
speed=speed,
|
|
197
|
+
refresh_per_second=refresh_per_second,
|
|
198
|
+
)
|
|
199
|
+
with verbose as self._current_status: # type: ignore[assignment]
|
|
200
|
+
yield self._current_status
|
|
201
|
+
self._current_status = None
|
|
202
|
+
else:
|
|
203
|
+
with self.console.status(
|
|
204
|
+
status=status,
|
|
205
|
+
spinner=spinner,
|
|
206
|
+
spinner_style=spinner_style,
|
|
207
|
+
speed=speed,
|
|
208
|
+
refresh_per_second=refresh_per_second,
|
|
209
|
+
) as self._current_status:
|
|
210
|
+
yield self._current_status
|
|
211
|
+
self._current_status = None
|
|
212
|
+
|
|
213
|
+
def capture_line(self, line: str) -> None:
|
|
214
|
+
"""Feed a line of output to the current verbose status display."""
|
|
215
|
+
if isinstance(self._current_status, VerboseStatus):
|
|
216
|
+
self._current_status.capture_line(line)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def is_verbose(self) -> bool:
|
|
220
|
+
"""Whether the current status is a VerboseStatus."""
|
|
221
|
+
return isinstance(self._current_status, VerboseStatus)
|
|
222
|
+
|
|
223
|
+
def status_update(
|
|
224
|
+
self,
|
|
225
|
+
status: Optional[RenderableType] = None,
|
|
226
|
+
*,
|
|
227
|
+
spinner: Optional[str] = None,
|
|
228
|
+
spinner_style: Optional[StyleType] = None,
|
|
229
|
+
speed: Optional[float] = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Wrapper on Status.update"""
|
|
232
|
+
if self._disabled:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
if self._current_status:
|
|
236
|
+
self._current_status.update(
|
|
237
|
+
status, spinner=spinner, spinner_style=spinner_style, speed=speed
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def status_stop(self) -> None:
|
|
241
|
+
"""Cancels the status. This will allow to use the REPL in debugging breakpoints"""
|
|
242
|
+
if self._disabled:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
if self._current_status:
|
|
246
|
+
self._current_status.stop()
|
|
@@ -70,15 +70,24 @@ class ToolkitContext(Context, ConfigProtocol):
|
|
|
70
70
|
[Union[PathLike[str], str]], _GeneratorContextManager[None, None, None]
|
|
71
71
|
]
|
|
72
72
|
|
|
73
|
-
def __init__(
|
|
74
|
-
|
|
73
|
+
def __init__(
|
|
74
|
+
self, config: Optional[ToolkitConfig] = None, remainder: str = ""
|
|
75
|
+
) -> None:
|
|
76
|
+
super().__init__(config, remainder=remainder)
|
|
75
77
|
self._set("_console", get_console())
|
|
76
78
|
# Check if status is disabled in the config
|
|
77
79
|
disabled = False
|
|
80
|
+
show_command_output = False
|
|
78
81
|
if config and hasattr(config, "get"):
|
|
79
82
|
disabled = config.get("disable_status", False)
|
|
83
|
+
show_command_output = config.get("show_command_output", False)
|
|
80
84
|
self._set(
|
|
81
|
-
"_status_helper",
|
|
85
|
+
"_status_helper",
|
|
86
|
+
StatusHelper(
|
|
87
|
+
console=self._console,
|
|
88
|
+
disabled=disabled,
|
|
89
|
+
show_command_output=show_command_output,
|
|
90
|
+
),
|
|
82
91
|
)
|
|
83
92
|
|
|
84
93
|
@property
|
|
@@ -39,11 +39,16 @@ class ToolkitExecutor(Executor):
|
|
|
39
39
|
|
|
40
40
|
:param core:
|
|
41
41
|
An optional `.ParseResult` holding parsed core program arguments.
|
|
42
|
-
Defaults to
|
|
42
|
+
Defaults to an empty `.ParseResult` if not given.
|
|
43
|
+
|
|
44
|
+
.. versionchanged:: invoke 3.0
|
|
45
|
+
``self.core`` now defaults to ``ParseResult()`` instead of
|
|
46
|
+
``None``, matching upstream ``Executor`` behaviour and ensuring
|
|
47
|
+
``core_parse_result.remainder`` is always accessible.
|
|
43
48
|
"""
|
|
44
49
|
self.collection = collection
|
|
45
50
|
self.config = config if config is not None else ToolkitConfig()
|
|
46
|
-
self.core = core
|
|
51
|
+
self.core = core if core is not None else ParseResult()
|
|
47
52
|
|
|
48
53
|
def execute(
|
|
49
54
|
self, *tasks: Union[str, Tuple[str, Dict[str, Any]], ParserContext]
|
|
@@ -134,7 +139,7 @@ class ToolkitExecutor(Executor):
|
|
|
134
139
|
# Get final context from the Call (which will know how to generate
|
|
135
140
|
# an appropriate one; e.g. subclasses might use extra data from
|
|
136
141
|
# being parameterized), handing in this config for use there.
|
|
137
|
-
context = call.make_context(config) # type: ignore[attr-defined]
|
|
142
|
+
context = call.make_context(config, core_parse_result=self.core) # type: ignore[attr-defined]
|
|
138
143
|
args = (context, *call.args) # type: ignore[attr-defined]
|
|
139
144
|
result = call.task(*args, **call.kwargs) # type: ignore[attr-defined]
|
|
140
145
|
if autoprint:
|
{invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/config.py
RENAMED
|
@@ -195,6 +195,38 @@ def _set_nested_value(config_dict: dict, path: str, value: Any) -> dict:
|
|
|
195
195
|
return config_dict
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def _remove_nested_value(config_dict: dict, path: str) -> dict:
|
|
199
|
+
"""Remove a value from a nested dictionary using dot notation.
|
|
200
|
+
Args:
|
|
201
|
+
config_dict: The config dictionary to modify
|
|
202
|
+
path: Dot-separated path (e.g., 'group.subgroup.key')
|
|
203
|
+
Returns:
|
|
204
|
+
The modified config dictionary
|
|
205
|
+
"""
|
|
206
|
+
keys = path.split(".")
|
|
207
|
+
path_dicts = [config_dict]
|
|
208
|
+
for key in keys[:-1]:
|
|
209
|
+
last_dict = path_dicts[-1]
|
|
210
|
+
if key in last_dict and isinstance(last_dict.get(key), dict):
|
|
211
|
+
path_dicts.append(last_dict[key])
|
|
212
|
+
else:
|
|
213
|
+
# Path doesn't exist
|
|
214
|
+
return config_dict
|
|
215
|
+
|
|
216
|
+
# Remove the final key
|
|
217
|
+
if keys[-1] in path_dicts[-1]:
|
|
218
|
+
del path_dicts[-1][keys[-1]]
|
|
219
|
+
|
|
220
|
+
# Clean up empty parent dictionaries
|
|
221
|
+
for i in range(len(path_dicts) - 2, -1, -1):
|
|
222
|
+
if not path_dicts[i + 1]:
|
|
223
|
+
del path_dicts[i][keys[i]]
|
|
224
|
+
else:
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
return config_dict
|
|
228
|
+
|
|
229
|
+
|
|
198
230
|
def _load_config_file(path: Path) -> dict:
|
|
199
231
|
"""Load a config file and return its contents as a dictionary.
|
|
200
232
|
|
|
@@ -676,6 +708,55 @@ def set_(
|
|
|
676
708
|
ctx.print(f" [dim]Saved to: {config_path}[/dim]")
|
|
677
709
|
|
|
678
710
|
|
|
711
|
+
@task(
|
|
712
|
+
name="unset",
|
|
713
|
+
help={
|
|
714
|
+
"path": "Dot-separated path to unset (e.g., 'run.echo', 'custom.api_key')",
|
|
715
|
+
"location": "Where to save: 'local' (default), 'user', or 'system'",
|
|
716
|
+
},
|
|
717
|
+
)
|
|
718
|
+
def unset(
|
|
719
|
+
ctx: Context,
|
|
720
|
+
path: Annotated[str | None, _complete_config_path] = None,
|
|
721
|
+
location: ConfigLocation = ConfigLocation.LOCAL,
|
|
722
|
+
):
|
|
723
|
+
"""
|
|
724
|
+
Unset a configuration value in a config file.
|
|
725
|
+
|
|
726
|
+
Examples:
|
|
727
|
+
invoke-toolkit -x config.unset run.echo
|
|
728
|
+
invoke-toolkit -x config.unset custom.ports
|
|
729
|
+
"""
|
|
730
|
+
if path is None:
|
|
731
|
+
ctx.print_err(
|
|
732
|
+
"[red]Error:[/red] 'path' is required.\n"
|
|
733
|
+
"[dim]Usage: config.unset <path>[/dim]\n"
|
|
734
|
+
"[dim]Use --help for more information.[/dim]"
|
|
735
|
+
)
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
config_path = _find_existing_config_file(ctx, location)
|
|
739
|
+
if not config_path or not config_path.exists():
|
|
740
|
+
ctx.print_err(
|
|
741
|
+
f"[yellow]No config file found at location '{location}'. Nothing to do.[/yellow]"
|
|
742
|
+
)
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
config_dict = _load_config_file(config_path)
|
|
746
|
+
original_config = config_dict.copy()
|
|
747
|
+
config_dict = _remove_nested_value(config_dict, path)
|
|
748
|
+
|
|
749
|
+
if config_dict == original_config:
|
|
750
|
+
ctx.print_err(
|
|
751
|
+
f"[yellow]Path '[bold]{path}[/bold]' not found in the configuration. Nothing to do.[/yellow]"
|
|
752
|
+
)
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
_save_config_file(config_path, config_dict)
|
|
756
|
+
ctx.print(f"[green]✓[/green] Unset [bold]{path}[/bold]")
|
|
757
|
+
ctx.print(f" [dim]Saved to: {config_path}[/dim]")
|
|
758
|
+
|
|
759
|
+
|
|
679
760
|
def _print_nested_schema(
|
|
680
761
|
ctx: Context, prefix: str, data: dict, indent: int = 2
|
|
681
762
|
) -> None:
|
|
@@ -195,6 +195,11 @@ class ToolkitProgram(Program):
|
|
|
195
195
|
debug("Disabling status context manager via CLI flag")
|
|
196
196
|
self.config["disable_status"] = True # type: ignore[index]
|
|
197
197
|
|
|
198
|
+
# Update config with show_command_output flag from CLI
|
|
199
|
+
if self.args["show_command_output"].value:
|
|
200
|
+
debug("Enabling command output display below status via CLI flag")
|
|
201
|
+
self.config["show_command_output"] = True # type: ignore[index]
|
|
202
|
+
|
|
198
203
|
# Update config with init_shell flag from CLI
|
|
199
204
|
if self.args["init_shell"].value:
|
|
200
205
|
debug("Initializing shell for runner via CLI flag")
|
|
@@ -351,6 +356,12 @@ class ToolkitProgram(Program):
|
|
|
351
356
|
default=False,
|
|
352
357
|
help="Disables the rich status context manager for debugging (useful when debugging breakpoints or using REPL)",
|
|
353
358
|
),
|
|
359
|
+
ToolkitArgument(
|
|
360
|
+
names=("show_command_output", "O"),
|
|
361
|
+
kind=bool,
|
|
362
|
+
default=False,
|
|
363
|
+
help="Shows last few lines of command output below the status spinner",
|
|
364
|
+
),
|
|
354
365
|
ToolkitArgument(
|
|
355
366
|
names=("init_shell",),
|
|
356
367
|
kind=bool,
|
|
@@ -4,6 +4,7 @@ in tasks.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import sys
|
|
7
|
+
from typing import TYPE_CHECKING, Optional
|
|
7
8
|
|
|
8
9
|
from invoke.runners import Local
|
|
9
10
|
from invoke.util import debug
|
|
@@ -11,6 +12,9 @@ from rich.syntax import Syntax
|
|
|
11
12
|
|
|
12
13
|
from invoke_toolkit.output import get_console
|
|
13
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from invoke_toolkit.config.status_helper import StatusHelper
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
class RedactingStream:
|
|
16
20
|
"""A file-like wrapper that redacts secrets before writing output."""
|
|
@@ -51,6 +55,46 @@ class RedactingStream:
|
|
|
51
55
|
return getattr(self.original_stream, name)
|
|
52
56
|
|
|
53
57
|
|
|
58
|
+
class OutputCapturingStream:
|
|
59
|
+
"""A file-like wrapper that captures output lines and feeds them to a StatusHelper."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, status_helper: "StatusHelper", original_stream):
|
|
62
|
+
self._status_helper = status_helper
|
|
63
|
+
self.original_stream = original_stream
|
|
64
|
+
self.encoding = getattr(original_stream, "encoding", "utf-8") or "utf-8"
|
|
65
|
+
self._buffer = ""
|
|
66
|
+
|
|
67
|
+
def write(self, text: str) -> int:
|
|
68
|
+
"""Write text, splitting into lines for capture."""
|
|
69
|
+
if not text:
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
self._buffer += text
|
|
73
|
+
while "\n" in self._buffer:
|
|
74
|
+
line, self._buffer = self._buffer.split("\n", 1)
|
|
75
|
+
self._status_helper.capture_line(line)
|
|
76
|
+
|
|
77
|
+
return len(text)
|
|
78
|
+
|
|
79
|
+
def flush(self) -> None:
|
|
80
|
+
"""Flush remaining buffer content."""
|
|
81
|
+
if self._buffer:
|
|
82
|
+
self._status_helper.capture_line(self._buffer)
|
|
83
|
+
self._buffer = ""
|
|
84
|
+
if hasattr(self.original_stream, "flush"):
|
|
85
|
+
self.original_stream.flush()
|
|
86
|
+
|
|
87
|
+
def isatty(self) -> bool:
|
|
88
|
+
"""Return whether the underlying stream is a tty."""
|
|
89
|
+
if hasattr(self.original_stream, "isatty"):
|
|
90
|
+
return self.original_stream.isatty()
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def __getattr__(self, name):
|
|
94
|
+
"""Delegate other attributes to the original stream."""
|
|
95
|
+
return getattr(self.original_stream, name)
|
|
96
|
+
|
|
97
|
+
|
|
54
98
|
class NoStdoutRunner(Local):
|
|
55
99
|
"""Invoke runner that prints to stderr when invoke is used with -e/--echo
|
|
56
100
|
and redacts secrets from subprocess output when redaction is enabled.
|
|
@@ -65,8 +109,13 @@ class NoStdoutRunner(Local):
|
|
|
65
109
|
debug("context is missing print")
|
|
66
110
|
print(self.opts["echo_format"].format(command=command), file=sys.stderr)
|
|
67
111
|
|
|
112
|
+
def _get_status_helper(self) -> "Optional[StatusHelper]":
|
|
113
|
+
if hasattr(self.context, "_status_helper"):
|
|
114
|
+
return self.context._status_helper # pylint: disable=protected-access
|
|
115
|
+
return None
|
|
116
|
+
|
|
68
117
|
def run(self, command, **kwargs):
|
|
69
|
-
"""Execute command with redacting streams
|
|
118
|
+
"""Execute command with redacting streams and optional output capture."""
|
|
70
119
|
# Get the configured console objects
|
|
71
120
|
out_console = get_console("out")
|
|
72
121
|
err_console = get_console("err")
|
|
@@ -92,4 +141,15 @@ class NoStdoutRunner(Local):
|
|
|
92
141
|
f"Running with redacting streams: {has_out_redaction=}, {has_err_redaction=}"
|
|
93
142
|
)
|
|
94
143
|
|
|
144
|
+
# If we're inside a verbose status, capture output for the panel
|
|
145
|
+
status_helper = self._get_status_helper()
|
|
146
|
+
if status_helper and status_helper.is_verbose:
|
|
147
|
+
out_stream = kwargs.get("out_stream") or sys.stdout
|
|
148
|
+
err_stream = kwargs.get("err_stream") or sys.stderr
|
|
149
|
+
kwargs["out_stream"] = OutputCapturingStream(status_helper, out_stream)
|
|
150
|
+
kwargs["err_stream"] = OutputCapturingStream(status_helper, err_stream)
|
|
151
|
+
# Disable hide so output flows through our capturing stream
|
|
152
|
+
# (the stream feeds the Live panel instead of printing to terminal)
|
|
153
|
+
kwargs["hide"] = False
|
|
154
|
+
|
|
95
155
|
return super().run(command, **kwargs)
|