invoke-toolkit 0.0.58__tar.gz → 0.0.59__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 (125) hide show
  1. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/PKG-INFO +1 -1
  2. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/completion.py +47 -9
  3. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/program/program.py +1 -0
  4. invoke_toolkit-0.0.59/tests/examples/completion_with_config/.gitignore +2 -0
  5. invoke_toolkit-0.0.59/tests/examples/completion_with_config/tasks.py +144 -0
  6. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_completion_with_choices.py +202 -0
  7. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/.gitignore +0 -0
  8. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/LICENSE.txt +0 -0
  9. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/README.md +0 -0
  10. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/pyproject.toml +0 -0
  11. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/__init__.py +0 -0
  12. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/__main__.py +0 -0
  13. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/collections.py +0 -0
  14. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/config/__init__.py +0 -0
  15. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/config/config.py +0 -0
  16. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/config/registry.py +0 -0
  17. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/config/schema.py +0 -0
  18. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/config/status_helper.py +0 -0
  19. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/context/__init__.py +0 -0
  20. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/context/context.py +0 -0
  21. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/context/types.py +0 -0
  22. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/executor.py +0 -0
  23. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/extensions/__init__.py +0 -0
  24. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
  25. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/extensions/tasks/config.py +0 -0
  26. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/extensions/tasks/create.py +0 -0
  27. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
  28. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
  29. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/loader/entrypoint.py +0 -0
  30. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/log/__init__.py +0 -0
  31. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/log/logger.py +0 -0
  32. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/output/__init__.py +0 -0
  33. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/output/console.py +0 -0
  34. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/output/utils.py +0 -0
  35. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/parser.py +0 -0
  36. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/program/__init__.py +0 -0
  37. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/program/main.py +0 -0
  38. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/runners/__init__.py +0 -0
  39. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/runners/rich.py +0 -0
  40. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/scripts/__init__.py +0 -0
  41. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/scripts/loader.py +0 -0
  42. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/tasks/__init__.py +0 -0
  43. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/tasks/autocomplete.py +0 -0
  44. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/tasks/cache.py +0 -0
  45. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/tasks/tasks.py +0 -0
  46. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/tasks/types.py +0 -0
  47. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/testing.py +0 -0
  48. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/utils/__init__.py +0 -0
  49. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/utils/fzf.py +0 -0
  50. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/utils/inspection.py +0 -0
  51. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/utils/singleton.py +0 -0
  52. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/src/invoke_toolkit/utils/text.py +0 -0
  53. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/templates/package-template/.gitignore.jinja +0 -0
  54. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/templates/package-template/README.md.jinja +0 -0
  55. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/templates/package-template/copier.yml +0 -0
  56. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/templates/package-template/pyproject.toml.jinja +0 -0
  57. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
  58. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
  59. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/__init__.py +0 -0
  60. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/conftest.py +0 -0
  61. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/examples/cached_completion/tasks.py +0 -0
  62. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/examples/config_schema/tasks.py +0 -0
  63. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/examples/enum_select_size/tasks.py +0 -0
  64. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/examples/fzf_selector/tasks.py +0 -0
  65. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/examples/literal_set_level/tasks.py +0 -0
  66. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/extensions/conftest.py +0 -0
  67. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/extensions/test_config_tasks.py +0 -0
  68. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/extensions/test_create.py +0 -0
  69. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/extensions/test_package_template_entrypoint.py +0 -0
  70. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/extensions/test_shell_tasks.py +0 -0
  71. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/__init__.py +0 -0
  72. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
  73. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
  74. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
  75. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/collection.py +0 -0
  76. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/echo.yaml +0 -0
  77. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
  78. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
  79. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
  80. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/no-dedupe.yaml +0 -0
  81. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/no-echo.yaml +0 -0
  82. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/package/invoke.yml +0 -0
  83. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/package/tasks/__init__.py +0 -0
  84. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
  85. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/runtime.py +0 -0
  86. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
  87. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
  88. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
  89. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/underscores/tasks.py +0 -0
  90. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/yaml/explicit.py +0 -0
  91. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/yaml/tasks.py +0 -0
  92. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/yml/explicit.py +0 -0
  93. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
  94. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/configs/yml/tasks.py +0 -0
  95. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/_support/has_modules.py +0 -0
  96. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/conftest.py +0 -0
  97. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/original_invoke/test_config.py +0 -0
  98. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/program/main.py +0 -0
  99. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/program/tasks/__init__.py +0 -0
  100. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/program/tasks/coll1.py +0 -0
  101. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/script/test_script.py +0 -0
  102. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/tasks/test_cache.py +0 -0
  103. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/tasks/test_extensions_config.py +0 -0
  104. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_annotated_help.py +0 -0
  105. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_cofig_class.py +0 -0
  106. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_collection.py +0 -0
  107. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_collection_configure.py +0 -0
  108. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_config_helper.py +0 -0
  109. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_config_registry.py +0 -0
  110. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_config_schema.py +0 -0
  111. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_console.py +0 -0
  112. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_context_class.py +0 -0
  113. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_disable_status_cli.py +0 -0
  114. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_enum_arguments.py +0 -0
  115. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_executor.py +0 -0
  116. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_file_completion.py +0 -0
  117. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_global_context.py +0 -0
  118. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_help_flags.py +0 -0
  119. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_invoke_compatibility.py +0 -0
  120. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_loader.py +0 -0
  121. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_parsing.py +0 -0
  122. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_proctitle.py +0 -0
  123. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_program_with_collection.py +0 -0
  124. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/test_toplevel.py +0 -0
  125. {invoke_toolkit-0.0.58 → invoke_toolkit-0.0.59}/tests/utils/test_fzf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invoke-toolkit
3
- Version: 0.0.58
3
+ Version: 0.0.59
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
@@ -143,7 +143,11 @@ def _get_file_completions(
143
143
 
144
144
 
145
145
  def get_choices_for_argument(
146
- collection, context_name: str, arg_name: str, incomplete: str = ""
146
+ collection,
147
+ context_name: str,
148
+ arg_name: str,
149
+ incomplete: str = "",
150
+ config: "ToolkitConfig | None" = None,
147
151
  ) -> List[str]:
148
152
  """
149
153
  Get available choices for an argument in a task.
@@ -159,6 +163,10 @@ def get_choices_for_argument(
159
163
  context_name: The task name
160
164
  arg_name: The argument name (without dashes)
161
165
  incomplete: The incomplete value typed by user (for callback filtering)
166
+ config: Optional already-loaded ToolkitConfig (e.g. from the program).
167
+ When provided the callbacks receive a context backed by this
168
+ config so project-level invoke.yaml values are visible.
169
+ Falls back to a fresh ToolkitConfig() when omitted.
162
170
 
163
171
  Returns:
164
172
  List of choice strings, or empty list if no choices defined
@@ -175,8 +183,12 @@ def get_choices_for_argument(
175
183
  callbacks = task._completion_callbacks # pylint: disable=protected-access
176
184
  if arg_name in callbacks:
177
185
  try:
178
- # Try to call the callback with context and incomplete
179
- ctx = ToolkitContext(config=ToolkitConfig())
186
+ # Use the provided config (with project values loaded) or fall
187
+ # back to a plain ToolkitConfig so at least built-in defaults
188
+ # are available.
189
+ ctx = ToolkitContext(
190
+ config=config if config is not None else ToolkitConfig()
191
+ )
180
192
 
181
193
  # Get timeout from config (default: 10 seconds)
182
194
  timeout = ctx.get_config_value(
@@ -331,6 +343,7 @@ def _handle_flag_completion(
331
343
  flag_name: str,
332
344
  collection,
333
345
  incomplete: str = "",
346
+ config: "ToolkitConfig | None" = None,
334
347
  ) -> bool:
335
348
  """
336
349
  Handle completion for a flag that takes a value.
@@ -340,6 +353,7 @@ def _handle_flag_completion(
340
353
  flag_name: The flag name (e.g., '--color')
341
354
  collection: The invoke collection
342
355
  incomplete: The incomplete value typed by user
356
+ config: Optional already-loaded ToolkitConfig forwarded to callbacks.
343
357
 
344
358
  Returns:
345
359
  True if choices were printed, False otherwise
@@ -370,7 +384,9 @@ def _handle_flag_completion(
370
384
  task_name = context.name
371
385
 
372
386
  if task_name:
373
- choices = get_choices_for_argument(collection, task_name, arg_name, incomplete)
387
+ choices = get_choices_for_argument(
388
+ collection, task_name, arg_name, incomplete, config=config
389
+ )
374
390
  if choices:
375
391
  debug(f"Found choices for {arg_name}: {choices}")
376
392
  for choice in choices:
@@ -387,6 +403,7 @@ def _try_complete_flag_value(
387
403
  completing_new_token: bool,
388
404
  initial_context: ParserContext,
389
405
  collection,
406
+ config: "ToolkitConfig | None" = None,
390
407
  ) -> bool:
391
408
  """
392
409
  Try to complete a value for a flag that takes a value.
@@ -396,6 +413,7 @@ def _try_complete_flag_value(
396
413
  completing_new_token: Whether we're completing a new token (trailing space)
397
414
  initial_context: The core context with flag definitions
398
415
  collection: The invoke collection
416
+ config: Optional already-loaded ToolkitConfig forwarded to callbacks.
399
417
 
400
418
  Returns:
401
419
  True if completion was handled, False otherwise
@@ -418,7 +436,9 @@ def _try_complete_flag_value(
418
436
  # We're completing a value for this flag
419
437
  incomplete = tokens[-1] if not completing_new_token else ""
420
438
  debug(f"Completing value for flag {prev_token}, incomplete={incomplete!r}")
421
- return _handle_flag_completion(initial_context, prev_token, collection, incomplete)
439
+ return _handle_flag_completion(
440
+ initial_context, prev_token, collection, incomplete, config=config
441
+ )
422
442
 
423
443
 
424
444
  def _get_positional_arg_index(context: ParserContext, tokens: List[str]) -> int:
@@ -474,6 +494,7 @@ def _handle_positional_completion(
474
494
  collection,
475
495
  positional_index: int,
476
496
  incomplete: str = "",
497
+ config: "ToolkitConfig | None" = None,
477
498
  ) -> tuple[bool, bool]:
478
499
  """
479
500
  Handle completion for positional arguments.
@@ -483,6 +504,7 @@ def _handle_positional_completion(
483
504
  collection: The invoke collection
484
505
  positional_index: Which positional argument we're completing (0-based)
485
506
  incomplete: The incomplete value typed by user
507
+ config: Optional already-loaded ToolkitConfig forwarded to callbacks.
486
508
 
487
509
  Returns:
488
510
  Tuple of (handled, should_suppress_fallback):
@@ -508,7 +530,9 @@ def _handle_positional_completion(
508
530
  debug(f"Completing positional arg {positional_index}: {arg_name}")
509
531
 
510
532
  # Try to get choices for this argument
511
- choices = get_choices_for_argument(collection, task_name, arg_name, incomplete)
533
+ choices = get_choices_for_argument(
534
+ collection, task_name, arg_name, incomplete, config=config
535
+ )
512
536
  if choices:
513
537
  debug(f"Found choices for {arg_name}: {choices[:10]}...")
514
538
  for choice in choices:
@@ -527,12 +551,24 @@ def complete_with_choices(
527
551
  initial_context: ParserContext,
528
552
  collection,
529
553
  parser: Parser,
554
+ config: "ToolkitConfig | None" = None,
530
555
  ) -> Exit:
531
556
  """
532
557
  Enhanced completion function that supports Enum and Literal choices.
533
558
 
534
559
  This function extends invoke's basic completion to handle completing
535
560
  argument values for parameters with defined choices, including positional arguments.
561
+
562
+ Args:
563
+ names: Binary names for stripping the program name from the invocation.
564
+ core: The core parse result (provides remainder).
565
+ initial_context: The core ParserContext with global flags.
566
+ collection: The loaded task collection.
567
+ parser: A fresh Parser instance for re-parsing the invocation.
568
+ config: The already-loaded ToolkitConfig from the program. When
569
+ provided, completion callbacks receive a context that has
570
+ access to project-level invoke.yaml values (e.g. custom keys
571
+ and overridden ``completion.callback_timeout``).
536
572
  """
537
573
  # Strip out program name
538
574
  invocation = _strip_program_name(names, core.remainder)
@@ -585,12 +621,14 @@ def complete_with_choices(
585
621
  print(name)
586
622
  # Known flags complete w/ values or nothing
587
623
  else:
588
- _handle_flag_completion(context, tail, collection, incomplete="")
624
+ _handle_flag_completion(
625
+ context, tail, collection, incomplete="", config=config
626
+ )
589
627
  # If not a flag, check if we're completing a flag value, positional args, or task names
590
628
  else:
591
629
  # Check if the previous token is a flag that takes a value
592
630
  if _try_complete_flag_value(
593
- tokens, completing_new_token, initial_context, collection
631
+ tokens, completing_new_token, initial_context, collection, config=config
594
632
  ):
595
633
  raise Exit
596
634
 
@@ -634,7 +672,7 @@ def complete_with_choices(
634
672
 
635
673
  # Try to complete positional argument
636
674
  handled, suppress_fallback = _handle_positional_completion(
637
- context, collection, positional_index, incomplete
675
+ context, collection, positional_index, incomplete, config=config
638
676
  )
639
677
  if handled or suppress_fallback:
640
678
  # Either we printed completions, or we're in a positional arg
@@ -591,6 +591,7 @@ class ToolkitProgram(Program):
591
591
  # NOTE: can't reuse self.parser as it has likely been mutated
592
592
  # between when it was set and now.
593
593
  parser=self._make_parser(),
594
+ config=self.config if isinstance(self.config, ToolkitConfig) else None,
594
595
  )
595
596
 
596
597
  # Fallback behavior if no tasks were given & no default specified
@@ -0,0 +1,2 @@
1
+ # Track the example invoke.yaml even though the root .gitignore excludes it.
2
+ !invoke.yaml
@@ -0,0 +1,144 @@
1
+ """
2
+ Example showing tab completion callbacks that read from ToolkitConfig.
3
+
4
+ This demonstrates the fix from v0.0.57: completion callbacks now receive a
5
+ proper ToolkitContext backed by the program's already-loaded ToolkitConfig,
6
+ so they can read project-level invoke.yaml values just like any regular task.
7
+
8
+ Directory layout::
9
+
10
+ completion_with_config/
11
+ ├── tasks.py # this file
12
+ └── invoke.yaml # sets deploy.allowed_envs and completion.callback_timeout
13
+
14
+ Try it::
15
+
16
+ # List available tasks
17
+ intk --search-root tests/examples/completion_with_config --list
18
+
19
+ # Tab-complete --target: suggestions filtered by deploy.allowed_envs from invoke.yaml
20
+ intk --search-root tests/examples/completion_with_config deploy --target <TAB>
21
+
22
+ # Tab-complete --env: only envs listed in deploy.allowed_envs are shown
23
+ intk --search-root tests/examples/completion_with_config deploy --env <TAB>
24
+
25
+ # Show which config values are in effect
26
+ intk --search-root tests/examples/completion_with_config show-config
27
+
28
+ Edit invoke.yaml and change deploy.allowed_envs to see the completions change
29
+ without touching this file.
30
+ """
31
+
32
+ from textwrap import dedent
33
+ from typing import Annotated
34
+
35
+ from invoke_toolkit import Context, task
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Static data – all known targets, keyed by environment name
39
+ # ---------------------------------------------------------------------------
40
+
41
+ _ALL_TARGETS: dict[str, list[str]] = {
42
+ "dev": ["dev-us-east", "dev-eu-west"],
43
+ "staging": ["staging-us-east", "staging-us-west", "staging-eu-west"],
44
+ "production": ["prod-us-east", "prod-us-west", "prod-eu-west", "prod-ap-south"],
45
+ }
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Completion callbacks
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ def complete_environments(ctx: Context, incomplete: str) -> list[str]:
53
+ """Return the environment names permitted by deploy.allowed_envs.
54
+
55
+ Reads ``deploy.allowed_envs`` from the project config (invoke.yaml).
56
+ When the key is absent every environment is offered.
57
+ """
58
+ allowed = ctx.get_config_value("deploy.allowed_envs", default=None)
59
+
60
+ if allowed:
61
+ envs = [e for e in allowed if e in _ALL_TARGETS]
62
+ else:
63
+ envs = list(_ALL_TARGETS.keys())
64
+
65
+ if incomplete:
66
+ envs = [e for e in envs if e.startswith(incomplete)]
67
+
68
+ return sorted(envs)
69
+
70
+
71
+ def complete_targets(ctx: Context, incomplete: str) -> list[str]:
72
+ """Return deployment targets restricted to the allowed environments.
73
+
74
+ Reads ``deploy.allowed_envs`` from the project config (invoke.yaml) so
75
+ operators can narrow the suggestion list without touching task code.
76
+ """
77
+ allowed = ctx.get_config_value("deploy.allowed_envs", default=None)
78
+
79
+ if allowed:
80
+ candidates = [
81
+ t for env in allowed if env in _ALL_TARGETS for t in _ALL_TARGETS[env]
82
+ ]
83
+ else:
84
+ candidates = [t for targets in _ALL_TARGETS.values() for t in targets]
85
+
86
+ if incomplete:
87
+ candidates = [t for t in candidates if t.startswith(incomplete)]
88
+
89
+ return sorted(candidates)
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Tasks
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ @task
98
+ def deploy(
99
+ ctx: Context,
100
+ target: Annotated[str, complete_targets] = "",
101
+ env: Annotated[str, complete_environments] = "dev",
102
+ ) -> None:
103
+ """Deploy to a target host.
104
+
105
+ Tab-complete ``--target`` to see hosts filtered by the environments listed
106
+ in ``deploy.allowed_envs`` (invoke.yaml).
107
+ Tab-complete ``--env`` to see the allowed environment names.
108
+
109
+ Args:
110
+ target: Deployment host (tab-complete to see available targets).
111
+ env: Environment name (tab-complete to see choices).
112
+ """
113
+ if not target:
114
+ ctx.rich_exit(
115
+ dedent(
116
+ """\
117
+ [red]No target specified.[/red]
118
+ Run with tab-completion or pass [cyan]--target <host>[/cyan].
119
+ """
120
+ )
121
+ )
122
+
123
+ ctx.print(f"[bold]Deploying[/bold] to [cyan]{target}[/cyan] (env: {env})")
124
+ ctx.run(f"echo 'deploy → {target}'")
125
+
126
+
127
+ @task
128
+ def show_config(ctx: Context) -> None:
129
+ """Show the config values that drive tab-completion in this example.
130
+
131
+ Run this to verify that your invoke.yaml overrides are being picked up.
132
+ """
133
+ timeout = ctx.get_config_value("completion.callback_timeout", default=10.0)
134
+ allowed = ctx.get_config_value("deploy.allowed_envs", default=None)
135
+
136
+ ctx.print("[bold]completion_with_config – effective settings[/bold]\n")
137
+ ctx.print(
138
+ dedent(
139
+ f"""\
140
+ completion.callback_timeout = [cyan]{timeout}[/cyan]
141
+ deploy.allowed_envs = [cyan]{allowed or "<all>"}[/cyan]
142
+ """
143
+ )
144
+ )
@@ -1170,3 +1170,205 @@ def test_completion_callback_has_access_to_config():
1170
1170
  assert "timeout=10.0" in choices, (
1171
1171
  f"Expected default timeout value (10.0) from ToolkitConfig, got: {choices}"
1172
1172
  )
1173
+
1174
+
1175
+ def test_completion_callback_reads_config_from_project_file(tmp_path):
1176
+ """Test that completion callbacks read config values from a project config file.
1177
+
1178
+ When get_choices_for_argument receives an already-loaded config (as passed by
1179
+ the program after load_project()), the callback must see the project-level
1180
+ invoke.yaml values, not just the built-in defaults.
1181
+ """
1182
+ from invoke_toolkit.config import ToolkitConfig
1183
+
1184
+ # Write a project-level invoke.yaml that overrides the default timeout
1185
+ config_file = tmp_path / "invoke.yaml"
1186
+ config_file.write_text("completion:\n callback_timeout: 42.0\n")
1187
+
1188
+ def config_aware_callback(ctx: Context, incomplete: str) -> list[str]:
1189
+ """A callback that reports the configured timeout."""
1190
+ timeout = ctx.get_config_value("completion.callback_timeout", default=999.0)
1191
+ return [f"timeout={timeout}"]
1192
+
1193
+ coll = ToolkitCollection()
1194
+
1195
+ @task
1196
+ def file_config_task(
1197
+ ctx: Context,
1198
+ option: Annotated[str, config_aware_callback],
1199
+ ) -> None:
1200
+ """Task whose callback reads config from a project file."""
1201
+
1202
+ coll.add_task(file_config_task) # type: ignore[arg-type]
1203
+
1204
+ # Build the config exactly as the program does: set project location then merge
1205
+ config = ToolkitConfig(project_location=tmp_path)
1206
+ config.load_project()
1207
+ config.merge()
1208
+ assert config["completion"]["callback_timeout"] == 42.0, (
1209
+ "Sanity check: invoke.yaml should override the default timeout"
1210
+ )
1211
+
1212
+ # Pass the loaded config – the callback must now see 42.0, not 10.0 or 999.0
1213
+ choices = get_choices_for_argument(
1214
+ coll, "file-config-task", "option", "", config=config
1215
+ )
1216
+
1217
+ assert len(choices) == 1, f"Expected 1 choice, got {len(choices)}: {choices}"
1218
+ assert "timeout=42.0" in choices, (
1219
+ f"Expected project-file value (42.0) from loaded config, got: {choices}"
1220
+ )
1221
+
1222
+
1223
+ def test_completion_callback_without_config_uses_toolkit_defaults(tmp_path):
1224
+ """Test that omitting config falls back to ToolkitConfig built-in defaults.
1225
+
1226
+ Callers that don't have a loaded config (e.g. direct unit tests) still get
1227
+ sensible defaults rather than a bare invoke.Config.
1228
+ """
1229
+
1230
+ # Write a project-level invoke.yaml – it must NOT affect the result here
1231
+ config_file = tmp_path / "invoke.yaml"
1232
+ config_file.write_text("completion:\n callback_timeout: 42.0\n")
1233
+
1234
+ def config_aware_callback(ctx: Context, incomplete: str) -> list[str]:
1235
+ timeout = ctx.get_config_value("completion.callback_timeout", default=999.0)
1236
+ return [f"timeout={timeout}"]
1237
+
1238
+ coll = ToolkitCollection()
1239
+
1240
+ @task
1241
+ def no_config_task(
1242
+ ctx: Context,
1243
+ option: Annotated[str, config_aware_callback],
1244
+ ) -> None:
1245
+ """Task whose callback is called without an explicit config."""
1246
+
1247
+ coll.add_task(no_config_task) # type: ignore[arg-type]
1248
+
1249
+ # No config kwarg → fresh ToolkitConfig() → built-in default (10.0)
1250
+ choices = get_choices_for_argument(coll, "no-config-task", "option", "")
1251
+
1252
+ assert len(choices) == 1, f"Expected 1 choice, got {len(choices)}: {choices}"
1253
+ assert "timeout=10.0" in choices, (
1254
+ f"Expected ToolkitConfig built-in default (10.0), got: {choices}"
1255
+ )
1256
+
1257
+
1258
+ def test_completion_callback_project_config_via_program(tmp_path):
1259
+ """End-to-end test: program loads invoke.yaml and passes config to completion.
1260
+
1261
+ Simulates the full program flow so we can verify that the config threaded
1262
+ from parse_cleanup → complete_with_choices → get_choices_for_argument
1263
+ makes project-level invoke.yaml values visible inside a callback.
1264
+ """
1265
+ # Write a tasks.py with a completion callback that reads a custom config key
1266
+ tasks_file = tmp_path / "tasks.py"
1267
+ tasks_file.write_text(
1268
+ "from typing import Annotated\n"
1269
+ "from invoke_toolkit import Context, task\n"
1270
+ "\n"
1271
+ "def complete_envs(ctx: Context, incomplete: str) -> list[str]:\n"
1272
+ " allowed = ctx.get_config_value('deploy.allowed_envs', default=None)\n"
1273
+ " if allowed:\n"
1274
+ " return [e for e in allowed if e.startswith(incomplete)]\n"
1275
+ " return ['fallback']\n"
1276
+ "\n"
1277
+ "@task\n"
1278
+ "def deploy(ctx: Context, env: Annotated[str, complete_envs] = '') -> None:\n"
1279
+ " '''Deploy to an environment.'''\n"
1280
+ " ctx.print(env)\n"
1281
+ )
1282
+
1283
+ # Write an invoke.yaml that sets a custom deploy.allowed_envs list
1284
+ (tmp_path / "invoke.yaml").write_text(
1285
+ "deploy:\n allowed_envs:\n - staging\n - production\n"
1286
+ )
1287
+
1288
+ argv = ["intk", "--complete", "--", "intk", "deploy", "--env", ""]
1289
+ program = TestingToolkitProgram()
1290
+ stdout_capture = io.StringIO()
1291
+ with redirect_stdout(stdout_capture):
1292
+ try:
1293
+ program.run(argv, exit=False)
1294
+ except (SystemExit, Exit):
1295
+ pass
1296
+
1297
+ # The program is run from the project root, not tmp_path, so the invoke.yaml
1298
+ # won't be picked up automatically. Drive the search-root explicitly instead.
1299
+ argv = [
1300
+ "intk",
1301
+ "--complete",
1302
+ "--",
1303
+ "intk",
1304
+ "--search-root",
1305
+ str(tmp_path),
1306
+ "deploy",
1307
+ "--env",
1308
+ "",
1309
+ ]
1310
+ program = TestingToolkitProgram()
1311
+ stdout_capture = io.StringIO()
1312
+ with redirect_stdout(stdout_capture):
1313
+ try:
1314
+ program.run(argv, exit=False)
1315
+ except (SystemExit, Exit):
1316
+ pass
1317
+
1318
+ output = stdout_capture.getvalue()
1319
+ assert "staging" in output, (
1320
+ f"Expected 'staging' from invoke.yaml allowed_envs, got: {output!r}"
1321
+ )
1322
+ assert "production" in output, (
1323
+ f"Expected 'production' from invoke.yaml allowed_envs, got: {output!r}"
1324
+ )
1325
+ assert "fallback" not in output, (
1326
+ f"Expected project config to be used (not fallback), got: {output!r}"
1327
+ )
1328
+
1329
+
1330
+ def test_completion_callback_config_not_base_invoke_config(tmp_path):
1331
+ """Test that completion callbacks receive ToolkitConfig, not base invoke Config.
1332
+
1333
+ The key invariant restored by the fix: the ctx passed to a completion callback
1334
+ must have a ToolkitConfig (which knows about the 'completion' key) rather than
1335
+ a plain invoke.config.Config (which does not define that key and would raise or
1336
+ return the fallback).
1337
+ """
1338
+
1339
+ received_config_types = []
1340
+
1341
+ def type_checking_callback(ctx: Context, incomplete: str) -> list[str]:
1342
+ """A callback that records the type of ctx.config."""
1343
+ received_config_types.append(type(ctx.config).__name__)
1344
+ # Also confirm the 'completion' section is accessible without raising
1345
+ timeout = ctx.get_config_value("completion.callback_timeout", default=None)
1346
+ return [f"config_type={type(ctx.config).__name__}", f"timeout={timeout}"]
1347
+
1348
+ coll = ToolkitCollection()
1349
+
1350
+ @task
1351
+ def type_task(
1352
+ ctx: Context,
1353
+ option: Annotated[str, type_checking_callback],
1354
+ ) -> None:
1355
+ """Task whose callback inspects the config type."""
1356
+
1357
+ coll.add_task(type_task) # type: ignore[arg-type]
1358
+
1359
+ choices = get_choices_for_argument(coll, "type-task", "option", "")
1360
+
1361
+ assert len(choices) == 2, f"Expected 2 choices, got {len(choices)}: {choices}"
1362
+
1363
+ # ctx.config must be a ToolkitConfig (or subclass), never a bare BaseConfig
1364
+ config_type_choice = next(c for c in choices if c.startswith("config_type="))
1365
+ assert "ToolkitConfig" in config_type_choice, (
1366
+ f"Expected ToolkitConfig in callback, got: {config_type_choice}"
1367
+ )
1368
+
1369
+ # The 'completion.callback_timeout' key must resolve to the default (10.0),
1370
+ # not fall through to None (which would happen with bare BaseConfig)
1371
+ timeout_choice = next(c for c in choices if c.startswith("timeout="))
1372
+ assert timeout_choice == "timeout=10.0", (
1373
+ f"Expected timeout=10.0 from ToolkitConfig defaults, got: {timeout_choice}"
1374
+ )