invoke-toolkit 0.0.66__tar.gz → 0.0.69__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 (151) hide show
  1. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/PKG-INFO +1 -1
  2. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/collections.py +7 -0
  3. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/runners/rich.py +37 -6
  4. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_collection.py +24 -0
  5. invoke_toolkit-0.0.69/tests/test_pty_stdin.py +24 -0
  6. invoke_toolkit-0.0.69/tests/test_release_guard.py +85 -0
  7. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/.gitignore +0 -0
  8. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/LICENSE.txt +0 -0
  9. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/README.md +0 -0
  10. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/pyproject.toml +0 -0
  11. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/__init__.py +0 -0
  12. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/__main__.py +0 -0
  13. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/completion.py +0 -0
  14. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/config/__init__.py +0 -0
  15. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/config/config.py +0 -0
  16. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/config/registry.py +0 -0
  17. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/config/schema.py +0 -0
  18. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/config/status_helper.py +0 -0
  19. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/context/__init__.py +0 -0
  20. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/context/async_tools.py +0 -0
  21. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/context/context.py +0 -0
  22. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/context/types.py +0 -0
  23. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/executor.py +0 -0
  24. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/extensions/__init__.py +0 -0
  25. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/extensions/tasks/__init__.py +0 -0
  26. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/extensions/tasks/config.py +0 -0
  27. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/extensions/tasks/create.py +0 -0
  28. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/extensions/tasks/dist.py +0 -0
  29. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/extensions/tasks/shell.py +0 -0
  30. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/field_resolvers.py +0 -0
  31. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/loader/entrypoint.py +0 -0
  32. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/log/__init__.py +0 -0
  33. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/log/logger.py +0 -0
  34. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/output/__init__.py +0 -0
  35. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/output/console.py +0 -0
  36. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/output/utils.py +0 -0
  37. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/parser.py +0 -0
  38. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/program/__init__.py +0 -0
  39. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/program/main.py +0 -0
  40. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/program/program.py +0 -0
  41. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/py.typed +0 -0
  42. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/runners/__init__.py +0 -0
  43. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/scripts/__init__.py +0 -0
  44. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/scripts/loader.py +0 -0
  45. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/tasks/__init__.py +0 -0
  46. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/tasks/autocomplete.py +0 -0
  47. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/tasks/cache.py +0 -0
  48. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/tasks/fields.py +0 -0
  49. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/tasks/fields.pyi +0 -0
  50. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/tasks/tasks.py +0 -0
  51. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/tasks/types.py +0 -0
  52. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/testing.py +0 -0
  53. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/utils/__init__.py +0 -0
  54. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/utils/fzf.py +0 -0
  55. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/utils/inspection.py +0 -0
  56. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/utils/singleton.py +0 -0
  57. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/src/invoke_toolkit/utils/text.py +0 -0
  58. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/package-template/.gitignore.jinja +0 -0
  59. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/package-template/README.md.jinja +0 -0
  60. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/package-template/copier.yml +0 -0
  61. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/package-template/pyproject.toml.jinja +0 -0
  62. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/package-template/src/{{package_slug}}/__init__.py.jinja +0 -0
  63. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/package-template/src/{{package_slug}}/tasks.py.jinja +0 -0
  64. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/provider-template/.gitignore.jinja +0 -0
  65. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/provider-template/README.md.jinja +0 -0
  66. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/provider-template/copier.yml +0 -0
  67. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/provider-template/pyproject.toml.jinja +0 -0
  68. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/provider-template/src/{{package_slug}}/__init__.py.jinja +0 -0
  69. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/templates/provider-template/tests/test_resolver.py.jinja +0 -0
  70. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/__init__.py +0 -0
  71. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/conftest.py +0 -0
  72. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/cached_completion/tasks.py +0 -0
  73. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/completion_with_config/.gitignore +0 -0
  74. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/completion_with_config/tasks.py +0 -0
  75. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/config_schema/tasks.py +0 -0
  76. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/enum_select_size/tasks.py +0 -0
  77. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/fzf_selector/tasks.py +0 -0
  78. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/literal_set_level/tasks.py +0 -0
  79. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/local_field_resolver/tasks.py +0 -0
  80. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/local_tasks/local_tasks.py +0 -0
  81. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/examples/local_tasks/tasks.py +0 -0
  82. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/extensions/conftest.py +0 -0
  83. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/extensions/test_config_tasks.py +0 -0
  84. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/extensions/test_create.py +0 -0
  85. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/extensions/test_package_template_entrypoint.py +0 -0
  86. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/extensions/test_shell_tasks.py +0 -0
  87. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/fixtures/future_fields.py +0 -0
  88. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/__init__.py +0 -0
  89. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/all-four/invoke.json +0 -0
  90. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/all-four/invoke.py +0 -0
  91. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/all-four/invoke.yml +0 -0
  92. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/collection.py +0 -0
  93. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/echo.yaml +0 -0
  94. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/json/invoke.json +0 -0
  95. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/json-and-python/invoke.json +0 -0
  96. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/json-and-python/invoke.py +0 -0
  97. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/no-dedupe.yaml +0 -0
  98. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/no-echo.yaml +0 -0
  99. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/package/invoke.yml +0 -0
  100. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/package/tasks/__init__.py +0 -0
  101. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/python/invoke.py +0 -0
  102. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/runtime.py +0 -0
  103. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/three-of-em/invoke.json +0 -0
  104. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/three-of-em/invoke.py +0 -0
  105. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/three-of-em/invoke.yml +0 -0
  106. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/underscores/tasks.py +0 -0
  107. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/yaml/explicit.py +0 -0
  108. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/yaml/tasks.py +0 -0
  109. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/yml/explicit.py +0 -0
  110. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/yml/invoke.yml +0 -0
  111. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/configs/yml/tasks.py +0 -0
  112. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/_support/has_modules.py +0 -0
  113. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/conftest.py +0 -0
  114. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/original_invoke/test_config.py +0 -0
  115. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/program/main.py +0 -0
  116. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/program/tasks/__init__.py +0 -0
  117. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/program/tasks/coll1.py +0 -0
  118. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/script/test_script.py +0 -0
  119. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/tasks/test_cache.py +0 -0
  120. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/tasks/test_extensions_config.py +0 -0
  121. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_alias_self_reference.py +0 -0
  122. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_annotated_help.py +0 -0
  123. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_async_edge_cases.py +0 -0
  124. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_async_support.py +0 -0
  125. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_cofig_class.py +0 -0
  126. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_collection_configure.py +0 -0
  127. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_completion_with_choices.py +0 -0
  128. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_config_helper.py +0 -0
  129. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_config_registry.py +0 -0
  130. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_config_schema.py +0 -0
  131. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_console.py +0 -0
  132. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_context_class.py +0 -0
  133. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_disable_status_cli.py +0 -0
  134. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_enum_arguments.py +0 -0
  135. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_executor.py +0 -0
  136. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_field_typing.py +0 -0
  137. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_fields.py +0 -0
  138. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_file_completion.py +0 -0
  139. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_global_context.py +0 -0
  140. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_help_flags.py +0 -0
  141. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_invoke_3_compat.py +0 -0
  142. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_invoke_compatibility.py +0 -0
  143. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_list_filter_flags.py +0 -0
  144. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_loader.py +0 -0
  145. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_parsing.py +0 -0
  146. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_proctitle.py +0 -0
  147. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_program_with_collection.py +0 -0
  148. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/test_toplevel.py +0 -0
  149. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/typecheck/fields.py +0 -0
  150. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/typecheck/fields_negative.py.fixture +0 -0
  151. {invoke_toolkit-0.0.66 → invoke_toolkit-0.0.69}/tests/utils/test_fzf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: invoke-toolkit
3
- Version: 0.0.66
3
+ Version: 0.0.69
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
@@ -250,6 +250,13 @@ class ToolkitCollection(Collection):
250
250
  if not local_tasks_file.exists():
251
251
  debug(f"No local_tasks.py found at {local_tasks_file}")
252
252
  return
253
+ if "local" in self.tasks:
254
+ logger.warning(
255
+ "Skipping local_tasks.py at %s because tasks.py already defines "
256
+ "the 'local' task namespace",
257
+ local_tasks_file,
258
+ )
259
+ return
253
260
 
254
261
  debug(f"Loading local tasks from {local_tasks_file}")
255
262
 
@@ -3,15 +3,24 @@ Config object used for invoke attribute resolution
3
3
  in tasks.
4
4
  """
5
5
 
6
+ import array
7
+ import errno
6
8
  import sys
7
- from typing import TYPE_CHECKING
9
+ from typing import IO, TYPE_CHECKING
8
10
 
9
- from invoke.runners import Local
10
- from invoke.util import debug
11
+ from invoke.runners import Local, ready_for_reading
12
+ from invoke.util import debug, has_fileno, isatty
11
13
  from rich.syntax import Syntax
12
14
 
13
15
  from invoke_toolkit.output import get_console
14
16
 
17
+ if sys.platform != "win32":
18
+ import fcntl
19
+ import termios
20
+ else:
21
+ fcntl = None # pylint: disable=invalid-name
22
+ termios = None # pylint: disable=invalid-name
23
+
15
24
  if TYPE_CHECKING:
16
25
  from invoke_toolkit.config.status_helper import StatusHelper
17
26
 
@@ -96,9 +105,31 @@ class OutputCapturingStream:
96
105
 
97
106
 
98
107
  class NoStdoutRunner(Local):
99
- """Invoke runner that prints to stderr when invoke is used with -e/--echo
100
- and redacts secrets from subprocess output when redaction is enabled.
101
- """
108
+ """Invoke runner with output handling and safe POSIX stdin probing."""
109
+
110
+ def read_our_stdin(self, input_: IO) -> str | None:
111
+ """Read stdin without Invoke's undersized POSIX ioctl buffer."""
112
+ if not ready_for_reading(input_):
113
+ return None
114
+
115
+ bytes_to_read = 1
116
+ if sys.platform != "win32" and isatty(input_) and has_fileno(input_):
117
+ buffer = array.array("i", [0])
118
+ try:
119
+ fcntl.ioctl(input_, termios.FIONREAD, buffer, True)
120
+ bytes_to_read = max(buffer[0], 1)
121
+ except OSError:
122
+ pass
123
+
124
+ try:
125
+ bytes_ = input_.read(bytes_to_read)
126
+ except OSError as e:
127
+ if e.errno != errno.EBADF:
128
+ raise
129
+ bytes_ = None
130
+ if bytes_ and isinstance(bytes_, bytes):
131
+ bytes_ = self.decode(bytes_)
132
+ return bytes_
102
133
 
103
134
  def echo(self, command):
104
135
  if hasattr(self.context, "print"):
@@ -1,10 +1,13 @@
1
1
  import ast
2
+ import logging
2
3
  import os
3
4
  import subprocess
4
5
  import sys
5
6
  from pathlib import Path
6
7
  from textwrap import dedent
8
+ from typing import Any, cast
7
9
 
10
+ from invoke_toolkit import Task, task
8
11
  from invoke_toolkit.collections import ToolkitCollection
9
12
 
10
13
 
@@ -216,6 +219,27 @@ def test_load_local_tasks_without_main_tasks(tmp_path: Path):
216
219
  assert "standalone-task" in local_col.tasks
217
220
 
218
221
 
222
+ def test_load_local_tasks_preserves_conflicting_main_task(tmp_path: Path, caplog):
223
+ """A main task named local takes precedence over local_tasks.py."""
224
+
225
+ def main_task(ctx):
226
+ pass
227
+
228
+ decorated_main_task = cast(Task[Any], task()(main_task))
229
+ ns = ToolkitCollection()
230
+ ns.add_task(decorated_main_task, aliases=("local",))
231
+ (tmp_path / "local_tasks.py").write_text(
232
+ "from invoke_toolkit import task\n\n@task()\ndef local_task(ctx):\n pass\n"
233
+ )
234
+
235
+ with caplog.at_level(logging.WARNING, logger="invoke"):
236
+ ns.load_local_tasks(search_path=tmp_path)
237
+
238
+ assert ns.tasks["local"] is decorated_main_task
239
+ assert "local" not in ns.collections
240
+ assert "Skipping local_tasks.py" in caplog.text
241
+
242
+
219
243
  def test_load_local_tasks_from_multiple_directories_without_module_collision(
220
244
  tmp_path: Path,
221
245
  ):
@@ -0,0 +1,24 @@
1
+ import os
2
+ import tty
3
+ from contextlib import closing
4
+
5
+ import pytest
6
+
7
+ from invoke_toolkit import Context
8
+ from invoke_toolkit.runners.rich import NoStdoutRunner
9
+
10
+
11
+ @pytest.mark.skipif(os.name == "nt", reason="PTYs are POSIX-only")
12
+ def test_read_our_stdin_handles_escape_sequence_from_pty():
13
+ master_fd, slave_fd = os.openpty()
14
+ try:
15
+ tty.setcbreak(slave_fd)
16
+ os.write(master_fd, b"\x1b[A")
17
+ stream = os.fdopen(slave_fd, "rb", closefd=False)
18
+ with closing(stream):
19
+ runner = NoStdoutRunner(Context())
20
+ runner.encoding = "utf-8"
21
+ assert runner.read_our_stdin(stream) == "\x1b[A"
22
+ finally:
23
+ os.close(master_fd)
24
+ os.close(slave_fd)
@@ -0,0 +1,85 @@
1
+ import os
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+
8
+ REPOSITORY_ROOT = Path(__file__).parents[1]
9
+ GUARD = REPOSITORY_ROOT / "scripts" / "validate-release-tag.sh"
10
+
11
+
12
+ def run(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
13
+ return subprocess.run(command, cwd=cwd, check=True, capture_output=True, text=True)
14
+
15
+
16
+ @pytest.fixture
17
+ def release_repository(tmp_path: Path) -> Path:
18
+ remote = tmp_path / "remote.git"
19
+ repository = tmp_path / "repository"
20
+
21
+ run(["git", "init", "--bare", str(remote)], tmp_path)
22
+ run(["git", "clone", str(remote), str(repository)], tmp_path)
23
+ run(["git", "config", "user.email", "test@example.com"], repository)
24
+ run(["git", "config", "user.name", "Test User"], repository)
25
+ (repository / "README").write_text("initial\n")
26
+ run(["git", "add", "README"], repository)
27
+ run(["git", "commit", "-m", "initial"], repository)
28
+ run(["git", "branch", "-M", "main"], repository)
29
+ run(["git", "push", "-u", "origin", "main"], repository)
30
+ return repository
31
+
32
+
33
+ def guard(tag_name: str, repository: Path) -> subprocess.CompletedProcess[str]:
34
+ output = repository / "github-output"
35
+ return subprocess.run(
36
+ [str(GUARD)],
37
+ cwd=repository,
38
+ env={**os.environ, "TAG_NAME": tag_name, "GITHUB_OUTPUT": str(output)},
39
+ capture_output=True,
40
+ text=True,
41
+ check=False,
42
+ )
43
+
44
+
45
+ def test_accepts_canonical_tag_at_origin_main(release_repository: Path) -> None:
46
+ run(["git", "tag", "v0.0.67"], release_repository)
47
+ run(["git", "push", "origin", "v0.0.67"], release_repository)
48
+
49
+ result = guard("v0.0.67", release_repository)
50
+
51
+ assert result.returncode == 0
52
+
53
+
54
+ def test_rejects_noncanonical_uppercase_tag(release_repository: Path) -> None:
55
+ run(["git", "tag", "V0.0.67"], release_repository)
56
+ run(["git", "push", "origin", "V0.0.67"], release_repository)
57
+
58
+ result = guard("V0.0.67", release_repository)
59
+
60
+ assert result.returncode != 0
61
+ assert "canonical lowercase tag" in result.stderr
62
+
63
+
64
+ def test_rejects_tag_behind_updated_origin_main(
65
+ release_repository: Path, tmp_path: Path
66
+ ) -> None:
67
+ run(["git", "tag", "v0.0.67"], release_repository)
68
+ run(["git", "push", "origin", "v0.0.67"], release_repository)
69
+
70
+ updater = tmp_path / "updater"
71
+ run(
72
+ ["git", "clone", str(release_repository.parent / "remote.git"), str(updater)],
73
+ tmp_path,
74
+ )
75
+ run(["git", "config", "user.email", "test@example.com"], updater)
76
+ run(["git", "config", "user.name", "Test User"], updater)
77
+ run(["git", "checkout", "main"], updater)
78
+ (updater / "README").write_text("updated\n")
79
+ run(["git", "commit", "-am", "advance main"], updater)
80
+ run(["git", "push"], updater)
81
+
82
+ result = guard("v0.0.67", release_repository)
83
+
84
+ assert result.returncode != 0
85
+ assert "does not point to origin/main" in result.stderr