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.
Files changed (128) hide show
  1. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/.gitignore +3 -0
  2. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/PKG-INFO +15 -7
  3. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/pyproject.toml +27 -9
  4. invoke_toolkit-0.0.63/src/invoke_toolkit/config/status_helper.py +246 -0
  5. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/context/context.py +12 -3
  6. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/executor.py +8 -3
  7. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/config.py +81 -0
  8. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/program/program.py +11 -0
  9. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/runners/rich.py +61 -1
  10. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/tasks.py +11 -4
  11. invoke_toolkit-0.0.63/tests/program/tasks/__init__.py +0 -0
  12. invoke_toolkit-0.0.63/tests/test_invoke_3_compat.py +184 -0
  13. invoke_toolkit-0.0.61/src/invoke_toolkit/config/status_helper.py +0 -121
  14. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/LICENSE.txt +0 -0
  15. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/README.md +0 -0
  16. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/__init__.py +0 -0
  17. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/__main__.py +0 -0
  18. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/collections.py +0 -0
  19. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/completion.py +0 -0
  20. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/__init__.py +0 -0
  21. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/config.py +0 -0
  22. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/registry.py +0 -0
  23. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/config/schema.py +0 -0
  24. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/context/__init__.py +0 -0
  25. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/context/types.py +0 -0
  26. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/__init__.py +0 -0
  27. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
  28. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/create.py +0 -0
  29. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
  30. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
  31. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/loader/entrypoint.py +0 -0
  32. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/log/__init__.py +0 -0
  33. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/log/logger.py +0 -0
  34. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/output/__init__.py +0 -0
  35. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/output/console.py +0 -0
  36. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/output/utils.py +0 -0
  37. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/parser.py +0 -0
  38. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/program/__init__.py +0 -0
  39. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/program/main.py +0 -0
  40. /invoke_toolkit-0.0.61/src/invoke_toolkit/runners/__init__.py → /invoke_toolkit-0.0.63/src/invoke_toolkit/py.typed +0 -0
  41. {invoke_toolkit-0.0.61/src/invoke_toolkit/scripts → invoke_toolkit-0.0.63/src/invoke_toolkit/runners}/__init__.py +0 -0
  42. {invoke_toolkit-0.0.61/src/invoke_toolkit/utils → invoke_toolkit-0.0.63/src/invoke_toolkit/scripts}/__init__.py +0 -0
  43. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/scripts/loader.py +0 -0
  44. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/__init__.py +0 -0
  45. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/autocomplete.py +0 -0
  46. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/cache.py +0 -0
  47. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/tasks/types.py +0 -0
  48. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/testing.py +0 -0
  49. {invoke_toolkit-0.0.61/tests/program/tasks → invoke_toolkit-0.0.63/src/invoke_toolkit/utils}/__init__.py +0 -0
  50. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/fzf.py +0 -0
  51. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/inspection.py +0 -0
  52. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/singleton.py +0 -0
  53. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/src/invoke_toolkit/utils/text.py +0 -0
  54. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/.gitignore.jinja +0 -0
  55. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/README.md.jinja +0 -0
  56. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/copier.yml +0 -0
  57. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/pyproject.toml.jinja +0 -0
  58. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
  59. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
  60. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/__init__.py +0 -0
  61. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/conftest.py +0 -0
  62. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/cached_completion/tasks.py +0 -0
  63. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/completion_with_config/.gitignore +0 -0
  64. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/completion_with_config/tasks.py +0 -0
  65. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/config_schema/tasks.py +0 -0
  66. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/enum_select_size/tasks.py +0 -0
  67. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/fzf_selector/tasks.py +0 -0
  68. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/examples/literal_set_level/tasks.py +0 -0
  69. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/conftest.py +0 -0
  70. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_config_tasks.py +0 -0
  71. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_create.py +0 -0
  72. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_package_template_entrypoint.py +0 -0
  73. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/extensions/test_shell_tasks.py +0 -0
  74. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/__init__.py +0 -0
  75. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
  76. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
  77. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
  78. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/collection.py +0 -0
  79. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/echo.yaml +0 -0
  80. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
  81. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
  82. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
  83. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/no-dedupe.yaml +0 -0
  84. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/no-echo.yaml +0 -0
  85. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/package/invoke.yml +0 -0
  86. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/package/tasks/__init__.py +0 -0
  87. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
  88. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/runtime.py +0 -0
  89. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
  90. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
  91. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
  92. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/underscores/tasks.py +0 -0
  93. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yaml/explicit.py +0 -0
  94. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yaml/tasks.py +0 -0
  95. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yml/explicit.py +0 -0
  96. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
  97. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/configs/yml/tasks.py +0 -0
  98. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/_support/has_modules.py +0 -0
  99. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/conftest.py +0 -0
  100. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/original_invoke/test_config.py +0 -0
  101. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/program/main.py +0 -0
  102. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/program/tasks/coll1.py +0 -0
  103. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/script/test_script.py +0 -0
  104. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/tasks/test_cache.py +0 -0
  105. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/tasks/test_extensions_config.py +0 -0
  106. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_annotated_help.py +0 -0
  107. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_cofig_class.py +0 -0
  108. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_collection.py +0 -0
  109. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_collection_configure.py +0 -0
  110. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_completion_with_choices.py +0 -0
  111. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_config_helper.py +0 -0
  112. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_config_registry.py +0 -0
  113. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_config_schema.py +0 -0
  114. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_console.py +0 -0
  115. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_context_class.py +0 -0
  116. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_disable_status_cli.py +0 -0
  117. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_enum_arguments.py +0 -0
  118. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_executor.py +0 -0
  119. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_file_completion.py +0 -0
  120. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_global_context.py +0 -0
  121. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_help_flags.py +0 -0
  122. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_invoke_compatibility.py +0 -0
  123. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_loader.py +0 -0
  124. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_parsing.py +0 -0
  125. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_proctitle.py +0 -0
  126. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_program_with_collection.py +0 -0
  127. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/test_toplevel.py +0 -0
  128. {invoke_toolkit-0.0.61 → invoke_toolkit-0.0.63}/tests/utils/test_fzf.py +0 -0
@@ -15,3 +15,6 @@ _scratchpad.py
15
15
  _[a-z]*.py
16
16
  _test/*
17
17
  invoke.yaml
18
+ # Makrdowns
19
+ *.md
20
+ !docs/**/*.md
@@ -1,20 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invoke-toolkit
3
- Version: 0.0.61
4
- Summary: A set of extended APIs for PyInvoke for composable scripts, plugins and richer output
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>=2.2.1
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.10.3; extra == 'copier'
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.10.3; extra == 'full'
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.10.3; extra == 'templating'
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 = "A set of extended APIs for PyInvoke for composable scripts, plugins and richer output"
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>=2.2.1",
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.10.3",
140
+ "copier>=9.16.0",
123
141
  ]
124
142
  templating = [
125
- "copier>=9.10.3",
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.10.3",
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.10.3",
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>=8.3.5",
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__(self, config: Optional[ToolkitConfig] = None) -> None:
74
- super().__init__(config)
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", StatusHelper(console=self._console, disabled=disabled)
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 ``None``.
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:
@@ -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 if redaction is enabled."""
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)