ansible-core 2.19.0b5__py3-none-any.whl → 2.19.0b7__py3-none-any.whl

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 (184) hide show
  1. ansible/_internal/_ansiballz/__init__.py +0 -0
  2. ansible/_internal/_ansiballz/_builder.py +101 -0
  3. ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
  4. ansible/_internal/_templating/_jinja_bits.py +22 -4
  5. ansible/_internal/_templating/_jinja_common.py +1 -1
  6. ansible/_internal/_templating/_jinja_plugins.py +5 -2
  7. ansible/_internal/_templating/_template_vars.py +72 -0
  8. ansible/_internal/_templating/_transform.py +6 -0
  9. ansible/_internal/_yaml/_constructor.py +4 -4
  10. ansible/_internal/_yaml/_dumper.py +26 -18
  11. ansible/cli/__init__.py +9 -14
  12. ansible/cli/adhoc.py +6 -3
  13. ansible/cli/arguments/option_helpers.py +1 -1
  14. ansible/cli/console.py +2 -2
  15. ansible/cli/doc.py +4 -4
  16. ansible/cli/inventory.py +5 -7
  17. ansible/config/base.yml +33 -6
  18. ansible/errors/__init__.py +2 -1
  19. ansible/executor/module_common.py +75 -44
  20. ansible/executor/powershell/psrp_put_file.ps1 +1 -1
  21. ansible/executor/process/worker.py +2 -2
  22. ansible/executor/task_executor.py +2 -2
  23. ansible/executor/task_queue_manager.py +34 -70
  24. ansible/executor/task_result.py +1 -1
  25. ansible/galaxy/api.py +3 -6
  26. ansible/galaxy/collection/__init__.py +1 -6
  27. ansible/galaxy/collection/concrete_artifact_manager.py +4 -10
  28. ansible/galaxy/dependency_resolution/providers.py +3 -3
  29. ansible/galaxy/role.py +2 -2
  30. ansible/inventory/group.py +6 -1
  31. ansible/inventory/host.py +6 -1
  32. ansible/module_utils/_internal/__init__.py +7 -4
  33. ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
  34. ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
  35. ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
  36. ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
  37. ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +10 -38
  38. ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
  39. ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
  40. ansible/module_utils/_internal/_datatag/__init__.py +23 -1
  41. ansible/module_utils/_internal/_deprecator.py +39 -34
  42. ansible/module_utils/_internal/_json/_profiles/__init__.py +1 -0
  43. ansible/module_utils/_internal/_messages.py +26 -2
  44. ansible/module_utils/_internal/_plugin_info.py +14 -1
  45. ansible/module_utils/ansible_release.py +1 -1
  46. ansible/module_utils/basic.py +58 -70
  47. ansible/module_utils/common/respawn.py +4 -41
  48. ansible/module_utils/common/yaml.py +1 -1
  49. ansible/module_utils/connection.py +8 -11
  50. ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
  51. ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
  52. ansible/module_utils/facts/hardware/base.py +1 -1
  53. ansible/module_utils/facts/hardware/linux.py +1 -1
  54. ansible/module_utils/facts/other/facter.py +1 -1
  55. ansible/module_utils/facts/sysctl.py +4 -6
  56. ansible/module_utils/facts/system/caps.py +2 -2
  57. ansible/module_utils/facts/system/distribution.py +2 -2
  58. ansible/module_utils/facts/system/local.py +1 -1
  59. ansible/module_utils/facts/virtual/linux.py +1 -1
  60. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  61. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  62. ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
  63. ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
  64. ansible/module_utils/service.py +1 -1
  65. ansible/module_utils/urls.py +5 -5
  66. ansible/modules/apt.py +9 -3
  67. ansible/modules/apt_repository.py +10 -10
  68. ansible/modules/assemble.py +7 -5
  69. ansible/modules/async_wrapper.py +7 -17
  70. ansible/modules/command.py +3 -3
  71. ansible/modules/copy.py +4 -4
  72. ansible/modules/cron.py +1 -1
  73. ansible/modules/expect.py +5 -5
  74. ansible/modules/file.py +16 -17
  75. ansible/modules/find.py +3 -3
  76. ansible/modules/get_url.py +17 -0
  77. ansible/modules/git.py +9 -7
  78. ansible/modules/hostname.py +2 -2
  79. ansible/modules/known_hosts.py +12 -14
  80. ansible/modules/package.py +6 -0
  81. ansible/modules/pip.py +9 -11
  82. ansible/modules/raw.py +2 -2
  83. ansible/modules/replace.py +2 -2
  84. ansible/modules/slurp.py +10 -13
  85. ansible/modules/stat.py +6 -8
  86. ansible/modules/unarchive.py +6 -6
  87. ansible/modules/user.py +1 -1
  88. ansible/modules/wait_for.py +38 -33
  89. ansible/modules/yum_repository.py +4 -3
  90. ansible/parsing/dataloader.py +2 -2
  91. ansible/parsing/mod_args.py +38 -20
  92. ansible/parsing/vault/__init__.py +9 -13
  93. ansible/playbook/base.py +7 -4
  94. ansible/playbook/helpers.py +1 -1
  95. ansible/playbook/included_file.py +3 -1
  96. ansible/playbook/play_context.py +2 -0
  97. ansible/playbook/playbook_include.py +23 -56
  98. ansible/playbook/role/__init__.py +38 -21
  99. ansible/playbook/taggable.py +19 -5
  100. ansible/playbook/task.py +2 -0
  101. ansible/plugins/action/__init__.py +2 -2
  102. ansible/plugins/action/assemble.py +2 -1
  103. ansible/plugins/action/assert.py +2 -2
  104. ansible/plugins/action/fetch.py +3 -3
  105. ansible/plugins/action/script.py +5 -4
  106. ansible/plugins/action/template.py +9 -3
  107. ansible/plugins/cache/__init__.py +17 -19
  108. ansible/plugins/callback/__init__.py +77 -87
  109. ansible/plugins/callback/default.py +0 -3
  110. ansible/plugins/callback/junit.py +0 -6
  111. ansible/plugins/callback/tree.py +5 -5
  112. ansible/plugins/connection/local.py +4 -4
  113. ansible/plugins/connection/paramiko_ssh.py +5 -5
  114. ansible/plugins/connection/ssh.py +9 -7
  115. ansible/plugins/connection/winrm.py +1 -1
  116. ansible/plugins/filter/core.py +19 -21
  117. ansible/plugins/filter/encryption.py +10 -2
  118. ansible/plugins/filter/pow.yml +1 -1
  119. ansible/plugins/filter/root.yml +1 -1
  120. ansible/plugins/filter/strftime.yml +3 -3
  121. ansible/plugins/filter/to_uuid.yml +1 -1
  122. ansible/plugins/inventory/script.py +1 -1
  123. ansible/plugins/list.py +5 -4
  124. ansible/plugins/loader.py +5 -0
  125. ansible/plugins/lookup/password.py +4 -6
  126. ansible/plugins/lookup/template.py +9 -4
  127. ansible/plugins/shell/powershell.py +3 -2
  128. ansible/plugins/shell/sh.py +3 -2
  129. ansible/plugins/strategy/__init__.py +3 -3
  130. ansible/plugins/test/core.py +2 -2
  131. ansible/release.py +1 -1
  132. ansible/template/__init__.py +9 -53
  133. ansible/utils/collection_loader/_collection_finder.py +3 -3
  134. ansible/utils/display.py +38 -37
  135. ansible/utils/galaxy.py +2 -2
  136. ansible/utils/hashing.py +6 -7
  137. ansible/utils/path.py +6 -8
  138. ansible/utils/py3compat.py +2 -1
  139. ansible/utils/ssh_functions.py +3 -2
  140. ansible/utils/vars.py +4 -1
  141. ansible/vars/manager.py +6 -3
  142. ansible/vars/plugins.py +3 -3
  143. ansible/vars/reserved.py +6 -4
  144. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/METADATA +1 -1
  145. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/RECORD +184 -173
  146. ansible_test/_internal/__init__.py +5 -0
  147. ansible_test/_internal/ansible_util.py +1 -1
  148. ansible_test/_internal/classification/python.py +6 -0
  149. ansible_test/_internal/cli/commands/__init__.py +0 -5
  150. ansible_test/_internal/cli/environments.py +51 -5
  151. ansible_test/_internal/commands/coverage/__init__.py +1 -1
  152. ansible_test/_internal/commands/integration/__init__.py +18 -5
  153. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  154. ansible_test/_internal/commands/integration/coverage.py +7 -2
  155. ansible_test/_internal/commands/sanity/__init__.py +3 -1
  156. ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
  157. ansible_test/_internal/commands/shell/__init__.py +43 -4
  158. ansible_test/_internal/commands/units/__init__.py +4 -1
  159. ansible_test/_internal/config.py +21 -13
  160. ansible_test/_internal/debugging.py +166 -0
  161. ansible_test/_internal/delegation.py +21 -13
  162. ansible_test/_internal/host_profiles.py +259 -16
  163. ansible_test/_internal/inventory.py +4 -0
  164. ansible_test/_internal/metadata.py +94 -4
  165. ansible_test/_internal/processes.py +80 -0
  166. ansible_test/_internal/provisioning.py +10 -4
  167. ansible_test/_internal/python_requirements.py +27 -0
  168. ansible_test/_internal/ssh.py +1 -5
  169. ansible_test/_internal/target.py +8 -0
  170. ansible_test/_internal/thread.py +2 -1
  171. ansible_test/_internal/timeout.py +1 -1
  172. ansible_test/_internal/util.py +20 -12
  173. ansible_test/_internal/util_common.py +13 -3
  174. ansible_test/_util/target/injector/python.py +8 -0
  175. ansible_test/_util/target/setup/requirements.py +3 -9
  176. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/WHEEL +0 -0
  177. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/entry_points.txt +0 -0
  178. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/COPYING +0 -0
  179. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  180. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  181. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  182. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  183. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  184. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,80 @@
1
+ """Wrappers around `ps` for querying running processes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import collections
6
+ import dataclasses
7
+ import os
8
+ import pathlib
9
+ import shlex
10
+
11
+ from ansible_test._internal.util import raw_command
12
+
13
+
14
+ @dataclasses.dataclass(frozen=True)
15
+ class ProcessData:
16
+ """Data about a running process."""
17
+
18
+ pid: int
19
+ ppid: int
20
+ command: str
21
+
22
+
23
+ @dataclasses.dataclass(frozen=True)
24
+ class Process:
25
+ """A process in the process tree."""
26
+
27
+ pid: int
28
+ command: str
29
+ parent: Process | None = None
30
+ children: tuple[Process, ...] = dataclasses.field(default_factory=tuple)
31
+
32
+ @property
33
+ def args(self) -> list[str]:
34
+ """The list of arguments that make up `command`."""
35
+ return shlex.split(self.command)
36
+
37
+ @property
38
+ def path(self) -> pathlib.Path:
39
+ """The path to the process."""
40
+ return pathlib.Path(self.args[0])
41
+
42
+
43
+ def get_process_data(pids: list[int] | None = None) -> list[ProcessData]:
44
+ """Return a list of running processes."""
45
+ if pids:
46
+ args = ['-p', ','.join(map(str, pids))]
47
+ else:
48
+ args = ['-A']
49
+
50
+ lines = raw_command(['ps'] + args + ['-o', 'pid,ppid,command'], capture=True)[0].splitlines()[1:]
51
+ processes = [ProcessData(pid=int(pid), ppid=int(ppid), command=command) for pid, ppid, command in (line.split(maxsplit=2) for line in lines)]
52
+
53
+ return processes
54
+
55
+
56
+ def get_process_tree() -> dict[int, Process]:
57
+ """Return the process tree."""
58
+ processes = get_process_data()
59
+ pid_to_process: dict[int, Process] = {}
60
+ pid_to_children: dict[int, list[Process]] = collections.defaultdict(list)
61
+
62
+ for data in processes:
63
+ pid_to_process[data.pid] = process = Process(pid=data.pid, command=data.command)
64
+
65
+ if data.ppid:
66
+ pid_to_children[data.ppid].append(process)
67
+
68
+ for data in processes:
69
+ pid_to_process[data.pid] = dataclasses.replace(
70
+ pid_to_process[data.pid],
71
+ parent=pid_to_process.get(data.ppid),
72
+ children=tuple(pid_to_children[data.pid]),
73
+ )
74
+
75
+ return pid_to_process
76
+
77
+
78
+ def get_current_process() -> Process:
79
+ """Return the current process along with its ancestors and descendants."""
80
+ return get_process_tree()[os.getpid()]
@@ -116,9 +116,11 @@ def prepare_profiles(
116
116
  else:
117
117
  run_pypi_proxy(args, targets_use_pypi)
118
118
 
119
+ controller_host_profile = t.cast(ControllerHostProfile, create_host_profile(args, args.controller, None))
120
+
119
121
  host_state = HostState(
120
- controller_profile=t.cast(ControllerHostProfile, create_host_profile(args, args.controller, True)),
121
- target_profiles=[create_host_profile(args, target, False) for target in args.targets],
122
+ controller_profile=controller_host_profile,
123
+ target_profiles=[create_host_profile(args, target, controller_host_profile) for target in args.targets],
122
124
  )
123
125
 
124
126
  if args.prime_containers:
@@ -137,7 +139,9 @@ def prepare_profiles(
137
139
  if not skip_setup:
138
140
  profile.setup()
139
141
 
140
- dispatch_jobs([(profile, WrappedThread(functools.partial(provision, profile))) for profile in host_state.profiles])
142
+ dispatch_jobs(
143
+ [(profile, WrappedThread(functools.partial(provision, profile), f'Provision: {profile}')) for profile in host_state.profiles]
144
+ )
141
145
 
142
146
  host_state.controller_profile.configure()
143
147
 
@@ -157,7 +161,9 @@ def prepare_profiles(
157
161
  if requirements:
158
162
  requirements(profile)
159
163
 
160
- dispatch_jobs([(profile, WrappedThread(functools.partial(configure, profile))) for profile in host_state.target_profiles])
164
+ dispatch_jobs(
165
+ [(profile, WrappedThread(functools.partial(configure, profile), f'Configure: {profile}')) for profile in host_state.target_profiles]
166
+ )
161
167
 
162
168
  return host_state
163
169
 
@@ -55,6 +55,11 @@ from .coverage_util import (
55
55
  get_coverage_version,
56
56
  )
57
57
 
58
+ if t.TYPE_CHECKING:
59
+ from .host_profiles import (
60
+ HostProfile,
61
+ )
62
+
58
63
  QUIET_PIP_SCRIPT_PATH = os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'quiet_pip.py')
59
64
  REQUIREMENTS_SCRIPT_PATH = os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'requirements.py')
60
65
 
@@ -122,6 +127,7 @@ class PipBootstrap(PipCommand):
122
127
 
123
128
  def install_requirements(
124
129
  args: EnvironmentConfig,
130
+ host_profile: HostProfile | None,
125
131
  python: PythonConfig,
126
132
  ansible: bool = False,
127
133
  command: bool = False,
@@ -133,6 +139,7 @@ def install_requirements(
133
139
  create_result_directories(args)
134
140
 
135
141
  if not requirements_allowed(args, controller):
142
+ post_install(host_profile)
136
143
  return
137
144
 
138
145
  if command and isinstance(args, (UnitsConfig, IntegrationConfig)) and args.coverage:
@@ -161,7 +168,17 @@ def install_requirements(
161
168
  sanity=None,
162
169
  )
163
170
 
171
+ from .host_profiles import DebuggableProfile
172
+
173
+ if isinstance(host_profile, DebuggableProfile) and host_profile.debugging_enabled and args.metadata.debugger_settings.package:
174
+ commands.append(PipInstall(
175
+ requirements=[],
176
+ constraints=[],
177
+ packages=[args.metadata.debugger_settings.package],
178
+ ))
179
+
164
180
  if not commands:
181
+ post_install(host_profile)
165
182
  return
166
183
 
167
184
  run_pip(args, python, commands, connection)
@@ -170,6 +187,16 @@ def install_requirements(
170
187
  if any(isinstance(command, PipInstall) and command.has_package('pyyaml') for command in commands):
171
188
  check_pyyaml(python)
172
189
 
190
+ post_install(host_profile)
191
+
192
+
193
+ def post_install(host_profile: HostProfile) -> None:
194
+ """Operations to perform after requirements are installed."""
195
+ from .host_profiles import DebuggableProfile
196
+
197
+ if isinstance(host_profile, DebuggableProfile):
198
+ host_profile.activate_debugger()
199
+
173
200
 
174
201
  def collect_bootstrap(python: PythonConfig) -> list[PipCommand]:
175
202
  """Return the details necessary to bootstrap pip into an empty virtual environment."""
@@ -13,7 +13,6 @@ import shlex
13
13
  import typing as t
14
14
 
15
15
  from .encoding import (
16
- to_bytes,
17
16
  to_text,
18
17
  )
19
18
 
@@ -223,13 +222,10 @@ def run_ssh_command(
223
222
  cmd_show = shlex.join(cmd)
224
223
  display.info('Run background command: %s' % cmd_show, verbosity=1, truncate=True)
225
224
 
226
- cmd_bytes = [to_bytes(arg) for arg in cmd]
227
- env_bytes = dict((to_bytes(k), to_bytes(v)) for k, v in env.items())
228
-
229
225
  if args.explain:
230
226
  process = SshProcess(None)
231
227
  else:
232
- process = SshProcess(subprocess.Popen(cmd_bytes, env=env_bytes, bufsize=-1, # pylint: disable=consider-using-with
228
+ process = SshProcess(subprocess.Popen(cmd, env=env, bufsize=-1, # pylint: disable=consider-using-with
233
229
  stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
234
230
 
235
231
  return process
@@ -582,6 +582,14 @@ class IntegrationTarget(CompletionTarget):
582
582
  else:
583
583
  static_aliases = tuple()
584
584
 
585
+ # non-group aliases which need to be extracted before group mangling occurs
586
+
587
+ self.env_set: dict[str, str] = {
588
+ match.group('key'): match.group('value') for match in (
589
+ re.match(r'env/set/(?P<key>[^/]+)/(?P<value>.*)', alias) for alias in static_aliases
590
+ ) if match
591
+ }
592
+
585
593
  # modules
586
594
 
587
595
  if self.name in modules:
@@ -17,11 +17,12 @@ TCallable = t.TypeVar('TCallable', bound=t.Callable[..., t.Any])
17
17
  class WrappedThread(threading.Thread):
18
18
  """Wrapper around Thread which captures results and exceptions."""
19
19
 
20
- def __init__(self, action: c.Callable[[], t.Any]) -> None:
20
+ def __init__(self, action: c.Callable[[], t.Any], name: str) -> None:
21
21
  super().__init__()
22
22
  self._result: queue.Queue[t.Any] = queue.Queue()
23
23
  self.action = action
24
24
  self.result = None
25
+ self.name = name
25
26
 
26
27
  def run(self) -> None:
27
28
  """
@@ -126,6 +126,6 @@ def configure_test_timeout(args: TestConfig) -> None:
126
126
 
127
127
  signal.signal(signal.SIGUSR1, timeout_handler)
128
128
 
129
- instance = WrappedThread(functools.partial(timeout_waiter, timeout_remaining.total_seconds()))
129
+ instance = WrappedThread(functools.partial(timeout_waiter, timeout_remaining.total_seconds()), 'Timeout Watchdog')
130
130
  instance.daemon = True
131
131
  instance.start()
@@ -533,16 +533,23 @@ def raw_command(
533
533
 
534
534
  try:
535
535
  try:
536
- cmd_bytes = [to_bytes(arg) for arg in cmd]
537
- env_bytes = dict((to_bytes(k), to_bytes(v)) for k, v in env.items())
538
- process = subprocess.Popen(cmd_bytes, env=env_bytes, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd) # pylint: disable=consider-using-with
536
+ process = subprocess.Popen(cmd, env=env, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd) # pylint: disable=consider-using-with
539
537
  except FileNotFoundError as ex:
540
538
  raise ApplicationError('Required program "%s" not found.' % cmd[0]) from ex
541
539
 
542
540
  if communicate:
543
541
  data_bytes = to_optional_bytes(data)
544
- stdout_bytes, stderr_bytes = communicate_with_process(process, data_bytes, stdout == subprocess.PIPE, stderr == subprocess.PIPE, capture=capture,
545
- output_stream=output_stream)
542
+
543
+ stdout_bytes, stderr_bytes = communicate_with_process(
544
+ name=cmd[0],
545
+ process=process,
546
+ stdin=data_bytes,
547
+ stdout=stdout == subprocess.PIPE,
548
+ stderr=stderr == subprocess.PIPE,
549
+ capture=capture,
550
+ output_stream=output_stream,
551
+ )
552
+
546
553
  stdout_text = to_optional_text(stdout_bytes, str_errors) or ''
547
554
  stderr_text = to_optional_text(stderr_bytes, str_errors) or ''
548
555
  else:
@@ -566,6 +573,7 @@ def raw_command(
566
573
 
567
574
 
568
575
  def communicate_with_process(
576
+ name: str,
569
577
  process: subprocess.Popen,
570
578
  stdin: t.Optional[bytes],
571
579
  stdout: bool,
@@ -583,16 +591,16 @@ def communicate_with_process(
583
591
  reader = OutputThread
584
592
 
585
593
  if stdin is not None:
586
- threads.append(WriterThread(process.stdin, stdin))
594
+ threads.append(WriterThread(process.stdin, stdin, name))
587
595
 
588
596
  if stdout:
589
- stdout_reader = reader(process.stdout, output_stream.get_buffer(sys.stdout.buffer))
597
+ stdout_reader = reader(process.stdout, output_stream.get_buffer(sys.stdout.buffer), name)
590
598
  threads.append(stdout_reader)
591
599
  else:
592
600
  stdout_reader = None
593
601
 
594
602
  if stderr:
595
- stderr_reader = reader(process.stderr, output_stream.get_buffer(sys.stderr.buffer))
603
+ stderr_reader = reader(process.stderr, output_stream.get_buffer(sys.stderr.buffer), name)
596
604
  threads.append(stderr_reader)
597
605
  else:
598
606
  stderr_reader = None
@@ -624,8 +632,8 @@ def communicate_with_process(
624
632
  class WriterThread(WrappedThread):
625
633
  """Thread to write data to stdin of a subprocess."""
626
634
 
627
- def __init__(self, handle: t.IO[bytes], data: bytes) -> None:
628
- super().__init__(self._run)
635
+ def __init__(self, handle: t.IO[bytes], data: bytes, name: str) -> None:
636
+ super().__init__(self._run, f'{self.__class__.__name__}: {name}')
629
637
 
630
638
  self.handle = handle
631
639
  self.data = data
@@ -642,8 +650,8 @@ class WriterThread(WrappedThread):
642
650
  class ReaderThread(WrappedThread, metaclass=abc.ABCMeta):
643
651
  """Thread to read stdout from a subprocess."""
644
652
 
645
- def __init__(self, handle: t.IO[bytes], buffer: t.BinaryIO) -> None:
646
- super().__init__(self._run)
653
+ def __init__(self, handle: t.IO[bytes], buffer: t.BinaryIO, name: str) -> None:
654
+ super().__init__(self._run, f'{self.__class__.__name__}: {name}')
647
655
 
648
656
  self.handle = handle
649
657
  self.buffer = buffer
@@ -451,10 +451,20 @@ def intercept_python(
451
451
  """
452
452
  Run a command while intercepting invocations of Python to control the version used.
453
453
  If the specified Python is an ansible-test managed virtual environment, it will be added to PATH to activate it.
454
- Otherwise a temporary directory will be created to ensure the correct Python can be found in PATH.
454
+ Otherwise, a temporary directory will be created to ensure the correct Python can be found in PATH.
455
455
  """
456
- env = env.copy()
457
456
  cmd = list(cmd)
457
+ env = get_injector_env(python, env)
458
+
459
+ return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd, always=always)
460
+
461
+
462
+ def get_injector_env(
463
+ python: PythonConfig,
464
+ env: dict[str, str],
465
+ ) -> dict[str, str]:
466
+ """Get the environment variables needed to inject the given Python interpreter into the environment."""
467
+ env = env.copy()
458
468
  inject_path = get_injector_path()
459
469
 
460
470
  # make sure scripts (including injector.py) find the correct Python interpreter
@@ -467,7 +477,7 @@ def intercept_python(
467
477
  env['ANSIBLE_TEST_PYTHON_VERSION'] = python.version
468
478
  env['ANSIBLE_TEST_PYTHON_INTERPRETER'] = python.path
469
479
 
470
- return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd, always=always)
480
+ return env
471
481
 
472
482
 
473
483
  def run_command(
@@ -15,6 +15,7 @@ def main():
15
15
  args = [sys.executable]
16
16
 
17
17
  ansible_lib_root = os.environ.get('ANSIBLE_TEST_ANSIBLE_LIB_ROOT')
18
+ debugger_config = os.environ.get('ANSIBLE_TEST_DEBUGGER_CONFIG')
18
19
  coverage_config = os.environ.get('COVERAGE_CONF')
19
20
  coverage_output = os.environ.get('COVERAGE_FILE')
20
21
 
@@ -28,6 +29,13 @@ def main():
28
29
  sys.exit('ERROR: Could not find `coverage` module. '
29
30
  'Did you use a virtualenv created without --system-site-packages or with the wrong interpreter?')
30
31
 
32
+ if debugger_config:
33
+ import json
34
+
35
+ debugger_options = json.loads(debugger_config)
36
+ os.environ.update(debugger_options['env'])
37
+ args += debugger_options['args']
38
+
31
39
  if name == 'python.py':
32
40
  if sys.argv[1] == '-c':
33
41
  # prevent simple misuse of python.py with -c which does not work with coverage
@@ -19,7 +19,6 @@ if DESIRED_RLIMIT_NOFILE < CURRENT_RLIMIT_NOFILE:
19
19
 
20
20
  import base64
21
21
  import contextlib
22
- import errno
23
22
  import io
24
23
  import json
25
24
  import os
@@ -349,18 +348,13 @@ def remove_tree(path): # type: (str) -> None
349
348
  """Remove the specified directory tree."""
350
349
  try:
351
350
  shutil.rmtree(to_bytes(path))
352
- except OSError as ex:
353
- if ex.errno != errno.ENOENT:
354
- raise
351
+ except FileNotFoundError:
352
+ pass
355
353
 
356
354
 
357
355
  def make_dirs(path): # type: (str) -> None
358
356
  """Create a directory at path, including any necessary parent directories."""
359
- try:
360
- os.makedirs(to_bytes(path))
361
- except OSError as ex:
362
- if ex.errno != errno.EEXIST:
363
- raise
357
+ os.makedirs(to_bytes(path), exist_ok=True)
364
358
 
365
359
 
366
360
  def open_binary_file(path, mode='rb'): # type: (str, str) -> t.IO[bytes]