ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b6__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 (225) hide show
  1. ansible/_internal/__init__.py +1 -1
  2. ansible/_internal/_ansiballz/__init__.py +0 -0
  3. ansible/_internal/_ansiballz/_builder.py +101 -0
  4. ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
  5. ansible/_internal/_collection_proxy.py +1 -1
  6. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  7. ansible/_internal/_errors/_captured.py +25 -30
  8. ansible/_internal/_errors/_error_factory.py +89 -0
  9. ansible/_internal/_errors/_error_utils.py +240 -0
  10. ansible/_internal/_errors/_task_timeout.py +28 -0
  11. ansible/_internal/_event_formatting.py +127 -0
  12. ansible/_internal/_json/__init__.py +5 -5
  13. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  14. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  15. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  16. ansible/_internal/_ssh/__init__.py +0 -0
  17. ansible/_internal/_ssh/_agent_launch.py +91 -0
  18. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  19. ansible/_internal/_templating/__init__.py +5 -3
  20. ansible/_internal/_templating/_datatag.py +2 -1
  21. ansible/_internal/_templating/_engine.py +3 -4
  22. ansible/_internal/_templating/_jinja_bits.py +28 -20
  23. ansible/_internal/_templating/_jinja_common.py +18 -27
  24. ansible/_internal/_templating/_jinja_plugins.py +36 -5
  25. ansible/_internal/_templating/_lazy_containers.py +5 -5
  26. ansible/_internal/_templating/_template_vars.py +72 -0
  27. ansible/_internal/_templating/_transform.py +26 -19
  28. ansible/_internal/_templating/_utils.py +1 -1
  29. ansible/_internal/_yaml/_constructor.py +4 -4
  30. ansible/_internal/_yaml/_dumper.py +26 -18
  31. ansible/_internal/_yaml/_errors.py +7 -7
  32. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  33. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  34. ansible/cli/__init__.py +11 -93
  35. ansible/cli/arguments/option_helpers.py +3 -4
  36. ansible/cli/console.py +1 -1
  37. ansible/cli/doc.py +86 -30
  38. ansible/cli/inventory.py +5 -7
  39. ansible/compat/importlib_resources.py +9 -12
  40. ansible/config/base.yml +46 -0
  41. ansible/errors/__init__.py +98 -50
  42. ansible/executor/module_common.py +75 -49
  43. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  44. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  45. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  46. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  47. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  48. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  49. ansible/executor/powershell/module_manifest.py +52 -0
  50. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  51. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  52. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  53. ansible/executor/process/worker.py +40 -115
  54. ansible/executor/task_executor.py +26 -61
  55. ansible/executor/task_result.py +2 -4
  56. ansible/galaxy/api.py +1 -4
  57. ansible/galaxy/collection/__init__.py +2 -10
  58. ansible/galaxy/collection/concrete_artifact_manager.py +2 -8
  59. ansible/galaxy/role.py +2 -2
  60. ansible/inventory/manager.py +1 -1
  61. ansible/module_utils/_internal/__init__.py +7 -7
  62. ansible/module_utils/_internal/_ambient_context.py +3 -3
  63. ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
  64. ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
  65. ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
  66. ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
  67. ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +13 -39
  68. ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
  69. ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
  70. ansible/module_utils/_internal/_datatag/__init__.py +43 -15
  71. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  72. ansible/module_utils/_internal/_deprecator.py +67 -55
  73. ansible/module_utils/_internal/_errors.py +88 -17
  74. ansible/module_utils/_internal/_event_utils.py +61 -0
  75. ansible/module_utils/_internal/_json/_profiles/__init__.py +22 -4
  76. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  77. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  78. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  79. ansible/module_utils/{common/messages.py → _internal/_messages.py} +54 -49
  80. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  81. ansible/module_utils/_internal/_plugin_info.py +15 -2
  82. ansible/module_utils/_internal/_stack.py +22 -0
  83. ansible/module_utils/_internal/_text_utils.py +6 -0
  84. ansible/module_utils/_internal/_traceback.py +11 -8
  85. ansible/module_utils/ansible_release.py +1 -1
  86. ansible/module_utils/basic.py +95 -71
  87. ansible/module_utils/common/arg_spec.py +2 -2
  88. ansible/module_utils/common/collections.py +6 -0
  89. ansible/module_utils/common/json.py +2 -2
  90. ansible/module_utils/common/respawn.py +4 -41
  91. ansible/module_utils/common/text/converters.py +3 -3
  92. ansible/module_utils/common/validation.py +1 -1
  93. ansible/module_utils/common/warnings.py +80 -23
  94. ansible/module_utils/common/yaml.py +1 -1
  95. ansible/module_utils/connection.py +8 -11
  96. ansible/module_utils/datatag.py +5 -2
  97. ansible/module_utils/facts/hardware/linux.py +1 -1
  98. ansible/module_utils/facts/sysctl.py +4 -6
  99. ansible/module_utils/facts/system/caps.py +2 -2
  100. ansible/module_utils/facts/system/distribution.py +16 -3
  101. ansible/module_utils/facts/system/local.py +1 -1
  102. ansible/module_utils/facts/virtual/linux.py +2 -2
  103. ansible/module_utils/service.py +3 -10
  104. ansible/module_utils/urls.py +4 -4
  105. ansible/modules/apt_repository.py +17 -39
  106. ansible/modules/assemble.py +2 -2
  107. ansible/modules/async_status.py +13 -11
  108. ansible/modules/async_wrapper.py +12 -22
  109. ansible/modules/command.py +3 -3
  110. ansible/modules/copy.py +4 -4
  111. ansible/modules/cron.py +1 -1
  112. ansible/modules/dnf5.py +14 -22
  113. ansible/modules/file.py +16 -17
  114. ansible/modules/find.py +3 -3
  115. ansible/modules/get_url.py +17 -0
  116. ansible/modules/git.py +9 -7
  117. ansible/modules/hostname.py +0 -1
  118. ansible/modules/known_hosts.py +12 -14
  119. ansible/modules/package.py +6 -0
  120. ansible/modules/replace.py +2 -2
  121. ansible/modules/service.py +3 -9
  122. ansible/modules/slurp.py +10 -13
  123. ansible/modules/stat.py +5 -7
  124. ansible/modules/unarchive.py +6 -6
  125. ansible/modules/user.py +1 -1
  126. ansible/modules/wait_for.py +28 -30
  127. ansible/modules/yum_repository.py +4 -3
  128. ansible/parsing/ajson.py +3 -5
  129. ansible/parsing/dataloader.py +6 -6
  130. ansible/parsing/mod_args.py +1 -1
  131. ansible/parsing/plugin_docs.py +2 -2
  132. ansible/parsing/utils/yaml.py +3 -3
  133. ansible/parsing/vault/__init__.py +10 -14
  134. ansible/playbook/base.py +7 -2
  135. ansible/playbook/included_file.py +3 -1
  136. ansible/playbook/play_context.py +2 -0
  137. ansible/playbook/playbook_include.py +1 -1
  138. ansible/playbook/taggable.py +19 -8
  139. ansible/playbook/task.py +2 -0
  140. ansible/plugins/__init__.py +0 -25
  141. ansible/plugins/action/__init__.py +8 -31
  142. ansible/plugins/action/add_host.py +1 -1
  143. ansible/plugins/action/assemble.py +8 -16
  144. ansible/plugins/action/async_status.py +7 -2
  145. ansible/plugins/action/copy.py +8 -7
  146. ansible/plugins/action/fetch.py +3 -3
  147. ansible/plugins/action/gather_facts.py +8 -8
  148. ansible/plugins/action/package.py +5 -8
  149. ansible/plugins/action/script.py +8 -15
  150. ansible/plugins/action/service.py +3 -7
  151. ansible/plugins/action/template.py +11 -10
  152. ansible/plugins/action/unarchive.py +5 -15
  153. ansible/plugins/action/uri.py +9 -20
  154. ansible/plugins/cache/__init__.py +17 -19
  155. ansible/plugins/callback/__init__.py +4 -6
  156. ansible/plugins/callback/junit.py +4 -2
  157. ansible/plugins/callback/tree.py +5 -5
  158. ansible/plugins/connection/local.py +6 -6
  159. ansible/plugins/connection/paramiko_ssh.py +5 -5
  160. ansible/plugins/connection/ssh.py +25 -15
  161. ansible/plugins/connection/winrm.py +6 -3
  162. ansible/plugins/doc_fragments/constructed.py +2 -2
  163. ansible/plugins/filter/core.py +32 -27
  164. ansible/plugins/filter/encryption.py +14 -6
  165. ansible/plugins/inventory/__init__.py +11 -10
  166. ansible/plugins/inventory/script.py +1 -1
  167. ansible/plugins/list.py +73 -19
  168. ansible/plugins/loader.py +7 -7
  169. ansible/plugins/lookup/csvfile.py +16 -71
  170. ansible/plugins/lookup/first_found.py +2 -1
  171. ansible/plugins/lookup/template.py +9 -4
  172. ansible/plugins/shell/__init__.py +56 -2
  173. ansible/plugins/shell/powershell.py +67 -9
  174. ansible/plugins/shell/sh.py +10 -5
  175. ansible/plugins/strategy/__init__.py +3 -3
  176. ansible/plugins/test/core.py +22 -16
  177. ansible/plugins/test/finished.yml +1 -1
  178. ansible/plugins/test/uri.py +2 -5
  179. ansible/release.py +1 -1
  180. ansible/template/__init__.py +38 -54
  181. ansible/utils/collection_loader/_collection_finder.py +3 -3
  182. ansible/utils/display.py +124 -138
  183. ansible/utils/galaxy.py +2 -2
  184. ansible/utils/hashing.py +6 -8
  185. ansible/utils/listify.py +6 -4
  186. ansible/utils/path.py +5 -7
  187. ansible/utils/py3compat.py +2 -1
  188. ansible/utils/ssh_functions.py +3 -2
  189. ansible/utils/unsafe_proxy.py +1 -1
  190. ansible/vars/hostvars.py +1 -1
  191. ansible/vars/plugins.py +3 -3
  192. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/METADATA +1 -1
  193. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/RECORD +224 -204
  194. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/WHEEL +1 -1
  195. ansible_test/_data/completion/docker.txt +3 -3
  196. ansible_test/_data/completion/remote.txt +1 -0
  197. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  198. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  199. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  200. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  201. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  202. ansible_test/_internal/commands/integration/coverage.py +7 -2
  203. ansible_test/_internal/host_profiles.py +62 -10
  204. ansible_test/_internal/provisioning.py +10 -4
  205. ansible_test/_internal/ssh.py +1 -5
  206. ansible_test/_internal/thread.py +2 -1
  207. ansible_test/_internal/timeout.py +1 -1
  208. ansible_test/_internal/util.py +40 -12
  209. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  210. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  211. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  212. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  213. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  214. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +61 -7
  215. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  216. ansible_test/_util/target/setup/requirements.py +3 -9
  217. ansible/_internal/_errors/_utils.py +0 -310
  218. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/entry_points.txt +0 -0
  219. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/COPYING +0 -0
  220. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  221. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  222. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  223. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  224. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  225. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/top_level.txt +0 -0
@@ -281,10 +281,41 @@ bootstrap_remote_rhel_9()
281
281
  done
282
282
  }
283
283
 
284
+ bootstrap_remote_rhel_10()
285
+ {
286
+ py_pkg_prefix="python3"
287
+
288
+ packages="
289
+ gcc
290
+ ${py_pkg_prefix}-devel
291
+ ${py_pkg_prefix}-pip
292
+ "
293
+
294
+ if [ "${controller}" ]; then
295
+ packages="
296
+ ${packages}
297
+ ${py_pkg_prefix}-cryptography
298
+ ${py_pkg_prefix}-jinja2
299
+ ${py_pkg_prefix}-packaging
300
+ ${py_pkg_prefix}-pyyaml
301
+ ${py_pkg_prefix}-resolvelib
302
+ "
303
+ fi
304
+
305
+ while true; do
306
+ # shellcheck disable=SC2086
307
+ dnf install -q -y ${packages} \
308
+ && break
309
+ echo "Failed to install packages. Sleeping before trying again..."
310
+ sleep 10
311
+ done
312
+ }
313
+
284
314
  bootstrap_remote_rhel()
285
315
  {
286
316
  case "${platform_version}" in
287
317
  9.*) bootstrap_remote_rhel_9 ;;
318
+ 10.*) bootstrap_remote_rhel_10 ;;
288
319
  esac
289
320
  }
290
321
 
@@ -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]
@@ -1,310 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import dataclasses
4
- import itertools
5
- import pathlib
6
- import sys
7
- import textwrap
8
- import typing as t
9
-
10
- from ansible.module_utils.common.messages import Detail, ErrorSummary
11
- from ansible._internal._datatag._tags import Origin
12
- from ansible.module_utils._internal import _ambient_context, _traceback
13
- from ansible import errors
14
-
15
- if t.TYPE_CHECKING:
16
- from ansible.utils.display import Display
17
-
18
-
19
- class RedactAnnotatedSourceContext(_ambient_context.AmbientContextBase):
20
- """
21
- When active, this context will redact annotated source lines, showing only the origin.
22
- """
23
-
24
-
25
- def _dedupe_and_concat_message_chain(message_parts: list[str]) -> str:
26
- message_parts = list(reversed(message_parts))
27
-
28
- message = message_parts.pop(0)
29
-
30
- for message_part in message_parts:
31
- # avoid duplicate messages where the cause was already concatenated to the exception message
32
- if message_part.endswith(message):
33
- message = message_part
34
- else:
35
- message = concat_message(message_part, message)
36
-
37
- return message
38
-
39
-
40
- def _collapse_error_details(error_details: t.Sequence[Detail]) -> list[Detail]:
41
- """
42
- Return a potentially modified error chain, with redundant errors collapsed into previous error(s) in the chain.
43
- This reduces the verbosity of messages by eliminating repetition when multiple errors in the chain share the same contextual information.
44
- """
45
- previous_error = error_details[0]
46
- previous_warnings: list[str] = []
47
- collapsed_error_details: list[tuple[Detail, list[str]]] = [(previous_error, previous_warnings)]
48
-
49
- for error in error_details[1:]:
50
- details_present = error.formatted_source_context or error.help_text
51
- details_changed = error.formatted_source_context != previous_error.formatted_source_context or error.help_text != previous_error.help_text
52
-
53
- if details_present and details_changed:
54
- previous_error = error
55
- previous_warnings = []
56
- collapsed_error_details.append((previous_error, previous_warnings))
57
- else:
58
- previous_warnings.append(error.msg)
59
-
60
- final_error_details: list[Detail] = []
61
-
62
- for error, messages in collapsed_error_details:
63
- final_error_details.append(dataclasses.replace(error, msg=_dedupe_and_concat_message_chain([error.msg] + messages)))
64
-
65
- return final_error_details
66
-
67
-
68
- def _get_cause(exception: BaseException) -> BaseException | None:
69
- # deprecated: description='remove support for orig_exc (deprecated in 2.23)' core_version='2.27'
70
-
71
- if not isinstance(exception, errors.AnsibleError):
72
- return exception.__cause__
73
-
74
- if exception.__cause__:
75
- if exception.orig_exc and exception.orig_exc is not exception.__cause__:
76
- _get_display().warning(
77
- msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given, but differed from the cause given by `raise ... from`.",
78
- )
79
-
80
- return exception.__cause__
81
-
82
- if exception.orig_exc:
83
- # encourage the use of `raise ... from` before deprecating `orig_exc`
84
- _get_display().warning(msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given without using `raise ... from orig_exc`.")
85
-
86
- return exception.orig_exc
87
-
88
- return None
89
-
90
-
91
- class _TemporaryDisplay:
92
- # DTFIX-FUTURE: generalize this and hide it in the display module so all users of Display can benefit
93
-
94
- @staticmethod
95
- def warning(*args, **kwargs):
96
- print(f'FALLBACK WARNING: {args} {kwargs}', file=sys.stderr)
97
-
98
- @staticmethod
99
- def deprecated(*args, **kwargs):
100
- print(f'FALLBACK DEPRECATION: {args} {kwargs}', file=sys.stderr)
101
-
102
-
103
- def _get_display() -> Display | _TemporaryDisplay:
104
- try:
105
- from ansible.utils.display import Display
106
- except ImportError:
107
- return _TemporaryDisplay()
108
-
109
- return Display()
110
-
111
-
112
- def _create_error_summary(exception: BaseException, event: _traceback.TracebackEvent | None = None) -> ErrorSummary:
113
- from . import _captured # avoid circular import due to AnsibleError import
114
-
115
- current_exception: BaseException | None = exception
116
- error_details: list[Detail] = []
117
-
118
- if event:
119
- formatted_traceback = _traceback.maybe_extract_traceback(exception, event)
120
- else:
121
- formatted_traceback = None
122
-
123
- while current_exception:
124
- if isinstance(current_exception, errors.AnsibleError):
125
- include_cause_message = current_exception._include_cause_message
126
- edc = Detail(
127
- msg=current_exception._original_message.strip(),
128
- formatted_source_context=current_exception._formatted_source_context,
129
- help_text=current_exception._help_text,
130
- )
131
- else:
132
- include_cause_message = True
133
- edc = Detail(
134
- msg=str(current_exception).strip(),
135
- )
136
-
137
- error_details.append(edc)
138
-
139
- if isinstance(current_exception, _captured.AnsibleCapturedError):
140
- detail = current_exception.error_summary
141
- error_details.extend(detail.details)
142
-
143
- if formatted_traceback and detail.formatted_traceback:
144
- formatted_traceback = (
145
- f'{detail.formatted_traceback}\n'
146
- f'The {current_exception.context} exception above was the direct cause of the following controller exception:\n\n'
147
- f'{formatted_traceback}'
148
- )
149
-
150
- if not include_cause_message:
151
- break
152
-
153
- current_exception = _get_cause(current_exception)
154
-
155
- return ErrorSummary(details=tuple(error_details), formatted_traceback=formatted_traceback)
156
-
157
-
158
- def concat_message(left: str, right: str) -> str:
159
- """Normalize `left` by removing trailing punctuation and spaces before appending new punctuation and `right`."""
160
- return f'{left.rstrip(". ")}: {right}'
161
-
162
-
163
- def get_chained_message(exception: BaseException) -> str:
164
- """
165
- Return the full chain of exception messages by concatenating the cause(s) until all are exhausted.
166
- """
167
- error_summary = _create_error_summary(exception)
168
- message_parts = [edc.msg for edc in error_summary.details]
169
-
170
- return _dedupe_and_concat_message_chain(message_parts)
171
-
172
-
173
- @dataclasses.dataclass(kw_only=True, frozen=True)
174
- class SourceContext:
175
- origin: Origin
176
- annotated_source_lines: list[str]
177
- target_line: str | None
178
-
179
- def __str__(self) -> str:
180
- msg_lines = [f'Origin: {self.origin}']
181
-
182
- if self.annotated_source_lines:
183
- msg_lines.append('')
184
- msg_lines.extend(self.annotated_source_lines)
185
-
186
- return '\n'.join(msg_lines)
187
-
188
- @classmethod
189
- def from_value(cls, value: t.Any) -> SourceContext | None:
190
- """Attempt to retrieve source and render a contextual indicator from the value's origin (if any)."""
191
- if value is None:
192
- return None
193
-
194
- if isinstance(value, Origin):
195
- origin = value
196
- value = None
197
- else:
198
- origin = Origin.get_tag(value)
199
-
200
- if RedactAnnotatedSourceContext.current(optional=True):
201
- return cls.error('content redacted')
202
-
203
- if origin and origin.path:
204
- return cls.from_origin(origin)
205
-
206
- # DTFIX-RELEASE: redaction context may not be sufficient to avoid secret disclosure without SensitiveData and other enhancements
207
- if value is None:
208
- truncated_value = None
209
- annotated_source_lines = []
210
- else:
211
- # DTFIX-FUTURE: cleanup/share width
212
- try:
213
- value = str(value)
214
- except Exception as ex:
215
- value = f'<< context unavailable: {ex} >>'
216
-
217
- truncated_value = textwrap.shorten(value, width=120)
218
- annotated_source_lines = [truncated_value]
219
-
220
- return SourceContext(
221
- origin=origin or Origin.UNKNOWN,
222
- annotated_source_lines=annotated_source_lines,
223
- target_line=truncated_value,
224
- )
225
-
226
- @staticmethod
227
- def error(message: str | None, origin: Origin | None = None) -> SourceContext:
228
- return SourceContext(
229
- origin=origin,
230
- annotated_source_lines=[f'(source not shown: {message})'] if message else [],
231
- target_line=None,
232
- )
233
-
234
- @classmethod
235
- def from_origin(cls, origin: Origin) -> SourceContext:
236
- """Attempt to retrieve source and render a contextual indicator of an error location."""
237
- from ansible.parsing.vault import is_encrypted # avoid circular import
238
-
239
- # DTFIX-FUTURE: support referencing the column after the end of the target line, so we can indicate where a missing character (quote) needs to be added
240
- # this is also useful for cases like end-of-stream reported by the YAML parser
241
-
242
- # DTFIX-FUTURE: Implement line wrapping and match annotated line width to the terminal display width.
243
-
244
- context_line_count: t.Final = 2
245
- max_annotated_line_width: t.Final = 120
246
- truncation_marker: t.Final = '...'
247
-
248
- target_line_num = origin.line_num
249
-
250
- if RedactAnnotatedSourceContext.current(optional=True):
251
- return cls.error('content redacted', origin)
252
-
253
- if not target_line_num or target_line_num < 1:
254
- return cls.error(None, origin) # message omitted since lack of line number is obvious from pos
255
-
256
- start_line_idx = max(0, (target_line_num - 1) - context_line_count) # if near start of file
257
- target_col_num = origin.col_num
258
-
259
- try:
260
- with pathlib.Path(origin.path).open() as src:
261
- first_line = src.readline()
262
- lines = list(itertools.islice(itertools.chain((first_line,), src), start_line_idx, target_line_num))
263
- except Exception as ex:
264
- return cls.error(type(ex).__name__, origin)
265
-
266
- if is_encrypted(first_line):
267
- return cls.error('content encrypted', origin)
268
-
269
- if len(lines) != target_line_num - start_line_idx:
270
- return cls.error('file truncated', origin)
271
-
272
- annotated_source_lines = []
273
-
274
- line_label_width = len(str(target_line_num))
275
- max_src_line_len = max_annotated_line_width - line_label_width - 1
276
-
277
- usable_line_len = max_src_line_len
278
-
279
- for line_num, line in enumerate(lines, start_line_idx + 1):
280
- line = line.rstrip('\n') # universal newline default mode on `open` ensures we'll never see anything but \n
281
- line = line.replace('\t', ' ') # mixed tab/space handling is intentionally disabled since we're both format and display config agnostic
282
-
283
- if len(line) > max_src_line_len:
284
- line = line[: max_src_line_len - len(truncation_marker)] + truncation_marker
285
- usable_line_len = max_src_line_len - len(truncation_marker)
286
-
287
- annotated_source_lines.append(f'{str(line_num).rjust(line_label_width)}{" " if line else ""}{line}')
288
-
289
- if target_col_num and usable_line_len >= target_col_num >= 1:
290
- column_marker = f'column {target_col_num}'
291
-
292
- target_col_idx = target_col_num - 1
293
-
294
- if target_col_idx + 2 + len(column_marker) > max_src_line_len:
295
- column_marker = f'{" " * (target_col_idx - len(column_marker) - 1)}{column_marker} ^'
296
- else:
297
- column_marker = f'{" " * target_col_idx}^ {column_marker}'
298
-
299
- column_marker = f'{" " * line_label_width} {column_marker}'
300
-
301
- annotated_source_lines.append(column_marker)
302
- elif target_col_num is None:
303
- underline_length = len(annotated_source_lines[-1]) - line_label_width - 1
304
- annotated_source_lines.append(f'{" " * line_label_width} {"^" * underline_length}')
305
-
306
- return SourceContext(
307
- origin=origin,
308
- annotated_source_lines=annotated_source_lines,
309
- target_line=lines[-1].rstrip('\n'), # universal newline default mode on `open` ensures we'll never see anything but \n
310
- )