invoke-toolkit 0.0.65__tar.gz → 0.0.66__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.65 → invoke_toolkit-0.0.66}/PKG-INFO +69 -1
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/README.md +68 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/pyproject.toml +15 -2
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/__init__.py +3 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/collections.py +15 -6
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/completion.py +20 -29
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/config/config.py +14 -14
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/config/registry.py +4 -3
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/config/schema.py +3 -2
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/config/status_helper.py +19 -18
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/context/async_tools.py +3 -2
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/context/context.py +13 -23
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/context/types.py +18 -16
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/executor.py +75 -57
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/extensions/tasks/config.py +4 -6
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/extensions/tasks/create.py +106 -55
- invoke_toolkit-0.0.66/src/invoke_toolkit/field_resolvers.py +333 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/output/console.py +19 -19
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/parser.py +12 -6
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/program/program.py +17 -18
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/runners/rich.py +2 -2
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/scripts/loader.py +2 -3
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/tasks/__init__.py +1 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/tasks/autocomplete.py +4 -3
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/tasks/cache.py +40 -73
- invoke_toolkit-0.0.66/src/invoke_toolkit/tasks/fields.py +121 -0
- invoke_toolkit-0.0.66/src/invoke_toolkit/tasks/fields.pyi +79 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/tasks/tasks.py +345 -113
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/testing.py +1 -2
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/utils/fzf.py +7 -7
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/utils/inspection.py +1 -2
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/utils/singleton.py +2 -1
- invoke_toolkit-0.0.66/templates/provider-template/.gitignore.jinja +6 -0
- invoke_toolkit-0.0.66/templates/provider-template/README.md.jinja +45 -0
- invoke_toolkit-0.0.66/templates/provider-template/copier.yml +52 -0
- invoke_toolkit-0.0.66/templates/provider-template/pyproject.toml.jinja +39 -0
- invoke_toolkit-0.0.66/templates/provider-template/src/{{package_slug}}/__init__.py.jinja +23 -0
- invoke_toolkit-0.0.66/templates/provider-template/tests/test_resolver.py.jinja +11 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/conftest.py +5 -8
- invoke_toolkit-0.0.66/tests/examples/local_field_resolver/tasks.py +47 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/extensions/test_create.py +81 -2
- invoke_toolkit-0.0.66/tests/fixtures/future_fields.py +11 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_annotated_help.py +1 -1
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_async_support.py +32 -3
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_completion_with_choices.py +0 -9
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_config_registry.py +2 -4
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_enum_arguments.py +1 -1
- invoke_toolkit-0.0.66/tests/test_field_typing.py +28 -0
- invoke_toolkit-0.0.66/tests/test_fields.py +895 -0
- invoke_toolkit-0.0.66/tests/typecheck/fields.py +51 -0
- invoke_toolkit-0.0.66/tests/typecheck/fields_negative.py.fixture +18 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/.gitignore +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/LICENSE.txt +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/__main__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/config/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/context/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/extensions/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/loader/entrypoint.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/log/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/log/logger.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/output/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/output/utils.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/program/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/program/main.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/py.typed +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/runners/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/scripts/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/tasks/types.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/utils/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/src/invoke_toolkit/utils/text.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/templates/package-template/.gitignore.jinja +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/templates/package-template/README.md.jinja +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/templates/package-template/copier.yml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/templates/package-template/pyproject.toml.jinja +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/cached_completion/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/completion_with_config/.gitignore +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/completion_with_config/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/config_schema/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/enum_select_size/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/fzf_selector/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/literal_set_level/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/local_tasks/local_tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/examples/local_tasks/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/extensions/conftest.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/extensions/test_config_tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/extensions/test_package_template_entrypoint.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/extensions/test_shell_tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/collection.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/echo.yaml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/no-dedupe.yaml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/no-echo.yaml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/package/invoke.yml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/package/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/runtime.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/underscores/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/yaml/explicit.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/yaml/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/yml/explicit.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/configs/yml/tasks.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/_support/has_modules.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/conftest.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/original_invoke/test_config.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/program/main.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/program/tasks/__init__.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/program/tasks/coll1.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/script/test_script.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/tasks/test_cache.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/tasks/test_extensions_config.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_alias_self_reference.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_async_edge_cases.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_cofig_class.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_collection.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_collection_configure.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_config_helper.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_config_schema.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_console.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_context_class.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_disable_status_cli.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_executor.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_file_completion.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_global_context.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_help_flags.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_invoke_3_compat.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_invoke_compatibility.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_list_filter_flags.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_loader.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_parsing.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_proctitle.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_program_with_collection.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/test_toplevel.py +0 -0
- {invoke_toolkit-0.0.65 → invoke_toolkit-0.0.66}/tests/utils/test_fzf.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: invoke-toolkit
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.66
|
|
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
|
|
@@ -167,6 +167,74 @@ To enable caching, install with the cache extra:
|
|
|
167
167
|
pip install invoke-toolkit[cache]
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
## Dynamic task defaults
|
|
171
|
+
|
|
172
|
+
Use `Field` when an argument default must be computed from the final task
|
|
173
|
+
context or resolved from a URI. Explicit command-line values always take
|
|
174
|
+
precedence.
|
|
175
|
+
|
|
176
|
+
Bind a resolver once, then reuse the resulting callable for scalar and file
|
|
177
|
+
defaults. A local resolver always returns one string value per request. The
|
|
178
|
+
resolver receives every URI using that callback and scheme in one batch.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from invoke_toolkit import Context, Field, FilePath, task
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def resolve_bw(ctx: Context, requests: list) -> dict[str, str]:
|
|
185
|
+
return {
|
|
186
|
+
request.parameter: ctx.run("bw get password ...", hide=True).stdout.strip()
|
|
187
|
+
for request in requests
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
BitwardenField = Field(resolver=resolve_bw)
|
|
192
|
+
ExistingFile = FilePath(exists=True, dir_okay=False)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@task
|
|
196
|
+
def deploy(
|
|
197
|
+
ctx: Context,
|
|
198
|
+
password: str = BitwardenField(default="bw://PASSWORD_ID"),
|
|
199
|
+
config: ExistingFile = BitwardenField(
|
|
200
|
+
default="bw://CONFIG_ID", cleanup="task"
|
|
201
|
+
),
|
|
202
|
+
) -> None:
|
|
203
|
+
...
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For `str` fields, the resolver string reaches the task unchanged. For `Path` or
|
|
207
|
+
`FilePath` fields, `Field.create_temporary_file()` writes that string to a
|
|
208
|
+
managed temporary file and passes its `Path` to the task. Override that method
|
|
209
|
+
in a `Field` subclass to control file creation; resolvers do not return paths.
|
|
210
|
+
|
|
211
|
+
For omitted arguments, lookup precedence is explicit CLI/Python/call value,
|
|
212
|
+
then `INVOKE_<PARAMETER>` environment value, then the resolved `ctx.config`
|
|
213
|
+
value, then the declared `Field` default or factory. URI values from config use
|
|
214
|
+
the same local or entry-point resolver dispatch as declared URI defaults.
|
|
215
|
+
|
|
216
|
+
`Field(default_factory=callback)` runs only if no higher-precedence value is
|
|
217
|
+
available. Factories and resolvers never run during help, listing, or shell
|
|
218
|
+
completion; `Path`/`FilePath` completion continues to work normally for
|
|
219
|
+
explicit values.
|
|
220
|
+
|
|
221
|
+
Temporary `Path` files from resolver-bound `Field` instances are owned by
|
|
222
|
+
invoke-toolkit. `cleanup="pipeline"` is the default and keeps the file through
|
|
223
|
+
expanded pre/main/post execution; `cleanup="task"` removes it when that task
|
|
224
|
+
returns. Cleanup runs after failures and cancellation.
|
|
225
|
+
|
|
226
|
+
Installed `invoke_toolkit.field_resolver` entry points remain supported for
|
|
227
|
+
compatibility but issue a warning recommending a task-local resolver. Generate a
|
|
228
|
+
resolver-only provider package with:
|
|
229
|
+
|
|
230
|
+
```console
|
|
231
|
+
intk -x create.package --provider op
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Provider packages expose no task collection. Providers return text only; for
|
|
235
|
+
`Path` fields invoke-toolkit materializes and cleans the resulting temporary
|
|
236
|
+
file according to that Field's cleanup lifetime.
|
|
237
|
+
|
|
170
238
|
## Development
|
|
171
239
|
|
|
172
240
|
This project utilizes the `pre-commit` framework, make sure you run:
|
|
@@ -106,6 +106,74 @@ To enable caching, install with the cache extra:
|
|
|
106
106
|
pip install invoke-toolkit[cache]
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## Dynamic task defaults
|
|
110
|
+
|
|
111
|
+
Use `Field` when an argument default must be computed from the final task
|
|
112
|
+
context or resolved from a URI. Explicit command-line values always take
|
|
113
|
+
precedence.
|
|
114
|
+
|
|
115
|
+
Bind a resolver once, then reuse the resulting callable for scalar and file
|
|
116
|
+
defaults. A local resolver always returns one string value per request. The
|
|
117
|
+
resolver receives every URI using that callback and scheme in one batch.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from invoke_toolkit import Context, Field, FilePath, task
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def resolve_bw(ctx: Context, requests: list) -> dict[str, str]:
|
|
124
|
+
return {
|
|
125
|
+
request.parameter: ctx.run("bw get password ...", hide=True).stdout.strip()
|
|
126
|
+
for request in requests
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
BitwardenField = Field(resolver=resolve_bw)
|
|
131
|
+
ExistingFile = FilePath(exists=True, dir_okay=False)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@task
|
|
135
|
+
def deploy(
|
|
136
|
+
ctx: Context,
|
|
137
|
+
password: str = BitwardenField(default="bw://PASSWORD_ID"),
|
|
138
|
+
config: ExistingFile = BitwardenField(
|
|
139
|
+
default="bw://CONFIG_ID", cleanup="task"
|
|
140
|
+
),
|
|
141
|
+
) -> None:
|
|
142
|
+
...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For `str` fields, the resolver string reaches the task unchanged. For `Path` or
|
|
146
|
+
`FilePath` fields, `Field.create_temporary_file()` writes that string to a
|
|
147
|
+
managed temporary file and passes its `Path` to the task. Override that method
|
|
148
|
+
in a `Field` subclass to control file creation; resolvers do not return paths.
|
|
149
|
+
|
|
150
|
+
For omitted arguments, lookup precedence is explicit CLI/Python/call value,
|
|
151
|
+
then `INVOKE_<PARAMETER>` environment value, then the resolved `ctx.config`
|
|
152
|
+
value, then the declared `Field` default or factory. URI values from config use
|
|
153
|
+
the same local or entry-point resolver dispatch as declared URI defaults.
|
|
154
|
+
|
|
155
|
+
`Field(default_factory=callback)` runs only if no higher-precedence value is
|
|
156
|
+
available. Factories and resolvers never run during help, listing, or shell
|
|
157
|
+
completion; `Path`/`FilePath` completion continues to work normally for
|
|
158
|
+
explicit values.
|
|
159
|
+
|
|
160
|
+
Temporary `Path` files from resolver-bound `Field` instances are owned by
|
|
161
|
+
invoke-toolkit. `cleanup="pipeline"` is the default and keeps the file through
|
|
162
|
+
expanded pre/main/post execution; `cleanup="task"` removes it when that task
|
|
163
|
+
returns. Cleanup runs after failures and cancellation.
|
|
164
|
+
|
|
165
|
+
Installed `invoke_toolkit.field_resolver` entry points remain supported for
|
|
166
|
+
compatibility but issue a warning recommending a task-local resolver. Generate a
|
|
167
|
+
resolver-only provider package with:
|
|
168
|
+
|
|
169
|
+
```console
|
|
170
|
+
intk -x create.package --provider op
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Provider packages expose no task collection. Providers return text only; for
|
|
174
|
+
`Path` fields invoke-toolkit materializes and cleans the resulting temporary
|
|
175
|
+
file according to that Field's cleanup lifetime.
|
|
176
|
+
|
|
109
177
|
## Development
|
|
110
178
|
|
|
111
179
|
This project utilizes the `pre-commit` framework, make sure you run:
|
|
@@ -126,6 +126,14 @@ exclude_lines = [
|
|
|
126
126
|
]
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
[tool.ruff]
|
|
130
|
+
target-version = "py310"
|
|
131
|
+
extend-exclude = ["tests/original_invoke"]
|
|
132
|
+
|
|
133
|
+
[tool.ruff.lint]
|
|
134
|
+
extend-select = ["UP"]
|
|
135
|
+
|
|
136
|
+
|
|
129
137
|
[tool.ruff.lint.per-file-ignores]
|
|
130
138
|
"*tasks*" = ["ARG001"]
|
|
131
139
|
"__init__.py" = ["E402"]
|
|
@@ -158,7 +166,6 @@ ci = [
|
|
|
158
166
|
"nox>=2025.10.16",
|
|
159
167
|
]
|
|
160
168
|
dev = [
|
|
161
|
-
|
|
162
169
|
"copier>=9.16.0",
|
|
163
170
|
"diskcache>=5.6.0",
|
|
164
171
|
"hunter>=3.7.0",
|
|
@@ -167,6 +174,7 @@ dev = [
|
|
|
167
174
|
"pre-commit-uv>=4.2.0",
|
|
168
175
|
"pylint>=3.2.7",
|
|
169
176
|
"pylint-per-file-ignores>=1.0.0",
|
|
177
|
+
"pyright>=1.1.404",
|
|
170
178
|
"pytest>=9.1.1",
|
|
171
179
|
"pytest-cov>=5.0.0",
|
|
172
180
|
"pytest-html>=4.1.1",
|
|
@@ -212,7 +220,7 @@ disable = [
|
|
|
212
220
|
|
|
213
221
|
per-file-ignores = [
|
|
214
222
|
"tasks.py:missing-function-docstring,missing-class-docstring,missing-module-docstring,invalid-name,too-many-branches",
|
|
215
|
-
"src/invoke_toolkit/tasks/tasks.py:too-many-arguments,reimported,import-outside-toplevel,too-many-locals",
|
|
223
|
+
"src/invoke_toolkit/tasks/tasks.py:too-many-arguments,reimported,import-outside-toplevel,too-many-locals,too-many-lines,unidiomatic-typecheck,protected-access",
|
|
216
224
|
"src/invoke_toolkit/tasks/types.py:invalid-name,import-outside-toplevel,unidiomatic-typecheck",
|
|
217
225
|
"src/invoke_toolkit/completion.py:too-many-locals,too-many-branches,too-many-statements",
|
|
218
226
|
"src/invoke_toolkit/program/program.py:ungrouped-imports,redefined-builtin,raise-missing-from,attribute-defined-outside-init,redefined-builtin,possibly-used-before-assignment,no-else-raise",
|
|
@@ -232,6 +240,11 @@ per-file-ignores = [
|
|
|
232
240
|
"src/invoke_toolkit/config/config.py:import-outside-toplevel",
|
|
233
241
|
"src/invoke_toolkit/config/registry.py:import-outside-toplevel,unidiomatic-typecheck",
|
|
234
242
|
"src/invoke_toolkit/extensions/tasks/config.py:too-many-locals,import-outside-toplevel",
|
|
243
|
+
"src/invoke_toolkit/extensions/tasks/create.py:too-many-locals,too-many-branches",
|
|
244
|
+
"src/invoke_toolkit/executor.py:too-many-locals,protected-access,duplicate-code",
|
|
245
|
+
"src/invoke_toolkit/field_resolvers.py:too-many-locals,too-many-branches",
|
|
246
|
+
"src/invoke_toolkit/tasks/fields.py:invalid-name,too-few-public-methods",
|
|
247
|
+
"tests/typecheck/fields.py:invalid-name",
|
|
235
248
|
]
|
|
236
249
|
|
|
237
250
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Extended collection with package inspection"""
|
|
2
2
|
|
|
3
|
+
# pylint: disable=ungrouped-imports
|
|
3
4
|
import hashlib
|
|
4
5
|
import importlib.util
|
|
5
6
|
import pkgutil
|
|
6
7
|
import sys
|
|
8
|
+
from collections.abc import Callable
|
|
7
9
|
from logging import getLogger
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from types import ModuleType
|
|
10
|
-
from typing import TYPE_CHECKING, Any,
|
|
12
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
11
13
|
|
|
12
14
|
try:
|
|
13
15
|
from typing import override # type: ignore[attr-defined]
|
|
@@ -288,17 +290,19 @@ class ToolkitCollection(Collection):
|
|
|
288
290
|
)
|
|
289
291
|
|
|
290
292
|
@override
|
|
293
|
+
# pylint: disable=arguments-renamed
|
|
291
294
|
def configure(
|
|
292
295
|
self,
|
|
293
296
|
mapping: dict[str, Any] | None = None,
|
|
294
297
|
*,
|
|
295
298
|
schema: "type[ConfigSchema] | ConfigSchema | None" = None,
|
|
299
|
+
**kwargs: Any,
|
|
296
300
|
) -> None:
|
|
297
301
|
"""Configure collection with optional attrs schema.
|
|
298
|
-
|
|
299
302
|
Args:
|
|
300
303
|
mapping: Traditional dict config (existing behavior)
|
|
301
304
|
schema: Attrs class (uses defaults) or instance (uses provided values)
|
|
305
|
+
**kwargs: Additional compatibility keyword arguments.
|
|
302
306
|
|
|
303
307
|
Examples:
|
|
304
308
|
# Traditional dict
|
|
@@ -337,9 +341,14 @@ class ToolkitCollection(Collection):
|
|
|
337
341
|
if self.name and self._config_schema:
|
|
338
342
|
register_schema(self.name, self._config_schema)
|
|
339
343
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
options = kwargs.pop("options", mapping)
|
|
345
|
+
if kwargs:
|
|
346
|
+
unexpected = next(iter(kwargs))
|
|
347
|
+
raise TypeError(f"unexpected keyword argument: {unexpected!r}")
|
|
348
|
+
|
|
349
|
+
if options:
|
|
350
|
+
# Merge options over schema defaults (options wins)
|
|
351
|
+
self._deep_merge(final_mapping, options)
|
|
343
352
|
|
|
344
353
|
if final_mapping:
|
|
345
354
|
super().configure(final_mapping)
|
|
@@ -395,7 +404,7 @@ class ToolkitCollection(Collection):
|
|
|
395
404
|
register_schema(name, schema)
|
|
396
405
|
|
|
397
406
|
config = getattr(module, "config", None)
|
|
398
|
-
collection:
|
|
407
|
+
collection: ToolkitCollection = ns.from_module(module)
|
|
399
408
|
clean_collection(collection=collection)
|
|
400
409
|
# TODO: Namespaced configuration seems to be an not present when merged!§
|
|
401
410
|
# if config and isinstance(config, (dict, )):
|
|
@@ -11,7 +11,6 @@ import re
|
|
|
11
11
|
import shlex
|
|
12
12
|
from concurrent.futures import ThreadPoolExecutor
|
|
13
13
|
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
14
|
-
from typing import List
|
|
15
14
|
|
|
16
15
|
from invoke.completion.complete import (
|
|
17
16
|
debug,
|
|
@@ -32,7 +31,7 @@ from invoke_toolkit.tasks.types import _FileCompletionMarker
|
|
|
32
31
|
def _get_file_completions(
|
|
33
32
|
marker: "_FileCompletionMarker",
|
|
34
33
|
incomplete: str,
|
|
35
|
-
) ->
|
|
34
|
+
) -> list[str]:
|
|
36
35
|
"""
|
|
37
36
|
Get file completions based on marker configuration.
|
|
38
37
|
|
|
@@ -96,7 +95,7 @@ def _get_file_completions(
|
|
|
96
95
|
# Determine prefix to prepend to results
|
|
97
96
|
dir_prefix = "" if base_dir == "." else base_dir + "/"
|
|
98
97
|
|
|
99
|
-
files:
|
|
98
|
+
files: list[str] = []
|
|
100
99
|
|
|
101
100
|
try:
|
|
102
101
|
if marker.pattern:
|
|
@@ -148,7 +147,7 @@ def get_choices_for_argument(
|
|
|
148
147
|
arg_name: str,
|
|
149
148
|
incomplete: str = "",
|
|
150
149
|
config: "ToolkitConfig | None" = None,
|
|
151
|
-
) ->
|
|
150
|
+
) -> list[str]:
|
|
152
151
|
"""
|
|
153
152
|
Get available choices for an argument in a task.
|
|
154
153
|
|
|
@@ -244,7 +243,7 @@ def get_choices_for_argument(
|
|
|
244
243
|
return []
|
|
245
244
|
|
|
246
245
|
|
|
247
|
-
def _strip_program_name(names:
|
|
246
|
+
def _strip_program_name(names: list[str], remainder: str) -> str:
|
|
248
247
|
"""
|
|
249
248
|
Strip the program name from the invocation string.
|
|
250
249
|
|
|
@@ -274,7 +273,7 @@ def _strip_program_name(names: List[str], remainder: str) -> str:
|
|
|
274
273
|
return invocation
|
|
275
274
|
|
|
276
275
|
|
|
277
|
-
def _get_directory_completions(incomplete: str) ->
|
|
276
|
+
def _get_directory_completions(incomplete: str) -> list[str]:
|
|
278
277
|
"""
|
|
279
278
|
Get directory completions for path arguments like --search-root.
|
|
280
279
|
|
|
@@ -310,7 +309,7 @@ def _get_directory_completions(incomplete: str) -> List[str]:
|
|
|
310
309
|
else:
|
|
311
310
|
dir_prefix = base_dir + "/"
|
|
312
311
|
|
|
313
|
-
directories:
|
|
312
|
+
directories: list[str] = []
|
|
314
313
|
|
|
315
314
|
try:
|
|
316
315
|
if not os.path.isdir(base_dir):
|
|
@@ -399,7 +398,7 @@ def _handle_flag_completion(
|
|
|
399
398
|
|
|
400
399
|
|
|
401
400
|
def _try_complete_flag_value(
|
|
402
|
-
tokens:
|
|
401
|
+
tokens: list[str],
|
|
403
402
|
completing_new_token: bool,
|
|
404
403
|
initial_context: ParserContext,
|
|
405
404
|
collection,
|
|
@@ -441,7 +440,7 @@ def _try_complete_flag_value(
|
|
|
441
440
|
)
|
|
442
441
|
|
|
443
442
|
|
|
444
|
-
def _get_positional_arg_index(context: ParserContext, tokens:
|
|
443
|
+
def _get_positional_arg_index(context: ParserContext, tokens: list[str]) -> int:
|
|
445
444
|
"""
|
|
446
445
|
Determine which positional argument we're completing based on tokens.
|
|
447
446
|
|
|
@@ -546,7 +545,7 @@ def _handle_positional_completion(
|
|
|
546
545
|
|
|
547
546
|
|
|
548
547
|
def complete_with_choices(
|
|
549
|
-
names:
|
|
548
|
+
names: list[str],
|
|
550
549
|
core,
|
|
551
550
|
initial_context: ParserContext,
|
|
552
551
|
collection,
|
|
@@ -572,7 +571,7 @@ def complete_with_choices(
|
|
|
572
571
|
"""
|
|
573
572
|
# Strip out program name
|
|
574
573
|
invocation = _strip_program_name(names, core.remainder)
|
|
575
|
-
debug("Completing for invocation: {!r}"
|
|
574
|
+
debug(f"Completing for invocation: {invocation!r}")
|
|
576
575
|
|
|
577
576
|
# Check if invocation ends with space (user wants to complete next token)
|
|
578
577
|
completing_new_token = invocation.rstrip() != invocation
|
|
@@ -583,32 +582,28 @@ def complete_with_choices(
|
|
|
583
582
|
# Handle flags (partial or otherwise)
|
|
584
583
|
if tokens and tokens[-1].startswith("-"):
|
|
585
584
|
tail = tokens[-1]
|
|
586
|
-
debug("Invocation's tail {!r} is flag-like"
|
|
585
|
+
debug(f"Invocation's tail {tail!r} is flag-like")
|
|
587
586
|
|
|
588
587
|
# Parse invocation to obtain current context
|
|
589
|
-
contexts:
|
|
588
|
+
contexts: list[ParserContext]
|
|
590
589
|
try:
|
|
591
|
-
debug("Seeking context name in tokens: {!r}"
|
|
590
|
+
debug(f"Seeking context name in tokens: {tokens!r}")
|
|
592
591
|
contexts = parser.parse_argv(tokens)
|
|
593
592
|
except ParseError as e:
|
|
594
|
-
debug(
|
|
595
|
-
"Got parser error ({!r}), grabbing last-seen context {!r}".format(
|
|
596
|
-
e, e.context
|
|
597
|
-
)
|
|
598
|
-
)
|
|
593
|
+
debug(f"Got parser error ({e!r}), grabbing last-seen context {e.context!r}")
|
|
599
594
|
contexts = [e.context] if e.context is not None else []
|
|
600
595
|
|
|
601
596
|
# Fall back to core context if no context seen
|
|
602
|
-
debug("Parsed invocation, contexts: {!r}"
|
|
597
|
+
debug(f"Parsed invocation, contexts: {contexts!r}")
|
|
603
598
|
if not contexts or not contexts[-1]:
|
|
604
599
|
context = initial_context
|
|
605
600
|
else:
|
|
606
601
|
context = contexts[-1]
|
|
607
602
|
|
|
608
|
-
debug("Selected context: {!r}"
|
|
603
|
+
debug(f"Selected context: {context!r}")
|
|
609
604
|
|
|
610
605
|
# Check if this flag is known
|
|
611
|
-
debug("Looking for {!r} in {!r}"
|
|
606
|
+
debug(f"Looking for {tail!r} in {context.flags!r}")
|
|
612
607
|
if tail not in context.flags:
|
|
613
608
|
debug("Not found, completing with flag names")
|
|
614
609
|
# Long flags - partial or just the dashes
|
|
@@ -633,16 +628,12 @@ def complete_with_choices(
|
|
|
633
628
|
raise Exit
|
|
634
629
|
|
|
635
630
|
# Try to parse to get the current context
|
|
636
|
-
contexts:
|
|
631
|
+
contexts: list[ParserContext] = []
|
|
637
632
|
try:
|
|
638
|
-
debug("Parsing tokens for positional completion: {!r}"
|
|
633
|
+
debug(f"Parsing tokens for positional completion: {tokens!r}")
|
|
639
634
|
contexts = parser.parse_argv(tokens)
|
|
640
635
|
except ParseError as e:
|
|
641
|
-
debug(
|
|
642
|
-
"Got parser error ({!r}), grabbing last-seen context {!r}".format(
|
|
643
|
-
e, e.context
|
|
644
|
-
)
|
|
645
|
-
)
|
|
636
|
+
debug(f"Got parser error ({e!r}), grabbing last-seen context {e.context!r}")
|
|
646
637
|
contexts = [e.context] if e.context is not None else []
|
|
647
638
|
|
|
648
639
|
# Check if we have a task context (not just core context)
|
|
@@ -4,7 +4,7 @@ This module defines some functions/callables
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import inspect as frame_inspect
|
|
7
|
-
from typing import TYPE_CHECKING, Any,
|
|
7
|
+
from typing import TYPE_CHECKING, Any, NoReturn, TypeVar, overload
|
|
8
8
|
|
|
9
9
|
from invoke.config import Config
|
|
10
10
|
from invoke.util import debug
|
|
@@ -52,10 +52,10 @@ class ToolkitConfig(Config):
|
|
|
52
52
|
|
|
53
53
|
def __init_subclass__(
|
|
54
54
|
cls,
|
|
55
|
-
prefix:
|
|
56
|
-
file_prefix:
|
|
57
|
-
env_prefix:
|
|
58
|
-
extra_defaults:
|
|
55
|
+
prefix: str | None = None,
|
|
56
|
+
file_prefix: str | None = None,
|
|
57
|
+
env_prefix: str | None = None,
|
|
58
|
+
extra_defaults: dict[str, Any] | None = None,
|
|
59
59
|
**kwargs,
|
|
60
60
|
):
|
|
61
61
|
super().__init_subclass__(**kwargs)
|
|
@@ -70,13 +70,13 @@ class ToolkitConfig(Config):
|
|
|
70
70
|
else:
|
|
71
71
|
cls.extra_defaults = None
|
|
72
72
|
|
|
73
|
-
extra_defaults:
|
|
73
|
+
extra_defaults: dict[str, Any] | None
|
|
74
74
|
|
|
75
75
|
# This method is a static method in the super class
|
|
76
76
|
# but it's converted to class method here, so we can
|
|
77
77
|
# a reference to the attribute set by __init_subclass__
|
|
78
78
|
@classmethod
|
|
79
|
-
def global_defaults(cls) ->
|
|
79
|
+
def global_defaults(cls) -> dict[str, Any]:
|
|
80
80
|
"""
|
|
81
81
|
Return the core default settings for Invoke.
|
|
82
82
|
|
|
@@ -89,7 +89,7 @@ class ToolkitConfig(Config):
|
|
|
89
89
|
|
|
90
90
|
.. versionadded:: 1.0
|
|
91
91
|
"""
|
|
92
|
-
ret:
|
|
92
|
+
ret: dict[str, Any] = Config.global_defaults()
|
|
93
93
|
extra_defaults = getattr(cls, "extra_defaults", None)
|
|
94
94
|
if extra_defaults:
|
|
95
95
|
debug(f"Using {cls} extra defaults: {extra_defaults}")
|
|
@@ -239,7 +239,7 @@ def _navigate_config_path(config: "ToolkitConfig", path: str) -> tuple[Any, bool
|
|
|
239
239
|
return current, True
|
|
240
240
|
|
|
241
241
|
|
|
242
|
-
def _get_task_argument_name(path: str, depth: int = 3) ->
|
|
242
|
+
def _get_task_argument_name(path: str, depth: int = 3) -> str | None: # pylint: disable=too-many-return-statements
|
|
243
243
|
"""
|
|
244
244
|
Try to infer the task argument name from the call stack.
|
|
245
245
|
|
|
@@ -324,7 +324,7 @@ def get_config_value(
|
|
|
324
324
|
path: str,
|
|
325
325
|
default: Any = _UNDEFINED_DEFAULT,
|
|
326
326
|
exit_message: str = ...,
|
|
327
|
-
exit_code:
|
|
327
|
+
exit_code: int | None = None,
|
|
328
328
|
required: bool = False,
|
|
329
329
|
) -> Any | NoReturn: ...
|
|
330
330
|
|
|
@@ -347,8 +347,8 @@ def get_config_value(
|
|
|
347
347
|
ctx: "ToolkitContext",
|
|
348
348
|
path: str,
|
|
349
349
|
default: Any = _UNDEFINED_DEFAULT,
|
|
350
|
-
exit_message:
|
|
351
|
-
exit_code:
|
|
350
|
+
exit_message: str | None = None,
|
|
351
|
+
exit_code: int | None = None,
|
|
352
352
|
required: bool = True,
|
|
353
353
|
) -> Any | NoReturn: ...
|
|
354
354
|
|
|
@@ -357,8 +357,8 @@ def get_config_value( # pylint: disable=inconsistent-return-statements
|
|
|
357
357
|
ctx: "ToolkitContext",
|
|
358
358
|
path: str,
|
|
359
359
|
default: Any = _UNDEFINED_DEFAULT,
|
|
360
|
-
exit_message:
|
|
361
|
-
exit_code:
|
|
360
|
+
exit_message: str | None = None,
|
|
361
|
+
exit_code: int | None = None,
|
|
362
362
|
required: bool = False,
|
|
363
363
|
) -> Any:
|
|
364
364
|
"""
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import types
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Union, get_origin
|
|
6
7
|
|
|
7
8
|
import attrs
|
|
8
9
|
|
|
@@ -187,12 +188,12 @@ def _unwrap_optional_type(type_hint: Any) -> type | None:
|
|
|
187
188
|
Returns:
|
|
188
189
|
The unwrapped type, or the original if not Optional/Union
|
|
189
190
|
"""
|
|
190
|
-
origin =
|
|
191
|
+
origin = get_origin(type_hint)
|
|
191
192
|
if origin is None:
|
|
192
193
|
return type_hint
|
|
193
194
|
|
|
194
195
|
# Handle Union types (including Optional which is Union[X, None])
|
|
195
|
-
if origin
|
|
196
|
+
if origin in (Union, types.UnionType):
|
|
196
197
|
args = getattr(type_hint, "__args__", ())
|
|
197
198
|
# Filter out NoneType and return the first non-None type
|
|
198
199
|
non_none_args = [a for a in args if a is not type(None)]
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, TypeVar, overload
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
|
|
8
9
|
import attrs
|
|
9
10
|
import cattrs
|
|
@@ -49,7 +50,7 @@ class ConfigSchema:
|
|
|
49
50
|
__config_description__: str | None = None
|
|
50
51
|
|
|
51
52
|
@classmethod
|
|
52
|
-
def from_dict(cls:
|
|
53
|
+
def from_dict(cls: type[T], data: dict[str, Any]) -> T:
|
|
53
54
|
"""Create instance from dict using cattrs.
|
|
54
55
|
|
|
55
56
|
Args:
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
Class that implements the ctx.status through the config class
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
# pylint: disable=ungrouped-imports
|
|
5
6
|
from collections import deque
|
|
7
|
+
from collections.abc import Generator
|
|
6
8
|
from contextlib import contextmanager
|
|
7
|
-
from typing import Generator, Optional, Union
|
|
8
9
|
|
|
9
10
|
from rich.console import Console, Group, RenderableType
|
|
10
11
|
from rich.live import Live
|
|
@@ -24,8 +25,8 @@ class NoOpStatus:
|
|
|
24
25
|
|
|
25
26
|
def __init__(
|
|
26
27
|
self,
|
|
27
|
-
status:
|
|
28
|
-
console:
|
|
28
|
+
status: RenderableType | None = None,
|
|
29
|
+
console: Console | None = None,
|
|
29
30
|
):
|
|
30
31
|
self.status = status
|
|
31
32
|
self.console = console or get_console()
|
|
@@ -43,11 +44,11 @@ class NoOpStatus:
|
|
|
43
44
|
|
|
44
45
|
def update(
|
|
45
46
|
self,
|
|
46
|
-
status:
|
|
47
|
+
status: RenderableType | None = None,
|
|
47
48
|
*,
|
|
48
|
-
spinner:
|
|
49
|
-
spinner_style:
|
|
50
|
-
speed:
|
|
49
|
+
spinner: str | None = None,
|
|
50
|
+
spinner_style: StyleType | None = None,
|
|
51
|
+
speed: float | None = None,
|
|
51
52
|
) -> None:
|
|
52
53
|
"""No-op update"""
|
|
53
54
|
|
|
@@ -65,7 +66,7 @@ class VerboseStatus: # pylint: disable=too-many-instance-attributes
|
|
|
65
66
|
self,
|
|
66
67
|
status: RenderableType,
|
|
67
68
|
*,
|
|
68
|
-
console:
|
|
69
|
+
console: Console | None = None,
|
|
69
70
|
spinner: str = "dots",
|
|
70
71
|
spinner_style: StyleType = "status.spinner",
|
|
71
72
|
speed: float = 1.0,
|
|
@@ -105,11 +106,11 @@ class VerboseStatus: # pylint: disable=too-many-instance-attributes
|
|
|
105
106
|
|
|
106
107
|
def update(
|
|
107
108
|
self,
|
|
108
|
-
status:
|
|
109
|
+
status: RenderableType | None = None,
|
|
109
110
|
*,
|
|
110
|
-
spinner:
|
|
111
|
-
spinner_style:
|
|
112
|
-
speed:
|
|
111
|
+
spinner: str | None = None,
|
|
112
|
+
spinner_style: StyleType | None = None,
|
|
113
|
+
speed: float | None = None,
|
|
113
114
|
) -> None:
|
|
114
115
|
"""Update the status text or spinner parameters."""
|
|
115
116
|
if status is not None:
|
|
@@ -150,7 +151,7 @@ class StatusHelper:
|
|
|
150
151
|
it can be accessed from the task's context
|
|
151
152
|
"""
|
|
152
153
|
|
|
153
|
-
_current_status:
|
|
154
|
+
_current_status: Status | VerboseStatus | None
|
|
154
155
|
_disabled: bool
|
|
155
156
|
_show_command_output: bool
|
|
156
157
|
|
|
@@ -174,7 +175,7 @@ class StatusHelper:
|
|
|
174
175
|
spinner_style: StyleType = "status.spinner",
|
|
175
176
|
speed: float = 1.0,
|
|
176
177
|
refresh_per_second: float = 12.5,
|
|
177
|
-
) -> Generator[
|
|
178
|
+
) -> Generator[Status | VerboseStatus, None, None]:
|
|
178
179
|
"""Context manager for status management"""
|
|
179
180
|
if self._disabled:
|
|
180
181
|
with NoOpStatus(status=status, console=self.console) as noop:
|
|
@@ -222,11 +223,11 @@ class StatusHelper:
|
|
|
222
223
|
|
|
223
224
|
def status_update(
|
|
224
225
|
self,
|
|
225
|
-
status:
|
|
226
|
+
status: RenderableType | None = None,
|
|
226
227
|
*,
|
|
227
|
-
spinner:
|
|
228
|
-
spinner_style:
|
|
229
|
-
speed:
|
|
228
|
+
spinner: str | None = None,
|
|
229
|
+
spinner_style: StyleType | None = None,
|
|
230
|
+
speed: float | None = None,
|
|
230
231
|
) -> None:
|
|
231
232
|
"""Wrapper on Status.update"""
|
|
232
233
|
if self._disabled:
|