invoke-toolkit 0.0.62__tar.gz → 0.0.64__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 (129) hide show
  1. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/.gitignore +1 -0
  2. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/PKG-INFO +2 -2
  3. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/pyproject.toml +1 -1
  4. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/context/context.py +4 -2
  5. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/executor.py +8 -3
  6. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/program/program.py +90 -18
  7. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/tasks/tasks.py +28 -5
  8. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/script/test_script.py +5 -4
  9. invoke_toolkit-0.0.64/tests/test_alias_self_reference.py +108 -0
  10. invoke_toolkit-0.0.64/tests/test_invoke_3_compat.py +184 -0
  11. invoke_toolkit-0.0.64/tests/test_list_filter_flags.py +208 -0
  12. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_proctitle.py +16 -8
  13. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/LICENSE.txt +0 -0
  14. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/README.md +0 -0
  15. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/__init__.py +0 -0
  16. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/__main__.py +0 -0
  17. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/collections.py +0 -0
  18. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/completion.py +0 -0
  19. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/config/__init__.py +0 -0
  20. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/config/config.py +0 -0
  21. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/config/registry.py +0 -0
  22. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/config/schema.py +0 -0
  23. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/config/status_helper.py +0 -0
  24. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/context/__init__.py +0 -0
  25. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/context/types.py +0 -0
  26. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/extensions/__init__.py +0 -0
  27. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
  28. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/extensions/tasks/config.py +0 -0
  29. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/extensions/tasks/create.py +0 -0
  30. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
  31. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
  32. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/loader/entrypoint.py +0 -0
  33. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/log/__init__.py +0 -0
  34. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/log/logger.py +0 -0
  35. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/output/__init__.py +0 -0
  36. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/output/console.py +0 -0
  37. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/output/utils.py +0 -0
  38. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/parser.py +0 -0
  39. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/program/__init__.py +0 -0
  40. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/program/main.py +0 -0
  41. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/py.typed +0 -0
  42. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/runners/__init__.py +0 -0
  43. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/runners/rich.py +0 -0
  44. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/scripts/__init__.py +0 -0
  45. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/scripts/loader.py +0 -0
  46. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/tasks/__init__.py +0 -0
  47. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/tasks/autocomplete.py +0 -0
  48. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/tasks/cache.py +0 -0
  49. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/tasks/types.py +0 -0
  50. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/testing.py +0 -0
  51. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/utils/__init__.py +0 -0
  52. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/utils/fzf.py +0 -0
  53. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/utils/inspection.py +0 -0
  54. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/utils/singleton.py +0 -0
  55. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/src/invoke_toolkit/utils/text.py +0 -0
  56. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/templates/package-template/.gitignore.jinja +0 -0
  57. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/templates/package-template/README.md.jinja +0 -0
  58. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/templates/package-template/copier.yml +0 -0
  59. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/templates/package-template/pyproject.toml.jinja +0 -0
  60. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
  61. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
  62. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/__init__.py +0 -0
  63. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/conftest.py +0 -0
  64. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/examples/cached_completion/tasks.py +0 -0
  65. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/examples/completion_with_config/.gitignore +0 -0
  66. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/examples/completion_with_config/tasks.py +0 -0
  67. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/examples/config_schema/tasks.py +0 -0
  68. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/examples/enum_select_size/tasks.py +0 -0
  69. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/examples/fzf_selector/tasks.py +0 -0
  70. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/examples/literal_set_level/tasks.py +0 -0
  71. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/extensions/conftest.py +0 -0
  72. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/extensions/test_config_tasks.py +0 -0
  73. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/extensions/test_create.py +0 -0
  74. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/extensions/test_package_template_entrypoint.py +0 -0
  75. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/extensions/test_shell_tasks.py +0 -0
  76. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/__init__.py +0 -0
  77. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
  78. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
  79. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
  80. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/collection.py +0 -0
  81. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/echo.yaml +0 -0
  82. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
  83. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
  84. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
  85. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/no-dedupe.yaml +0 -0
  86. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/no-echo.yaml +0 -0
  87. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/package/invoke.yml +0 -0
  88. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/package/tasks/__init__.py +0 -0
  89. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
  90. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/runtime.py +0 -0
  91. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
  92. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
  93. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
  94. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/underscores/tasks.py +0 -0
  95. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/yaml/explicit.py +0 -0
  96. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/yaml/tasks.py +0 -0
  97. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/yml/explicit.py +0 -0
  98. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
  99. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/configs/yml/tasks.py +0 -0
  100. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/_support/has_modules.py +0 -0
  101. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/conftest.py +0 -0
  102. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/original_invoke/test_config.py +0 -0
  103. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/program/main.py +0 -0
  104. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/program/tasks/__init__.py +0 -0
  105. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/program/tasks/coll1.py +0 -0
  106. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/tasks/test_cache.py +0 -0
  107. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/tasks/test_extensions_config.py +0 -0
  108. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_annotated_help.py +0 -0
  109. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_cofig_class.py +0 -0
  110. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_collection.py +0 -0
  111. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_collection_configure.py +0 -0
  112. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_completion_with_choices.py +0 -0
  113. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_config_helper.py +0 -0
  114. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_config_registry.py +0 -0
  115. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_config_schema.py +0 -0
  116. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_console.py +0 -0
  117. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_context_class.py +0 -0
  118. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_disable_status_cli.py +0 -0
  119. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_enum_arguments.py +0 -0
  120. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_executor.py +0 -0
  121. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_file_completion.py +0 -0
  122. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_global_context.py +0 -0
  123. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_help_flags.py +0 -0
  124. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_invoke_compatibility.py +0 -0
  125. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_loader.py +0 -0
  126. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_parsing.py +0 -0
  127. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_program_with_collection.py +0 -0
  128. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/test_toplevel.py +0 -0
  129. {invoke_toolkit-0.0.62 → invoke_toolkit-0.0.64}/tests/utils/test_fzf.py +0 -0
@@ -18,3 +18,4 @@ invoke.yaml
18
18
  # Makrdowns
19
19
  *.md
20
20
  !docs/**/*.md
21
+ .worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invoke-toolkit
3
- Version: 0.0.62
3
+ Version: 0.0.64
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
@@ -36,7 +36,7 @@ Requires-Dist: appdirs
36
36
  Requires-Dist: attrs>=24.1.0
37
37
  Requires-Dist: cattrs>=24.1.0
38
38
  Requires-Dist: httpx>=0.28.1
39
- Requires-Dist: invoke>=2.2.1
39
+ Requires-Dist: invoke>=3.0.3
40
40
  Requires-Dist: platformdirs>=4.5.0
41
41
  Requires-Dist: pyyaml>=6.0.3
42
42
  Requires-Dist: rich>=13
@@ -45,7 +45,7 @@ classifiers = [
45
45
  "Typing :: Typed",
46
46
  ]
47
47
  dependencies = [
48
- "invoke>=2.2.1",
48
+ "invoke>=3.0.3",
49
49
  "rich>=13",
50
50
  "typing-extensions>=4.13.2",
51
51
  "appdirs",
@@ -70,8 +70,10 @@ class ToolkitContext(Context, ConfigProtocol):
70
70
  [Union[PathLike[str], str]], _GeneratorContextManager[None, None, None]
71
71
  ]
72
72
 
73
- def __init__(self, config: Optional[ToolkitConfig] = None) -> None:
74
- super().__init__(config)
73
+ def __init__(
74
+ self, config: Optional[ToolkitConfig] = None, remainder: str = ""
75
+ ) -> None:
76
+ super().__init__(config, remainder=remainder)
75
77
  self._set("_console", get_console())
76
78
  # Check if status is disabled in the config
77
79
  disabled = False
@@ -39,11 +39,16 @@ class ToolkitExecutor(Executor):
39
39
 
40
40
  :param core:
41
41
  An optional `.ParseResult` holding parsed core program arguments.
42
- Defaults to ``None``.
42
+ Defaults to an empty `.ParseResult` if not given.
43
+
44
+ .. versionchanged:: invoke 3.0
45
+ ``self.core`` now defaults to ``ParseResult()`` instead of
46
+ ``None``, matching upstream ``Executor`` behaviour and ensuring
47
+ ``core_parse_result.remainder`` is always accessible.
43
48
  """
44
49
  self.collection = collection
45
50
  self.config = config if config is not None else ToolkitConfig()
46
- self.core = core
51
+ self.core = core if core is not None else ParseResult()
47
52
 
48
53
  def execute(
49
54
  self, *tasks: Union[str, Tuple[str, Dict[str, Any]], ParserContext]
@@ -134,7 +139,7 @@ class ToolkitExecutor(Executor):
134
139
  # Get final context from the Call (which will know how to generate
135
140
  # an appropriate one; e.g. subclasses might use extra data from
136
141
  # being parameterized), handing in this config for use there.
137
- context = call.make_context(config) # type: ignore[attr-defined]
142
+ context = call.make_context(config, core_parse_result=self.core) # type: ignore[attr-defined]
138
143
  args = (context, *call.args) # type: ignore[attr-defined]
139
144
  result = call.task(*args, **call.kwargs) # type: ignore[attr-defined]
140
145
  if autoprint:
@@ -15,7 +15,7 @@ from importlib import metadata
15
15
  from importlib.util import module_from_spec
16
16
  from logging import getLogger
17
17
  from pathlib import Path
18
- from typing import Dict, List, Optional, Sequence, Tuple, Union
18
+ from typing import Dict, List, Literal, Optional, Sequence, Tuple, Union
19
19
 
20
20
  from rich.table import Table
21
21
 
@@ -81,6 +81,9 @@ class ToolkitProgram(Program):
81
81
  config_class,
82
82
  binary_names,
83
83
  )
84
+ # Names of sub-collections that were loaded from entry-point plugins.
85
+ # Populated in load_collection(); used by --list-tasks / --list-plugins.
86
+ self._plugin_collection_names: set[str] = set()
84
87
 
85
88
  def get_version(self) -> str:
86
89
  """Compute version
@@ -368,10 +371,92 @@ class ToolkitProgram(Program):
368
371
  default=False,
369
372
  help="Initializes shell for runner via CLI flag",
370
373
  ),
374
+ ToolkitArgument(
375
+ names=("list-tasks", "lt"),
376
+ kind=bool,
377
+ default=False,
378
+ help="List only the main tasks discovered in the current directory (excludes plugins)",
379
+ ),
380
+ ToolkitArgument(
381
+ names=("list-plugins", "lp"),
382
+ kind=bool,
383
+ default=False,
384
+ help="List only the plugin-provided task collections (excludes main tasks)",
385
+ ),
371
386
  ]
372
387
  args.extend(toolkit_program_arguments)
373
388
  return args
374
389
 
390
+ def _handle_list_flags(self) -> None:
391
+ """
392
+ Handle --list, --list-tasks, and --list-plugins flags.
393
+
394
+ Sets ``list_format`` / ``list_depth`` / ``scoped_collection`` as needed
395
+ and raises `Exit` when any listing flag is active.
396
+ """
397
+ self.list_format = self.args["list-format"].value
398
+ self.list_depth = self.args["list-depth"].value
399
+
400
+ # --list [root]: standard task listing (optionally scoped to a sub-collection)
401
+ list_root = self.args.list.value # True or a string namespace
402
+ if list_root:
403
+ if isinstance(list_root, str):
404
+ self.list_root = list_root
405
+ try:
406
+ sub = self.collection.subcollection_from_path(list_root)
407
+ self.scoped_collection = sub
408
+ except KeyError:
409
+ msg = "Sub-collection '{}' not found!"
410
+ raise Exit(msg.format(list_root))
411
+ self.list_tasks()
412
+ raise Exit
413
+
414
+ # --list-tasks: show only main (non-plugin) tasks
415
+ if self.args["list-tasks"].value:
416
+ debug("Saw --list-tasks, listing main tasks only & exiting")
417
+ self.scoped_collection = self._make_filtered_collection("tasks")
418
+ self.list_tasks()
419
+ raise Exit
420
+
421
+ # --list-plugins: show only plugin-provided collections
422
+ if self.args["list-plugins"].value:
423
+ debug("Saw --list-plugins, listing plugin collections only & exiting")
424
+ if not self._plugin_collection_names:
425
+ raise Exit("No plugin collections found.")
426
+ self.scoped_collection = self._make_filtered_collection("plugins")
427
+ self.list_tasks()
428
+ raise Exit
429
+
430
+ def _make_filtered_collection(
431
+ self, mode: "Literal['tasks', 'plugins']"
432
+ ) -> "ToolkitCollection":
433
+ """
434
+ Return a shallow copy of ``self.scoped_collection`` containing only
435
+ the subset of tasks and sub-collections appropriate for *mode*.
436
+
437
+ - ``'tasks'`` – direct tasks + non-plugin sub-collections only
438
+ - ``'plugins'`` – plugin sub-collections only (no direct tasks)
439
+ """
440
+ source = self.scoped_collection
441
+ filtered = ToolkitCollection(source.name or "")
442
+ if mode == "tasks":
443
+ # Include all direct tasks on the scoped collection
444
+ for task_name, task in source.tasks.items():
445
+ filtered.add_task(task, name=task_name)
446
+ # Include sub-collections that are NOT from plugins
447
+ for coll_name, coll in source.collections.items():
448
+ if coll_name not in self._plugin_collection_names:
449
+ filtered.add_collection(coll, name=coll_name)
450
+ else: # mode == "plugins"
451
+ # Include only the plugin sub-collections
452
+ for coll_name, coll in source.collections.items():
453
+ if coll_name in self._plugin_collection_names:
454
+ filtered.add_collection(coll, name=coll_name)
455
+ # Preserve the default task pointer when it still makes sense
456
+ if source.default and source.default in filtered.tasks:
457
+ filtered.default = source.default
458
+ return filtered
459
+
375
460
  def _load_entry_points_collection(self, start: str) -> "ToolkitCollection | None":
376
461
  """
377
462
  Load task collections from entry points.
@@ -453,6 +538,7 @@ class ToolkitProgram(Program):
453
538
  debug("Merging entry point collections with tasks.py")
454
539
  for name, collection in ep_collection.collections.items():
455
540
  self.collection.add_collection(collection, name=name)
541
+ self._plugin_collection_names.add(name)
456
542
  except CollectionNotFound as e:
457
543
  start = self.args["search-root"].value or "."
458
544
  # First try to load entry points (for packages without tasks.py)
@@ -460,6 +546,7 @@ class ToolkitProgram(Program):
460
546
  if ep_collection is not None:
461
547
  debug("Loading collections from entry points")
462
548
  self.collection = ep_collection
549
+ self._plugin_collection_names.update(ep_collection.collections.keys())
463
550
  else:
464
551
  # If entry points weren't found, try local_tasks or fail
465
552
  if not (Path(start) / "local_tasks.py").exists():
@@ -571,23 +658,8 @@ class ToolkitProgram(Program):
571
658
  # should?
572
659
  raise ParseError("No idea what '{}' is!".format(halp))
573
660
 
574
- # Print discovered tasks if necessary
575
- list_root = self.args.list.value # will be True or string
576
-
577
- self.list_format = self.args["list-format"].value
578
- self.list_depth = self.args["list-depth"].value
579
- if list_root:
580
- # Not just --list, but --list some-root - do moar work
581
- if isinstance(list_root, str):
582
- self.list_root = list_root
583
- try:
584
- sub = self.collection.subcollection_from_path(list_root)
585
- self.scoped_collection = sub
586
- except KeyError:
587
- msg = "Sub-collection '{}' not found!"
588
- raise Exit(msg.format(list_root))
589
- self.list_tasks()
590
- raise Exit
661
+ # Handle --list / --list-tasks / --list-plugins (raises Exit if triggered)
662
+ self._handle_list_flags()
591
663
 
592
664
  # Print completion helpers if necessary
593
665
  if self.args.complete.value:
@@ -2,10 +2,11 @@
2
2
  Type annotated tasks and and overrides over invoke
3
3
  """
4
4
 
5
- # pylint: disable=too-many-statements
5
+ # pylint: disable=too-many-statements,duplicate-code
6
6
 
7
7
  import inspect
8
8
  import os
9
+ import warnings
9
10
  import subprocess
10
11
  import types
11
12
  from enum import Enum
@@ -774,7 +775,22 @@ def task( # pylint: disable=too-many-arguments,too-many-branches
774
775
  if name is not None:
775
776
  task_kwargs["name"] = name
776
777
  if aliases:
777
- task_kwargs["aliases"] = aliases
778
+ # Normalize a name to invoke's canonical form (underscores → hyphens).
779
+ effective_name = (name or getattr(f, "__name__", "")).replace("_", "-")
780
+ filtered_aliases = [
781
+ a for a in aliases if a.replace("_", "-") != effective_name
782
+ ]
783
+ skipped = set(aliases) - set(filtered_aliases)
784
+ if skipped:
785
+ warnings.warn(
786
+ f"Task '{effective_name}': alias(es) {sorted(skipped)!r} "
787
+ "match the task name and will be ignored to prevent a "
788
+ "RecursionError at runtime.",
789
+ UserWarning,
790
+ stacklevel=2,
791
+ )
792
+ if filtered_aliases:
793
+ task_kwargs["aliases"] = filtered_aliases
778
794
  if positional:
779
795
  task_kwargs["positional"] = positional
780
796
  if optional:
@@ -822,9 +838,16 @@ def task( # pylint: disable=too-many-arguments,too-many-branches
822
838
 
823
839
 
824
840
  class ToolkitCall(Call):
825
- def make_context(self, config):
826
- """Generates the Context for the task"""
827
- return ToolkitContext(config=config)
841
+ def make_context(self, config, core_parse_result=None):
842
+ """Generates the Context for the task.
843
+
844
+ .. versionchanged:: invoke 3.0
845
+ Added the ``core_parse_result`` parameter so that
846
+ ``Context.remainder`` is populated from the CLI parser's remainder
847
+ value (text after a standalone ``--``).
848
+ """
849
+ remainder = core_parse_result.remainder if core_parse_result is not None else ""
850
+ return ToolkitContext(config=config, remainder=remainder)
828
851
 
829
852
 
830
853
  def call(task_: "Task", *args: Any, **kwargs: Any) -> "Call":
@@ -146,10 +146,12 @@ def test_script_loads_project_config(tmp_path: Path, capsys, monkeypatch):
146
146
  # Create a temporary directory with a script and config file
147
147
  monkeypatch.chdir(tmp_path)
148
148
 
149
- # Create invoke.yml config file with echo=true and pty=true
149
+ # Create invoke.yml config file with echo=true
150
+ # Note: pty=true is intentionally omitted — a PTY requires a real terminal
151
+ # and would cause pytest to raise OSError when output is captured.
150
152
  config_file = tmp_path / "invoke.yml"
151
153
  config_file.write_text(
152
- "run:\n echo: true\n pty: true\n",
154
+ "run:\n echo: true\n",
153
155
  encoding="utf-8",
154
156
  )
155
157
 
@@ -158,8 +160,7 @@ def test_script_loads_project_config(tmp_path: Path, capsys, monkeypatch):
158
160
  def check_config(c):
159
161
  # Check that config was loaded
160
162
  assert c.config.run.echo is True, "echo should be True from invoke.yml"
161
- assert c.config.run.pty is True, "pty should be True from invoke.yml"
162
- c.run("echo 'Config loaded successfully'")
163
+ c.run("echo 'Config loaded successfully'", in_stream=False)
163
164
 
164
165
  # Run script and verify config was loaded
165
166
  # Note: need to pass script name as first arg for invoke
@@ -0,0 +1,108 @@
1
+ """Tests for self-referential alias guard in @task."""
2
+
3
+ import warnings
4
+
5
+ import pytest
6
+ from invoke import Collection
7
+
8
+ from invoke_toolkit.tasks.tasks import task
9
+
10
+
11
+ def _make_collection(*tasks):
12
+ c = Collection()
13
+ for t in tasks:
14
+ c.add_task(t)
15
+ return c
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Warning is emitted
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ def test_warns_when_alias_matches_function_name_underscore():
24
+ """Alias equal to the function name (underscore form) triggers a UserWarning."""
25
+ with pytest.warns(UserWarning, match="match the task name"):
26
+
27
+ @task(aliases=["my_task"])
28
+ def my_task(ctx):
29
+ pass
30
+
31
+
32
+ def test_warns_when_alias_matches_function_name_hyphen():
33
+ """Alias equal to the hyphenated function name triggers a UserWarning."""
34
+ with pytest.warns(UserWarning, match="match the task name"):
35
+
36
+ @task(aliases=["my-task"])
37
+ def my_task(ctx):
38
+ pass
39
+
40
+
41
+ def test_warns_when_alias_matches_explicit_name():
42
+ """Alias equal to an explicit name= override triggers a UserWarning."""
43
+ with pytest.warns(UserWarning, match="match the task name"):
44
+
45
+ @task(name="deploy", aliases=["deploy"])
46
+ def run_deploy(ctx):
47
+ pass
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Self-referential alias is stripped (no RecursionError)
52
+ # ---------------------------------------------------------------------------
53
+
54
+
55
+ def test_self_alias_stripped_task_is_reachable():
56
+ """After stripping the self-alias the task is still reachable by its name."""
57
+ with warnings.catch_warnings():
58
+ warnings.simplefilter("ignore", UserWarning)
59
+
60
+ @task(aliases=["my_task"])
61
+ def my_task(ctx):
62
+ pass
63
+
64
+ c = _make_collection(my_task)
65
+ # Must not raise RecursionError
66
+ resolved = c["my-task"]
67
+ assert resolved is not None
68
+
69
+
70
+ def test_self_alias_stripped_valid_aliases_kept():
71
+ """Valid aliases are preserved; only the self-referential one is removed."""
72
+ with warnings.catch_warnings():
73
+ warnings.simplefilter("ignore", UserWarning)
74
+
75
+ @task(aliases=["my_task", "mt"])
76
+ def my_task(ctx):
77
+ pass
78
+
79
+ c = _make_collection(my_task)
80
+ assert c["mt"] is not None
81
+ assert c["my-task"] is not None
82
+
83
+
84
+ def test_all_aliases_self_referential_no_aliases_registered():
85
+ """When every alias is self-referential the task has no aliases."""
86
+ with warnings.catch_warnings():
87
+ warnings.simplefilter("ignore", UserWarning)
88
+
89
+ @task(aliases=["my_task3"])
90
+ def my_task3(ctx):
91
+ pass
92
+
93
+ c = _make_collection(my_task3)
94
+ # task_names maps canonical name → list of aliases; list should be empty
95
+ assert c.task_names["my-task3"] == []
96
+
97
+
98
+ def test_no_warning_for_distinct_alias():
99
+ """A genuinely distinct alias produces no warning."""
100
+ with warnings.catch_warnings():
101
+ warnings.simplefilter("error", UserWarning) # would raise if a warning fires
102
+
103
+ @task(aliases=["mt"])
104
+ def my_task(ctx):
105
+ pass
106
+
107
+ c = _make_collection(my_task)
108
+ assert c["mt"] is not None
@@ -0,0 +1,184 @@
1
+ """
2
+ Tests for invoke 3.0.x compatibility.
3
+
4
+ invoke 3.0 introduced two related breaking changes that affect invoke-toolkit:
5
+
6
+ 1. ``Call.make_context`` now requires a second argument ``core_parse_result``
7
+ (a ``ParseResult`` instance) so that ``Context.remainder`` can be populated
8
+ from the CLI parser's remainder value (text after a standalone ``--``).
9
+
10
+ 2. ``Context.__init__`` now accepts a ``remainder`` keyword argument and stores
11
+ it as ``self.remainder``.
12
+
13
+ These tests verify that:
14
+ - ``ToolkitCall.make_context`` accepts ``core_parse_result`` and propagates
15
+ ``remainder`` into the resulting ``ToolkitContext``.
16
+ - ``ToolkitContext`` exposes ``ctx.remainder`` correctly.
17
+ - ``ToolkitExecutor`` never passes ``None`` as ``core_parse_result`` (it
18
+ defaults to an empty ``ParseResult()``).
19
+ - End-to-end: a task can read ``ctx.remainder`` when invoked with ``-- extra``.
20
+ """
21
+
22
+ from invoke.parser import ParseResult
23
+ from invoke.tasks import Task
24
+
25
+ from invoke_toolkit import Context, task
26
+ from invoke_toolkit.collections import ToolkitCollection
27
+ from invoke_toolkit.config import ToolkitConfig
28
+ from invoke_toolkit.context.context import ToolkitContext
29
+ from invoke_toolkit.executor import ToolkitExecutor
30
+ from invoke_toolkit.tasks.tasks import ToolkitCall
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Unit: ToolkitCall.make_context
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ def _dummy_task_func(ctx: Context) -> None: # pragma: no cover
39
+ """Minimal real task used as a ToolkitCall target in unit tests."""
40
+
41
+
42
+ _DUMMY_TASK: Task = task(_dummy_task_func) # type: ignore[assignment]
43
+
44
+
45
+ def _make_parse_result(remainder: str = "") -> ParseResult:
46
+ """Return a ParseResult whose .remainder is set to *remainder*."""
47
+ pr = ParseResult()
48
+ pr.remainder = remainder
49
+ return pr
50
+
51
+
52
+ def test_make_context_without_core_parse_result():
53
+ """make_context(config) with no core_parse_result → remainder defaults to ''."""
54
+ config = ToolkitConfig()
55
+ c = ToolkitCall(_DUMMY_TASK)
56
+ ctx = c.make_context(config)
57
+ assert isinstance(ctx, ToolkitContext)
58
+ assert ctx.remainder == ""
59
+
60
+
61
+ def test_make_context_with_empty_parse_result():
62
+ """make_context(config, core_parse_result=ParseResult()) → remainder is ''."""
63
+ config = ToolkitConfig()
64
+ c = ToolkitCall(_DUMMY_TASK)
65
+ ctx = c.make_context(config, core_parse_result=_make_parse_result(""))
66
+ assert isinstance(ctx, ToolkitContext)
67
+ assert ctx.remainder == ""
68
+
69
+
70
+ def test_make_context_propagates_remainder():
71
+ """make_context passes ParseResult.remainder into ToolkitContext.remainder."""
72
+ config = ToolkitConfig()
73
+ c = ToolkitCall(_DUMMY_TASK)
74
+ ctx = c.make_context(config, core_parse_result=_make_parse_result("extra args"))
75
+ assert ctx.remainder == "extra args"
76
+
77
+
78
+ def test_make_context_with_none_core_parse_result():
79
+ """Passing core_parse_result=None explicitly is safe (backward compat)."""
80
+ config = ToolkitConfig()
81
+ c = ToolkitCall(_DUMMY_TASK)
82
+ ctx = c.make_context(config, core_parse_result=None)
83
+ assert ctx.remainder == ""
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Unit: ToolkitContext.__init__
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ def test_toolkit_context_remainder_default():
92
+ """ToolkitContext() has remainder='' by default."""
93
+ ctx = ToolkitContext()
94
+ assert ctx.remainder == ""
95
+
96
+
97
+ def test_toolkit_context_remainder_kwarg():
98
+ """ToolkitContext(remainder=...) stores the value on the instance."""
99
+ ctx = ToolkitContext(remainder="docker run --rm alpine")
100
+ assert ctx.remainder == "docker run --rm alpine"
101
+
102
+
103
+ def test_toolkit_context_remainder_with_config():
104
+ """ToolkitContext(config=..., remainder=...) works with both args."""
105
+ config = ToolkitConfig()
106
+ ctx = ToolkitContext(config=config, remainder="--verbose")
107
+ assert ctx.remainder == "--verbose"
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Unit: ToolkitExecutor.__init__
112
+ # ---------------------------------------------------------------------------
113
+
114
+
115
+ def test_executor_core_defaults_to_parse_result():
116
+ """ToolkitExecutor.core is never None; defaults to ParseResult()."""
117
+ col = ToolkitCollection()
118
+ executor = ToolkitExecutor(collection=col)
119
+ assert executor.core is not None
120
+ assert isinstance(executor.core, ParseResult)
121
+
122
+
123
+ def test_executor_core_accepts_explicit_parse_result():
124
+ """ToolkitExecutor.core stores an explicitly provided ParseResult."""
125
+ col = ToolkitCollection()
126
+ pr = _make_parse_result("some remainder")
127
+ executor = ToolkitExecutor(collection=col, core=pr)
128
+ assert executor.core is pr
129
+ assert executor.core.remainder == "some remainder"
130
+
131
+
132
+ def test_executor_core_none_becomes_empty_parse_result():
133
+ """Passing core=None explicitly still yields a ParseResult(), not None."""
134
+ col = ToolkitCollection()
135
+ executor = ToolkitExecutor(collection=col, core=None)
136
+ assert executor.core is not None
137
+ assert isinstance(executor.core, ParseResult)
138
+ assert executor.core.remainder == ""
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Integration: ctx.remainder is accessible inside a task
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ def test_task_receives_remainder_via_context():
147
+ """A task can read ctx.remainder when the program is given a remainder."""
148
+ captured: list[str] = []
149
+
150
+ ns = ToolkitCollection()
151
+
152
+ @task # type: ignore[arg-type]
153
+ def wrapper_task(ctx: Context):
154
+ """Wrapper task that captures ctx.remainder."""
155
+ captured.append(ctx.remainder)
156
+
157
+ ns.add_task(wrapper_task) # type: ignore[arg-type]
158
+
159
+ pr = _make_parse_result("echo hello world")
160
+ config = ToolkitConfig()
161
+ executor = ToolkitExecutor(collection=ns, config=config, core=pr)
162
+ executor.execute("wrapper-task")
163
+
164
+ assert len(captured) == 1
165
+ assert captured[0] == "echo hello world"
166
+
167
+
168
+ def test_task_remainder_empty_when_not_provided():
169
+ """ctx.remainder is '' when no remainder was parsed."""
170
+ captured: list[str] = []
171
+
172
+ ns = ToolkitCollection()
173
+
174
+ @task # type: ignore[arg-type]
175
+ def simple_task(ctx: Context):
176
+ """Simple task that captures ctx.remainder."""
177
+ captured.append(ctx.remainder)
178
+
179
+ ns.add_task(simple_task) # type: ignore[arg-type]
180
+
181
+ executor = ToolkitExecutor(collection=ns)
182
+ executor.execute("simple-task")
183
+
184
+ assert captured == [""]