invoke-toolkit 0.0.56__tar.gz → 0.0.58__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 (123) hide show
  1. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/PKG-INFO +1 -1
  2. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/completion.py +4 -4
  3. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/config/config.py +24 -26
  4. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/context/context.py +86 -0
  5. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/extensions/tasks/config.py +2 -4
  6. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/extensions/tasks/create.py +6 -5
  7. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/templates/package-template/pyproject.toml.jinja +3 -5
  8. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/extensions/test_create.py +139 -1
  9. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/collection.py +10 -0
  10. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/echo.yaml +2 -0
  11. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/no-dedupe.yaml +2 -0
  12. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/no-echo.yaml +2 -0
  13. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/package/invoke.yml +3 -0
  14. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/package/tasks/__init__.py +6 -0
  15. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/runtime.py +6 -0
  16. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/underscores/tasks.py +6 -0
  17. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/yaml/explicit.py +9 -0
  18. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/yaml/tasks.py +6 -0
  19. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/yml/explicit.py +9 -0
  20. invoke_toolkit-0.0.58/tests/original_invoke/_support/configs/yml/tasks.py +6 -0
  21. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_completion_with_choices.py +44 -0
  22. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_config_helper.py +76 -42
  23. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/utils/test_fzf.py +12 -7
  24. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/.gitignore +0 -0
  25. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/LICENSE.txt +0 -0
  26. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/README.md +0 -0
  27. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/pyproject.toml +0 -0
  28. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/__init__.py +0 -0
  29. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/__main__.py +0 -0
  30. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/collections.py +0 -0
  31. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/config/__init__.py +0 -0
  32. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/config/registry.py +0 -0
  33. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/config/schema.py +0 -0
  34. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/config/status_helper.py +0 -0
  35. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/context/__init__.py +0 -0
  36. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/context/types.py +0 -0
  37. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/executor.py +0 -0
  38. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/extensions/__init__.py +0 -0
  39. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
  40. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
  41. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
  42. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/loader/entrypoint.py +0 -0
  43. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/log/__init__.py +0 -0
  44. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/log/logger.py +0 -0
  45. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/output/__init__.py +0 -0
  46. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/output/console.py +0 -0
  47. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/output/utils.py +0 -0
  48. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/parser.py +0 -0
  49. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/program/__init__.py +0 -0
  50. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/program/main.py +0 -0
  51. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/program/program.py +0 -0
  52. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/runners/__init__.py +0 -0
  53. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/runners/rich.py +0 -0
  54. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/scripts/__init__.py +0 -0
  55. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/scripts/loader.py +0 -0
  56. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/tasks/__init__.py +0 -0
  57. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/tasks/autocomplete.py +0 -0
  58. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/tasks/cache.py +0 -0
  59. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/tasks/tasks.py +0 -0
  60. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/tasks/types.py +0 -0
  61. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/testing.py +0 -0
  62. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/utils/__init__.py +0 -0
  63. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/utils/fzf.py +0 -0
  64. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/utils/inspection.py +0 -0
  65. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/utils/singleton.py +0 -0
  66. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/src/invoke_toolkit/utils/text.py +0 -0
  67. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/templates/package-template/.gitignore.jinja +0 -0
  68. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/templates/package-template/README.md.jinja +0 -0
  69. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/templates/package-template/copier.yml +0 -0
  70. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
  71. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
  72. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/__init__.py +0 -0
  73. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/conftest.py +0 -0
  74. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/examples/cached_completion/tasks.py +0 -0
  75. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/examples/config_schema/tasks.py +0 -0
  76. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/examples/enum_select_size/tasks.py +0 -0
  77. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/examples/fzf_selector/tasks.py +0 -0
  78. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/examples/literal_set_level/tasks.py +0 -0
  79. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/extensions/conftest.py +0 -0
  80. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/extensions/test_config_tasks.py +0 -0
  81. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/extensions/test_package_template_entrypoint.py +0 -0
  82. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/extensions/test_shell_tasks.py +0 -0
  83. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/__init__.py +0 -0
  84. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
  85. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
  86. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
  87. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
  88. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
  89. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
  90. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
  91. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
  92. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
  93. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
  94. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
  95. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/_support/has_modules.py +0 -0
  96. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/conftest.py +0 -0
  97. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/original_invoke/test_config.py +0 -0
  98. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/program/main.py +0 -0
  99. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/program/tasks/__init__.py +0 -0
  100. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/program/tasks/coll1.py +0 -0
  101. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/script/test_script.py +0 -0
  102. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/tasks/test_cache.py +0 -0
  103. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/tasks/test_extensions_config.py +0 -0
  104. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_annotated_help.py +0 -0
  105. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_cofig_class.py +0 -0
  106. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_collection.py +0 -0
  107. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_collection_configure.py +0 -0
  108. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_config_registry.py +0 -0
  109. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_config_schema.py +0 -0
  110. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_console.py +0 -0
  111. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_context_class.py +0 -0
  112. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_disable_status_cli.py +0 -0
  113. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_enum_arguments.py +0 -0
  114. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_executor.py +0 -0
  115. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_file_completion.py +0 -0
  116. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_global_context.py +0 -0
  117. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_help_flags.py +0 -0
  118. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_invoke_compatibility.py +0 -0
  119. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_loader.py +0 -0
  120. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_parsing.py +0 -0
  121. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_proctitle.py +0 -0
  122. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_program_with_collection.py +0 -0
  123. {invoke_toolkit-0.0.56 → invoke_toolkit-0.0.58}/tests/test_toplevel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invoke-toolkit
3
- Version: 0.0.56
3
+ Version: 0.0.58
4
4
  Summary: A set of extended APIs for PyInvoke for composable scripts, plugins and richer output
5
5
  Project-URL: Documentation, https://github.com/D3f0/invoke-toolkit#readme
6
6
  Project-URL: Issues, https://github.com/D3f0/invoke-toolkit/issues
@@ -20,7 +20,7 @@ from invoke.completion.complete import (
20
20
  from invoke.exceptions import Exit, ParseError
21
21
  from invoke.parser import Parser, ParserContext
22
22
 
23
- from invoke_toolkit.config import get_config_value
23
+ from invoke_toolkit.config import ToolkitConfig
24
24
  from invoke_toolkit.context import ToolkitContext
25
25
  from invoke_toolkit.tasks.tasks import (
26
26
  _extract_enum_params,
@@ -176,11 +176,11 @@ def get_choices_for_argument(
176
176
  if arg_name in callbacks:
177
177
  try:
178
178
  # Try to call the callback with context and incomplete
179
- ctx = ToolkitContext()
179
+ ctx = ToolkitContext(config=ToolkitConfig())
180
180
 
181
181
  # Get timeout from config (default: 10 seconds)
182
- timeout = get_config_value(
183
- ctx, "completion.callback_timeout", default=10.0
182
+ timeout = ctx.get_config_value(
183
+ "completion.callback_timeout", default=10.0
184
184
  )
185
185
 
186
186
  # Execute callback with timeout
@@ -391,32 +391,30 @@ def get_config_value( # pylint: disable=inconsistent-return-statements
391
391
  SystemExit: Raised via ctx.rich_exit() if required value is missing.
392
392
 
393
393
  Examples:
394
- >>> from invoke_toolkit import task, Context
395
- >>> from invoke_toolkit.config import get_config_value
396
- >>> @task()
397
- >>> def my_task(ctx: Context, db_host: str = "") -> None:
398
- >>> # Simple usage
399
- >>> db_host = db_host or get_config_value(
400
- >>> ctx, "database.host", default="localhost"
401
- >>> )
402
- >>>
403
- >>> # With nested path and default
404
- >>> db_port = get_config_value(
405
- >>> ctx, "database.settings.port", default=5432
406
- >>> )
407
- >>>
408
- >>> # Required value with custom message
409
- >>> api_key = get_config_value(
410
- >>> ctx, "api.key",
411
- >>> exit_code=2,
412
- >>> exit_message="API key must be configured in 'api.key'"
413
- >>> )
414
- >>>
415
- >>> # Required value with auto-detected argument name
416
- >>> secret = get_config_value(
417
- >>> ctx, "secrets.token",
418
- >>> exit_code=1 # Auto-detects 'secret' parameter name
419
- >>> )
394
+ Preferred: Use the context method (no import needed)::
395
+
396
+ @task()
397
+ def my_task(ctx: Context) -> None:
398
+ # Simple usage with default
399
+ db_host = ctx.get_config_value("database.host", default="localhost")
400
+
401
+ # Required value - exits if missing
402
+ api_key = ctx.get_config_value("api.key", required=True)
403
+
404
+ # Required with custom exit code and message
405
+ secret = ctx.get_config_value(
406
+ "secrets.token",
407
+ exit_code=2,
408
+ exit_message="Token must be configured"
409
+ )
410
+
411
+ Alternative: Use the standalone function (backward compatible)::
412
+
413
+ from invoke_toolkit.config import get_config_value
414
+
415
+ @task()
416
+ def my_task(ctx: Context) -> None:
417
+ db_host = get_config_value(ctx, "database.host", default="localhost")
420
418
  """
421
419
  # Mark as required if exit parameters are provided or if required=True
422
420
  has_exit_params = (exit_message is not None) or (exit_code is not None) or required
@@ -7,6 +7,7 @@ from contextlib import _GeneratorContextManager, contextmanager
7
7
  from os import PathLike
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
+ Any,
10
11
  Callable,
11
12
  Generator,
12
13
  Iterator,
@@ -16,6 +17,7 @@ from typing import (
16
17
  Protocol,
17
18
  TypeVar,
18
19
  Union,
20
+ overload,
19
21
  )
20
22
 
21
23
  import setproctitle
@@ -329,3 +331,87 @@ class ToolkitContext(Context, ConfigProtocol):
329
331
  ["tmux", "rename-window", previous_tmux_title],
330
332
  check=False,
331
333
  )
334
+
335
+ # Type overloads for get_config_value - returns T when no exit params
336
+ @overload
337
+ def get_config_value(
338
+ self,
339
+ path: str,
340
+ default: T = ..., # type: ignore[assignment]
341
+ exit_message: None = None,
342
+ exit_code: None = None,
343
+ required: bool = False,
344
+ ) -> Any | T: ...
345
+
346
+ # Type hints NoReturn when exit_message provided
347
+ @overload
348
+ def get_config_value(
349
+ self,
350
+ path: str,
351
+ default: Any = ...,
352
+ exit_message: str = ...,
353
+ exit_code: Optional[int] = None,
354
+ required: bool = False,
355
+ ) -> Any | NoReturn: ...
356
+
357
+ # Type hints NoReturn when exit_code provided
358
+ @overload
359
+ def get_config_value(
360
+ self,
361
+ path: str,
362
+ default: Any = ...,
363
+ exit_message: None = None,
364
+ exit_code: int = ...,
365
+ required: bool = False,
366
+ ) -> Any | NoReturn: ...
367
+
368
+ # Type hints NoReturn when required=True
369
+ @overload
370
+ def get_config_value(
371
+ self,
372
+ path: str,
373
+ default: Any = ...,
374
+ exit_message: Optional[str] = None,
375
+ exit_code: Optional[int] = None,
376
+ required: bool = True,
377
+ ) -> Any | NoReturn: ...
378
+
379
+ def get_config_value(
380
+ self,
381
+ path: str,
382
+ default: Any = ...,
383
+ exit_message: Optional[str] = None,
384
+ exit_code: Optional[int] = None,
385
+ required: bool = False,
386
+ ) -> Any:
387
+ """Get a configuration value from config with dot notation support.
388
+
389
+ This is a convenience method that wraps invoke_toolkit.config.get_config_value.
390
+ See that function for full documentation.
391
+
392
+ Args:
393
+ path: Dot-separated path to the config value (e.g., 'database.host')
394
+ default: Default value if path not found
395
+ exit_message: Custom message when value required but missing
396
+ exit_code: Exit code for ctx.rich_exit() when missing
397
+ required: Whether value is required (exits if missing)
398
+
399
+ Returns:
400
+ The config value if found, otherwise default value.
401
+
402
+ Example:
403
+ @task()
404
+ def my_task(ctx: Context) -> None:
405
+ db_host = ctx.get_config_value("database.host", default="localhost")
406
+ api_key = ctx.get_config_value("api.key", required=True)
407
+ """
408
+ # Import here to avoid circular imports
409
+ from invoke_toolkit.config.config import ( # pylint: disable=import-outside-toplevel
410
+ _UNDEFINED_DEFAULT,
411
+ get_config_value as _get_config_value,
412
+ )
413
+
414
+ # Handle the default sentinel - ellipsis means "use undefined default"
415
+ if default is ...:
416
+ default = _UNDEFINED_DEFAULT
417
+ return _get_config_value(self, path, default, exit_message, exit_code, required)
@@ -404,10 +404,8 @@ def _complete_config_path(ctx: Context, incomplete: str) -> list[str]:
404
404
 
405
405
  def _complete_my_items(ctx: Context, incomplete: str) -> list[str]:
406
406
  '''Completion callback for my custom items.'''
407
- from invoke_toolkit.config import get_config_value
408
-
409
- # Get data from context or config
410
- items = get_config_value(ctx, "my.items", default=[])
407
+ # Get data from config using context method (no import needed)
408
+ items = ctx.get_config_value("my.items", default=[])
411
409
 
412
410
  # Filter by incomplete prefix
413
411
  matching = [i for i in items if i.startswith(incomplete)]
@@ -65,7 +65,7 @@ def _get_template() -> str:
65
65
  script_template = dedent(rf"""
66
66
  #!/usr/bin/env -S uv run --script
67
67
  # /// script
68
- # requires-python = ">=3.10"
68
+ # requires-python = ">=3.11"
69
69
  # dependencies = [
70
70
  # "invoke-toolkit{version_for_template}",
71
71
  # ]
@@ -322,9 +322,9 @@ def package(
322
322
  template_data = {
323
323
  "package_name": actual_name,
324
324
  "package_slug": package_slug,
325
- "collection_name": package_slug,
325
+ "collection_name": extension_short_name,
326
326
  "extension_short_name": extension_short_name,
327
- "python_version": "3.10",
327
+ "python_version": "3.11",
328
328
  }
329
329
 
330
330
  run_copy(
@@ -342,9 +342,10 @@ def package(
342
342
  dedent(
343
343
  f"""
344
344
  [yellow]Next steps:[/yellow]
345
- cd {actual_name}
345
+ cd {target_path}
346
346
  uv sync
347
- uv pip install -e .
347
+ # Test your package
348
+ uv run --directory {target_path} -m invoke-toolkit -l
348
349
  """
349
350
  ).strip()
350
351
  )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "{{ package_name }}"
7
- dynamic = ["version"]
7
+ version = "0.1.0"
8
8
  description = "{{ project_description }}"
9
9
  readme = "README.md"
10
10
  requires-python = ">={{ python_version }}"
@@ -17,9 +17,10 @@ classifiers = [
17
17
  "Intended Audience :: Developers",
18
18
  "License :: OSI Approved :: MIT License",
19
19
  "Programming Language :: Python :: 3",
20
- "Programming Language :: Python :: 3.10",
21
20
  "Programming Language :: Python :: 3.11",
22
21
  "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
23
24
  ]
24
25
  dependencies = [
25
26
  "invoke-toolkit", # Requires invoke-toolkit, any version
@@ -28,8 +29,5 @@ dependencies = [
28
29
  [project.entry-points."invoke_toolkit.collection"]
29
30
  "{{ extension_short_name }}" = "{{ package_slug }}:collection"
30
31
 
31
- [tool.hatch.version]
32
- path = "src/{{ package_slug }}/__init__.py"
33
-
34
32
  [tool.hatch.build.targets.wheel]
35
33
  packages = ["src/{{ package_slug }}"]
@@ -274,7 +274,7 @@ def _verify_python_requirement(lines):
274
274
  """Verify requires-python field exists and has correct value."""
275
275
  for line in lines:
276
276
  if "requires-python" in line:
277
- assert ">=3.10" in line, f"Expected requires-python >=3.10, got: '{line}'"
277
+ assert ">=3.11" in line, f"Expected requires-python >=3.11, got: '{line}'"
278
278
  return
279
279
 
280
280
  raise AssertionError("requires-python field not found in metadata")
@@ -658,6 +658,144 @@ def test_package_uses_git_config_template(
658
658
  assert (pkg_dir / "pyproject.toml").exists(), "pyproject.toml should exist"
659
659
 
660
660
 
661
+ def _assert_cli_examples_use_name(
662
+ short_name: str, pkg_dir: Path, package_slug: str
663
+ ) -> None:
664
+ """
665
+ Helper: asserts that the rendered README.md and __init__.py use ``short_name``
666
+ (the intk-visible collection name) in all CLI usage examples, and that no
667
+ raw Jinja placeholders leaked through.
668
+ """
669
+ # --- README.md checks ---
670
+ readme_path = pkg_dir / "README.md"
671
+ assert readme_path.exists(), "README.md was not generated"
672
+ readme = readme_path.read_text(encoding="utf-8")
673
+
674
+ assert f"collection named `{short_name}`" in readme, (
675
+ f"README should reference '{short_name}' in the collection listing sentence, "
676
+ f"but got:\n{readme}"
677
+ )
678
+ assert f"`{short_name}.hello`" in readme, (
679
+ f"README should reference '{short_name}' in the task list entry, "
680
+ f"but got:\n{readme}"
681
+ )
682
+ assert f"intk {short_name}.hello" in readme, (
683
+ f"README should reference '{short_name}' in the example command, "
684
+ f"but got:\n{readme}"
685
+ )
686
+ assert "{{ collection_name }}" not in readme, (
687
+ "Jinja variable {{ collection_name }} was not rendered in README.md"
688
+ )
689
+ assert "{{ package_name }}" not in readme, (
690
+ "Jinja variable {{ package_name }} was not rendered in README.md"
691
+ )
692
+
693
+ # --- __init__.py comment checks ---
694
+ init_path = pkg_dir / "src" / package_slug / "__init__.py"
695
+ assert init_path.exists(), "__init__.py was not generated"
696
+ init = init_path.read_text(encoding="utf-8")
697
+
698
+ assert f"intk {short_name}.hello" in init, (
699
+ f"__init__.py example comment should reference '{short_name}' for 'hello', "
700
+ f"but got:\n{init}"
701
+ )
702
+ assert f"intk {short_name}.my_task" in init, (
703
+ f"__init__.py example comment should reference '{short_name}' for 'my_task', "
704
+ f"but got:\n{init}"
705
+ )
706
+ assert f"intk {short_name}.tasks.hello" in init, (
707
+ f"__init__.py 'NOT' comment should reference '{short_name}' for nested 'tasks.hello', "
708
+ f"but got:\n{init}"
709
+ )
710
+ assert f"intk {short_name}.utils.my_task" in init, (
711
+ f"__init__.py 'NOT' comment should reference '{short_name}' for nested 'utils.my_task', "
712
+ f"but got:\n{init}"
713
+ )
714
+ assert "{{ collection_name }}" not in init, (
715
+ "Jinja variable {{ collection_name }} was not rendered in __init__.py"
716
+ )
717
+ assert "{{ package_name }}" not in init, (
718
+ "Jinja variable {{ package_name }} was not rendered in __init__.py"
719
+ )
720
+
721
+
722
+ def test_template_cli_examples_use_collection_name_unprefixed(
723
+ tmp_path: Path,
724
+ monkeypatch: pytest.MonkeyPatch,
725
+ ):
726
+ """
727
+ Regression test (non-prefixed package): verifies that the CLI usage examples
728
+ in README.md and __init__.py use the package slug as the collection name.
729
+
730
+ For a package named ``my-readme-pkg`` (no ``invoke-toolkit-`` prefix) the
731
+ intk-visible name is the full slug ``my_readme_pkg``.
732
+ """
733
+ import invoke_toolkit
734
+
735
+ invoke_toolkit_path = Path(invoke_toolkit.__file__).parent
736
+ default_template = (
737
+ invoke_toolkit_path.parent.parent / "templates" / "package-template"
738
+ )
739
+ if not default_template.exists():
740
+ pytest.skip("Default template not found in development setup")
741
+
742
+ monkeypatch.chdir(tmp_path)
743
+
744
+ package_name = "my-readme-pkg"
745
+ package_slug = package_name.replace("-", "_")
746
+ # Non-prefixed: collection_name == package_slug
747
+ expected_short_name = package_slug
748
+
749
+ x = TestingToolkitProgram()
750
+ x.run(["", "-x", "create.package", "--name", package_name])
751
+
752
+ _assert_cli_examples_use_name(
753
+ expected_short_name, tmp_path / package_name, package_slug
754
+ )
755
+
756
+
757
+ def test_template_cli_examples_use_collection_name_prefixed(
758
+ tmp_path: Path,
759
+ monkeypatch: pytest.MonkeyPatch,
760
+ ):
761
+ """
762
+ Regression test (invoke-toolkit-prefixed package): verifies that the CLI
763
+ usage examples use only the short name after stripping the ``invoke-toolkit-``
764
+ prefix, not the full package name.
765
+
766
+ For ``--name my-pkg --ext-name myext`` the full package name becomes
767
+ ``invoke-toolkit-myext``, but intk exposes it as ``myext``.
768
+ """
769
+ import invoke_toolkit
770
+
771
+ invoke_toolkit_path = Path(invoke_toolkit.__file__).parent
772
+ default_template = (
773
+ invoke_toolkit_path.parent.parent / "templates" / "package-template"
774
+ )
775
+ if not default_template.exists():
776
+ pytest.skip("Default template not found in development setup")
777
+
778
+ monkeypatch.chdir(tmp_path)
779
+
780
+ ext_name = "myext"
781
+ full_package_name = f"invoke-toolkit-{ext_name}"
782
+ package_slug = full_package_name.replace("-", "_")
783
+ # Prefixed: collection_name == ext_name (the part after "invoke-toolkit-")
784
+ expected_short_name = ext_name
785
+
786
+ x = TestingToolkitProgram()
787
+ x.run(["", "-x", "create.package", "--name", "ignored", "--ext-name", ext_name])
788
+
789
+ pkg_dir = tmp_path / full_package_name
790
+ _assert_cli_examples_use_name(expected_short_name, pkg_dir, package_slug)
791
+
792
+ # Extra guard: the full package name must NOT appear as the collection name
793
+ readme = (pkg_dir / "README.md").read_text(encoding="utf-8")
794
+ assert f"intk {full_package_name}.hello" not in readme, (
795
+ "README must not use the full prefixed package name as the collection name"
796
+ )
797
+
798
+
661
799
  def test_package_template_priority_explicit_over_git_config(
662
800
  tmp_path: Path,
663
801
  monkeypatch: pytest.MonkeyPatch,
@@ -0,0 +1,10 @@
1
+ from invoke import Collection, ctask # pylint: disable=no-name-in-module
2
+
3
+
4
+ @ctask
5
+ def go(c):
6
+ c.run("false") # Ensures a kaboom if mocking fails
7
+
8
+
9
+ ns = Collection(go)
10
+ ns.configure({"run": {"echo": True}})
@@ -0,0 +1,2 @@
1
+ tasks:
2
+ dedupe: false
@@ -0,0 +1,3 @@
1
+ outer:
2
+ inner:
3
+ hooray: "package"
@@ -0,0 +1,6 @@
1
+ from invoke import task
2
+
3
+
4
+ @task
5
+ def mytask(c):
6
+ assert c.outer.inner.hooray == "package"
@@ -0,0 +1,6 @@
1
+ from invoke import task
2
+
3
+
4
+ @task
5
+ def mytask(c):
6
+ assert c.outer.inner.hooray == "yaml"
@@ -0,0 +1,6 @@
1
+ from invoke import task
2
+
3
+
4
+ @task
5
+ def i_have_underscores(c):
6
+ pass
@@ -0,0 +1,9 @@
1
+ from invoke import task, Collection
2
+
3
+
4
+ @task
5
+ def mytask(c):
6
+ assert c.outer.inner.hooray == "yaml"
7
+
8
+
9
+ ns = Collection(mytask)
@@ -0,0 +1,6 @@
1
+ from invoke import task
2
+
3
+
4
+ @task
5
+ def mytask(c):
6
+ assert c.outer.inner.hooray == "yaml"
@@ -0,0 +1,9 @@
1
+ from invoke import task, Collection
2
+
3
+
4
+ @task
5
+ def mytask(c):
6
+ assert c.outer.inner.hooray == "yml"
7
+
8
+
9
+ ns = Collection(mytask)
@@ -0,0 +1,6 @@
1
+ from invoke import task
2
+
3
+
4
+ @task
5
+ def mytask(c):
6
+ assert c.outer.inner.hooray == "yml"
@@ -1126,3 +1126,47 @@ def test_completion_callback_cached_decorator_without_diskcache():
1126
1126
  # Check cache info shows no backend
1127
1127
  cache_info = no_cache_callback.cache_info()
1128
1128
  assert cache_info["backend"] == "none"
1129
+
1130
+
1131
+ def test_completion_callback_has_access_to_config():
1132
+ """Test that completion callbacks have access to ToolkitConfig.
1133
+
1134
+ This test demonstrates that the fix for instantiating ToolkitConfig in completion
1135
+ callbacks allows them to access the config object and read config values like
1136
+ completion.callback_timeout with the correct defaults.
1137
+
1138
+ Before the fix, ctx.get_config_value() would only return default values because
1139
+ ToolkitContext was instantiated without any config, causing it to use the
1140
+ base invoke.config.Config instead of ToolkitConfig.
1141
+ """
1142
+
1143
+ def config_aware_callback(ctx: Context, incomplete: str) -> list[str]:
1144
+ """A callback that reads config values."""
1145
+ # Uses ctx.get_config_value() method - no import needed
1146
+ # This works because ctx has ToolkitConfig (not base Config)
1147
+ timeout = ctx.get_config_value("completion.callback_timeout", default=999.0)
1148
+
1149
+ # With the fix, this should be 10.0 (the ToolkitConfig default)
1150
+ # Without the fix, it would fall back to the default value (999.0)
1151
+ return [f"timeout={timeout}"]
1152
+
1153
+ coll = ToolkitCollection()
1154
+
1155
+ @task
1156
+ def config_task(
1157
+ ctx: Context,
1158
+ option: Annotated[str, config_aware_callback],
1159
+ ) -> None:
1160
+ """Task that uses config in completion callback."""
1161
+
1162
+ coll.add_task(config_task) # type: ignore[arg-type]
1163
+
1164
+ # Get completion choices
1165
+ choices = get_choices_for_argument(coll, "config-task", "option", "")
1166
+
1167
+ # Verify the callback could access the ToolkitConfig default value
1168
+ assert len(choices) == 1, f"Expected 1 choice, got {len(choices)}: {choices}"
1169
+ # Should be the ToolkitConfig default (10.0), not the fallback default (999.0)
1170
+ assert "timeout=10.0" in choices, (
1171
+ f"Expected default timeout value (10.0) from ToolkitConfig, got: {choices}"
1172
+ )