ansible-core 2.17.4__py3-none-any.whl → 2.18.0b1__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.

Potentially problematic release.


This version of ansible-core might be problematic. Click here for more details.

Files changed (320) hide show
  1. ansible/__main__.py +2 -17
  2. ansible/cli/__init__.py +3 -15
  3. ansible/cli/config.py +187 -24
  4. ansible/cli/console.py +1 -1
  5. ansible/cli/doc.py +38 -16
  6. ansible/cli/galaxy.py +30 -53
  7. ansible/cli/inventory.py +2 -2
  8. ansible/cli/pull.py +2 -2
  9. ansible/cli/scripts/ansible_connection_cli_stub.py +1 -10
  10. ansible/config/base.yml +127 -57
  11. ansible/config/manager.py +89 -11
  12. ansible/constants.py +32 -9
  13. ansible/errors/__init__.py +5 -0
  14. ansible/executor/interpreter_discovery.py +1 -1
  15. ansible/executor/play_iterator.py +16 -0
  16. ansible/executor/playbook_executor.py +1 -4
  17. ansible/executor/powershell/become_wrapper.ps1 +4 -5
  18. ansible/executor/powershell/bootstrap_wrapper.ps1 +2 -3
  19. ansible/executor/powershell/exec_wrapper.ps1 +1 -1
  20. ansible/executor/powershell/module_manifest.py +2 -2
  21. ansible/executor/task_executor.py +50 -39
  22. ansible/executor/task_queue_manager.py +1 -1
  23. ansible/executor/task_result.py +1 -1
  24. ansible/galaxy/api.py +3 -4
  25. ansible/galaxy/collection/__init__.py +21 -10
  26. ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
  27. ansible/galaxy/collection/galaxy_api_proxy.py +10 -16
  28. ansible/galaxy/collection/gpg.py +17 -23
  29. ansible/galaxy/data/COPYING +7 -0
  30. ansible/galaxy/data/apb/Dockerfile.j2 +1 -0
  31. ansible/galaxy/data/apb/Makefile.j2 +1 -0
  32. ansible/galaxy/data/apb/README.md +7 -3
  33. ansible/galaxy/data/apb/apb.yml.j2 +1 -0
  34. ansible/galaxy/data/apb/defaults/main.yml.j2 +1 -0
  35. ansible/galaxy/data/apb/handlers/main.yml.j2 +1 -0
  36. ansible/galaxy/data/apb/meta/main.yml.j2 +1 -0
  37. ansible/galaxy/data/apb/playbooks/deprovision.yml.j2 +1 -0
  38. ansible/galaxy/data/apb/playbooks/provision.yml.j2 +1 -0
  39. ansible/galaxy/data/apb/tasks/main.yml.j2 +1 -0
  40. ansible/galaxy/data/apb/tests/ansible.cfg +1 -0
  41. ansible/galaxy/data/apb/tests/inventory +1 -0
  42. ansible/galaxy/data/apb/tests/test.yml.j2 +1 -0
  43. ansible/galaxy/data/apb/vars/main.yml.j2 +1 -0
  44. ansible/galaxy/data/collections_galaxy_meta.yml +1 -0
  45. ansible/galaxy/data/container/defaults/main.yml.j2 +1 -0
  46. ansible/galaxy/data/container/handlers/main.yml.j2 +1 -0
  47. ansible/galaxy/data/container/meta/container.yml.j2 +1 -0
  48. ansible/galaxy/data/container/meta/main.yml.j2 +1 -0
  49. ansible/galaxy/data/container/tasks/main.yml.j2 +1 -0
  50. ansible/galaxy/data/container/tests/ansible.cfg +1 -0
  51. ansible/galaxy/data/container/tests/inventory +1 -0
  52. ansible/galaxy/data/container/tests/test.yml.j2 +1 -0
  53. ansible/galaxy/data/container/vars/main.yml.j2 +1 -0
  54. ansible/galaxy/data/default/collection/README.md.j2 +1 -0
  55. ansible/galaxy/data/default/collection/galaxy.yml.j2 +1 -0
  56. ansible/galaxy/data/default/collection/meta/runtime.yml +1 -0
  57. ansible/galaxy/data/default/collection/plugins/README.md.j2 +1 -0
  58. ansible/galaxy/data/default/role/defaults/main.yml.j2 +1 -0
  59. ansible/galaxy/data/default/role/handlers/main.yml.j2 +1 -0
  60. ansible/galaxy/data/default/role/meta/main.yml.j2 +1 -0
  61. ansible/galaxy/data/default/role/tasks/main.yml.j2 +1 -0
  62. ansible/galaxy/data/default/role/tests/inventory +1 -0
  63. ansible/galaxy/data/default/role/tests/test.yml.j2 +1 -0
  64. ansible/galaxy/data/default/role/vars/main.yml.j2 +1 -0
  65. ansible/galaxy/data/network/cliconf_plugins/example.py.j2 +1 -0
  66. ansible/galaxy/data/network/defaults/main.yml.j2 +1 -0
  67. ansible/galaxy/data/network/library/example_command.py.j2 +1 -0
  68. ansible/galaxy/data/network/library/example_config.py.j2 +1 -0
  69. ansible/galaxy/data/network/library/example_facts.py.j2 +1 -0
  70. ansible/galaxy/data/network/meta/main.yml.j2 +1 -0
  71. ansible/galaxy/data/network/module_utils/example.py.j2 +1 -0
  72. ansible/galaxy/data/network/netconf_plugins/example.py.j2 +1 -0
  73. ansible/galaxy/data/network/tasks/main.yml.j2 +1 -0
  74. ansible/galaxy/data/network/terminal_plugins/example.py.j2 +1 -0
  75. ansible/galaxy/data/network/tests/inventory +1 -0
  76. ansible/galaxy/data/network/tests/test.yml.j2 +1 -0
  77. ansible/galaxy/data/network/vars/main.yml.j2 +1 -0
  78. ansible/galaxy/dependency_resolution/providers.py +3 -3
  79. ansible/galaxy/role.py +1 -1
  80. ansible/galaxy/token.py +20 -8
  81. ansible/keyword_desc.yml +1 -1
  82. ansible/module_utils/_internal/__init__.py +0 -0
  83. ansible/module_utils/_internal/_concurrent/__init__.py +0 -0
  84. ansible/module_utils/_internal/_concurrent/_daemon_threading.py +28 -0
  85. ansible/module_utils/_internal/_concurrent/_futures.py +21 -0
  86. ansible/module_utils/ansible_release.py +2 -2
  87. ansible/module_utils/api.py +2 -2
  88. ansible/module_utils/basic.py +14 -11
  89. ansible/module_utils/common/collections.py +1 -1
  90. ansible/module_utils/common/file.py +0 -6
  91. ansible/module_utils/common/process.py +22 -9
  92. ansible/module_utils/common/text/converters.py +5 -8
  93. ansible/module_utils/common/text/formatters.py +20 -4
  94. ansible/module_utils/common/validation.py +33 -25
  95. ansible/module_utils/compat/paramiko.py +6 -1
  96. ansible/module_utils/compat/selinux.py +2 -2
  97. ansible/module_utils/connection.py +8 -24
  98. ansible/module_utils/csharp/Ansible.Become.cs +14 -25
  99. ansible/module_utils/csharp/Ansible.Process.cs +1 -1
  100. ansible/module_utils/distro/__init__.py +1 -1
  101. ansible/module_utils/distro/_distro.py +8 -4
  102. ansible/module_utils/facts/collector.py +2 -0
  103. ansible/module_utils/facts/default_collectors.py +3 -1
  104. ansible/module_utils/facts/hardware/aix.py +54 -52
  105. ansible/module_utils/facts/hardware/darwin.py +37 -34
  106. ansible/module_utils/facts/hardware/freebsd.py +55 -15
  107. ansible/module_utils/facts/hardware/hpux.py +3 -0
  108. ansible/module_utils/facts/hardware/linux.py +101 -57
  109. ansible/module_utils/facts/hardware/netbsd.py +3 -0
  110. ansible/module_utils/facts/hardware/openbsd.py +4 -1
  111. ansible/module_utils/facts/hardware/sunos.py +7 -1
  112. ansible/module_utils/facts/network/aix.py +16 -17
  113. ansible/module_utils/facts/network/fc_wwn.py +4 -1
  114. ansible/module_utils/facts/network/hpux.py +21 -4
  115. ansible/module_utils/facts/network/iscsi.py +7 -8
  116. ansible/module_utils/facts/network/linux.py +0 -2
  117. ansible/module_utils/facts/other/facter.py +9 -4
  118. ansible/module_utils/facts/other/ohai.py +5 -5
  119. ansible/module_utils/facts/packages.py +49 -7
  120. ansible/module_utils/facts/sysctl.py +33 -31
  121. ansible/module_utils/facts/system/distribution.py +1 -1
  122. ansible/module_utils/facts/system/local.py +12 -22
  123. ansible/module_utils/facts/system/service_mgr.py +3 -1
  124. ansible/module_utils/facts/system/systemd.py +47 -0
  125. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  126. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  127. ansible/module_utils/splitter.py +1 -1
  128. ansible/modules/add_host.py +1 -1
  129. ansible/modules/apt.py +43 -32
  130. ansible/modules/apt_key.py +6 -6
  131. ansible/modules/apt_repository.py +23 -14
  132. ansible/modules/assemble.py +7 -2
  133. ansible/modules/assert.py +4 -4
  134. ansible/modules/blockinfile.py +3 -6
  135. ansible/modules/command.py +1 -1
  136. ansible/modules/copy.py +4 -4
  137. ansible/modules/cron.py +13 -10
  138. ansible/modules/deb822_repository.py +16 -17
  139. ansible/modules/debconf.py +9 -9
  140. ansible/modules/debug.py +1 -1
  141. ansible/modules/dnf.py +79 -164
  142. ansible/modules/dnf5.py +48 -31
  143. ansible/modules/dpkg_selections.py +2 -2
  144. ansible/modules/expect.py +2 -2
  145. ansible/modules/fetch.py +2 -2
  146. ansible/modules/file.py +5 -3
  147. ansible/modules/find.py +40 -12
  148. ansible/modules/gather_facts.py +4 -2
  149. ansible/modules/get_url.py +29 -24
  150. ansible/modules/git.py +35 -35
  151. ansible/modules/group.py +71 -1
  152. ansible/modules/hostname.py +2 -4
  153. ansible/modules/include_vars.py +5 -5
  154. ansible/modules/iptables.py +13 -16
  155. ansible/modules/known_hosts.py +16 -13
  156. ansible/modules/lineinfile.py +1 -4
  157. ansible/modules/meta.py +6 -1
  158. ansible/modules/mount_facts.py +651 -0
  159. ansible/modules/package_facts.py +63 -80
  160. ansible/modules/pause.py +4 -3
  161. ansible/modules/pip.py +14 -14
  162. ansible/modules/replace.py +1 -4
  163. ansible/modules/rpm_key.py +31 -11
  164. ansible/modules/service.py +8 -8
  165. ansible/modules/service_facts.py +20 -5
  166. ansible/modules/set_stats.py +1 -1
  167. ansible/modules/setup.py +3 -3
  168. ansible/modules/stat.py +3 -3
  169. ansible/modules/subversion.py +1 -1
  170. ansible/modules/systemd.py +16 -10
  171. ansible/modules/systemd_service.py +16 -10
  172. ansible/modules/sysvinit.py +4 -4
  173. ansible/modules/unarchive.py +35 -22
  174. ansible/modules/uri.py +24 -18
  175. ansible/modules/user.py +145 -12
  176. ansible/modules/validate_argument_spec.py +3 -3
  177. ansible/modules/wait_for_connection.py +2 -1
  178. ansible/modules/yum_repository.py +136 -179
  179. ansible/parsing/dataloader.py +2 -2
  180. ansible/parsing/mod_args.py +11 -10
  181. ansible/parsing/vault/__init__.py +8 -3
  182. ansible/parsing/yaml/constructor.py +10 -8
  183. ansible/parsing/yaml/objects.py +1 -1
  184. ansible/playbook/base.py +12 -23
  185. ansible/playbook/helpers.py +4 -0
  186. ansible/playbook/loop_control.py +8 -0
  187. ansible/playbook/play.py +4 -22
  188. ansible/playbook/play_context.py +0 -16
  189. ansible/playbook/playbook_include.py +2 -2
  190. ansible/playbook/role/__init__.py +2 -2
  191. ansible/playbook/task.py +1 -1
  192. ansible/plugins/__init__.py +2 -0
  193. ansible/plugins/action/__init__.py +7 -9
  194. ansible/plugins/action/reboot.py +2 -2
  195. ansible/plugins/become/__init__.py +1 -1
  196. ansible/plugins/callback/__init__.py +44 -3
  197. ansible/plugins/callback/default.py +1 -1
  198. ansible/plugins/cliconf/__init__.py +1 -1
  199. ansible/plugins/connection/paramiko_ssh.py +2 -80
  200. ansible/plugins/connection/psrp.py +33 -82
  201. ansible/plugins/connection/ssh.py +0 -8
  202. ansible/plugins/connection/winrm.py +46 -1
  203. ansible/plugins/doc_fragments/connection_pipelining.py +2 -2
  204. ansible/plugins/doc_fragments/constructed.py +10 -10
  205. ansible/plugins/doc_fragments/default_callback.py +8 -8
  206. ansible/plugins/doc_fragments/files.py +5 -5
  207. ansible/plugins/doc_fragments/inventory_cache.py +2 -2
  208. ansible/plugins/doc_fragments/result_format_callback.py +6 -6
  209. ansible/plugins/doc_fragments/return_common.py +1 -1
  210. ansible/plugins/doc_fragments/shell_common.py +2 -10
  211. ansible/plugins/doc_fragments/shell_windows.py +0 -9
  212. ansible/plugins/doc_fragments/url.py +2 -2
  213. ansible/plugins/doc_fragments/url_windows.py +4 -5
  214. ansible/plugins/doc_fragments/validate.py +1 -1
  215. ansible/plugins/filter/core.py +2 -0
  216. ansible/plugins/filter/human_to_bytes.yml +9 -0
  217. ansible/plugins/filter/password_hash.yml +1 -1
  218. ansible/plugins/filter/strftime.yml +1 -1
  219. ansible/plugins/filter/to_nice_json.yml +7 -3
  220. ansible/plugins/filter/to_uuid.yml +1 -1
  221. ansible/plugins/inventory/script.py +1 -1
  222. ansible/plugins/list.py +1 -1
  223. ansible/plugins/loader.py +0 -11
  224. ansible/plugins/lookup/config.py +1 -1
  225. ansible/plugins/lookup/csvfile.py +21 -9
  226. ansible/plugins/lookup/env.py +8 -9
  227. ansible/plugins/lookup/ini.py +10 -1
  228. ansible/plugins/lookup/random_choice.py +2 -2
  229. ansible/plugins/lookup/url.py +7 -2
  230. ansible/plugins/shell/__init__.py +15 -20
  231. ansible/plugins/shell/powershell.py +9 -6
  232. ansible/plugins/strategy/__init__.py +16 -7
  233. ansible/plugins/test/core.py +23 -1
  234. ansible/plugins/test/issubset.yml +1 -1
  235. ansible/plugins/test/subset.yml +1 -1
  236. ansible/plugins/test/timedout.yml +20 -0
  237. ansible/plugins/test/vault_encrypted.yml +6 -6
  238. ansible/plugins/test/vaulted_file.yml +19 -0
  239. ansible/release.py +2 -2
  240. ansible/template/__init__.py +3 -8
  241. ansible/utils/collection_loader/_collection_finder.py +23 -55
  242. ansible/utils/display.py +44 -31
  243. ansible/utils/jsonrpc.py +1 -1
  244. ansible/utils/listify.py +1 -5
  245. ansible/utils/path.py +3 -0
  246. ansible/utils/vars.py +18 -27
  247. ansible/vars/manager.py +7 -150
  248. ansible/vars/plugins.py +1 -1
  249. ansible_core-2.18.0b1.dist-info/Apache-License.txt +202 -0
  250. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/METADATA +36 -23
  251. ansible_core-2.18.0b1.dist-info/MIT-license.txt +14 -0
  252. ansible_core-2.18.0b1.dist-info/PSF-license.txt +48 -0
  253. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/RECORD +311 -306
  254. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/WHEEL +1 -1
  255. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/entry_points.txt +1 -1
  256. ansible_core-2.18.0b1.dist-info/simplified_bsd.txt +8 -0
  257. ansible_test/_data/completion/docker.txt +7 -7
  258. ansible_test/_data/completion/remote.txt +5 -4
  259. ansible_test/_data/completion/windows.txt +4 -4
  260. ansible_test/_data/requirements/ansible-test.txt +1 -2
  261. ansible_test/_data/requirements/constraints.txt +1 -2
  262. ansible_test/_data/requirements/sanity.ansible-doc.txt +3 -3
  263. ansible_test/_data/requirements/sanity.changelog.in +1 -1
  264. ansible_test/_data/requirements/sanity.changelog.txt +4 -4
  265. ansible_test/_data/requirements/sanity.import.plugin.txt +2 -2
  266. ansible_test/_data/requirements/sanity.import.txt +1 -1
  267. ansible_test/_data/requirements/sanity.integration-aliases.txt +1 -1
  268. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  269. ansible_test/_data/requirements/sanity.pylint.txt +5 -7
  270. ansible_test/_data/requirements/sanity.runtime-metadata.txt +2 -2
  271. ansible_test/_data/requirements/sanity.validate-modules.txt +3 -3
  272. ansible_test/_data/requirements/sanity.yamllint.in +1 -0
  273. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  274. ansible_test/_internal/ansible_util.py +8 -35
  275. ansible_test/_internal/ci/azp.py +1 -1
  276. ansible_test/_internal/classification/__init__.py +0 -2
  277. ansible_test/_internal/cli/parsers/key_value_parsers.py +3 -0
  278. ansible_test/_internal/commands/integration/cloud/hcloud.py +1 -1
  279. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  280. ansible_test/_internal/commands/integration/cloud/nios.py +1 -1
  281. ansible_test/_internal/commands/sanity/__init__.py +96 -19
  282. ansible_test/_internal/commands/sanity/pylint.py +20 -24
  283. ansible_test/_internal/completion.py +2 -0
  284. ansible_test/_internal/constants.py +0 -1
  285. ansible_test/_internal/coverage_util.py +1 -2
  286. ansible_test/_internal/docker_util.py +1 -1
  287. ansible_test/_internal/host_configs.py +10 -0
  288. ansible_test/_internal/host_profiles.py +9 -13
  289. ansible_test/_internal/pypi_proxy.py +1 -1
  290. ansible_test/_internal/python_requirements.py +5 -14
  291. ansible_test/_internal/timeout.py +1 -1
  292. ansible_test/_internal/util.py +40 -0
  293. ansible_test/_internal/util_common.py +5 -1
  294. ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.json +3 -1
  295. ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.py +6 -3
  296. ansible_test/_util/controller/sanity/code-smell/empty-init.json +0 -2
  297. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  298. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  299. ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +1 -19
  300. ansible_test/_util/controller/sanity/shellcheck/exclude.txt +1 -0
  301. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +67 -2
  302. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +27 -5
  303. ansible_test/_util/target/cli/ansible_test_cli_stub.py +0 -0
  304. ansible_test/_util/target/common/constants.py +2 -2
  305. ansible_test/_util/target/injector/python.py +5 -0
  306. ansible_test/_util/target/pytest/plugins/ansible_pytest_coverage.py +6 -0
  307. ansible_test/_util/target/sanity/import/importer.py +1 -1
  308. ansible_test/_util/target/setup/bootstrap.sh +6 -17
  309. ansible_test/_util/target/setup/requirements.py +14 -20
  310. ansible_test/config/config.yml +1 -1
  311. ansible_core-2.17.4.data/scripts/ansible-test +0 -44
  312. ansible_test/_data/requirements/sanity.mypy.in +0 -10
  313. ansible_test/_data/requirements/sanity.mypy.txt +0 -18
  314. ansible_test/_internal/commands/sanity/mypy.py +0 -274
  315. ansible_test/_util/controller/sanity/mypy/ansible-core.ini +0 -116
  316. ansible_test/_util/controller/sanity/mypy/ansible-test.ini +0 -27
  317. ansible_test/_util/controller/sanity/mypy/modules.ini +0 -92
  318. ansible_test/_util/controller/sanity/mypy/packaging.ini +0 -20
  319. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/COPYING +0 -0
  320. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/top_level.txt +0 -0
@@ -43,7 +43,6 @@ from ...util import (
43
43
 
44
44
  from ...util_common import (
45
45
  run_command,
46
- process_scoped_temporary_file,
47
46
  )
48
47
 
49
48
  from ...ansible_util import (
@@ -87,7 +86,7 @@ class PylintTest(SanitySingleVersion):
87
86
  return [target for target in targets if os.path.splitext(target.path)[1] == '.py' or is_subdir(target.path, 'bin')]
88
87
 
89
88
  def test(self, args: SanityConfig, targets: SanityTargets, python: PythonConfig) -> TestResult:
90
- min_python_version_db_path = self.create_min_python_db(args, targets.targets)
89
+ target_paths = set(target.path for target in self.filter_remote_targets(list(targets.targets)))
91
90
 
92
91
  plugin_dir = os.path.join(SANITY_ROOT, 'pylint', 'plugins')
93
92
  plugin_names = sorted(p[0] for p in [
@@ -115,7 +114,13 @@ class PylintTest(SanitySingleVersion):
115
114
  def add_context(available_paths: set[str], context_name: str, context_filter: c.Callable[[str], bool]) -> None:
116
115
  """Add the specified context to the context list, consuming available paths that match the given context filter."""
117
116
  filtered_paths = set(p for p in available_paths if context_filter(p))
118
- contexts.append((context_name, sorted(filtered_paths)))
117
+
118
+ if selected_paths := sorted(path for path in filtered_paths if path in target_paths):
119
+ contexts.append((context_name, True, selected_paths))
120
+
121
+ if selected_paths := sorted(path for path in filtered_paths if path not in target_paths):
122
+ contexts.append((context_name, False, selected_paths))
123
+
119
124
  available_paths -= filtered_paths
120
125
 
121
126
  def filter_path(path_filter: str = None) -> c.Callable[[str], bool]:
@@ -166,12 +171,12 @@ class PylintTest(SanitySingleVersion):
166
171
 
167
172
  test_start = datetime.datetime.now(tz=datetime.timezone.utc)
168
173
 
169
- for context, context_paths in sorted(contexts):
174
+ for context, is_target, context_paths in sorted(contexts):
170
175
  if not context_paths:
171
176
  continue
172
177
 
173
178
  context_start = datetime.datetime.now(tz=datetime.timezone.utc)
174
- messages += self.pylint(args, context, context_paths, plugin_dir, plugin_names, python, collection_detail, min_python_version_db_path)
179
+ messages += self.pylint(args, context, is_target, context_paths, plugin_dir, plugin_names, python, collection_detail)
175
180
  context_end = datetime.datetime.now(tz=datetime.timezone.utc)
176
181
 
177
182
  context_times.append('%s: %d (%s)' % (context, len(context_paths), context_end - context_start))
@@ -202,32 +207,16 @@ class PylintTest(SanitySingleVersion):
202
207
 
203
208
  return SanitySuccess(self.name)
204
209
 
205
- def create_min_python_db(self, args: SanityConfig, targets: t.Iterable[TestTarget]) -> str:
206
- """Create a database of target file paths and their minimum required Python version, returning the path to the database."""
207
- target_paths = set(target.path for target in self.filter_remote_targets(list(targets)))
208
- controller_min_version = CONTROLLER_PYTHON_VERSIONS[0]
209
- target_min_version = REMOTE_ONLY_PYTHON_VERSIONS[0]
210
- min_python_versions = {
211
- os.path.abspath(target.path): target_min_version if target.path in target_paths else controller_min_version for target in targets
212
- }
213
-
214
- min_python_version_db_path = process_scoped_temporary_file(args)
215
-
216
- with open(min_python_version_db_path, 'w') as database_file:
217
- json.dump(min_python_versions, database_file)
218
-
219
- return min_python_version_db_path
220
-
221
210
  @staticmethod
222
211
  def pylint(
223
212
  args: SanityConfig,
224
213
  context: str,
214
+ is_target: bool,
225
215
  paths: list[str],
226
216
  plugin_dir: str,
227
217
  plugin_names: list[str],
228
218
  python: PythonConfig,
229
219
  collection_detail: CollectionDetail,
230
- min_python_version_db_path: str,
231
220
  ) -> list[dict[str, str]]:
232
221
  """Run pylint using the config specified by the context on the specified paths."""
233
222
  rcfile = os.path.join(SANITY_ROOT, 'pylint', 'config', context.split('/')[0] + '.cfg')
@@ -249,6 +238,13 @@ class PylintTest(SanitySingleVersion):
249
238
  disable_plugins = set(i.strip() for i in config.get('disable-plugins', '').split(',') if i)
250
239
  load_plugins = set(plugin_names + ['pylint.extensions.mccabe']) - disable_plugins
251
240
 
241
+ if is_target:
242
+ context_label = 'target'
243
+ min_python_version = REMOTE_ONLY_PYTHON_VERSIONS[0]
244
+ else:
245
+ context_label = 'controller'
246
+ min_python_version = CONTROLLER_PYTHON_VERSIONS[0]
247
+
252
248
  cmd = [
253
249
  python.path,
254
250
  '-m', 'pylint',
@@ -259,7 +255,7 @@ class PylintTest(SanitySingleVersion):
259
255
  '--rcfile', rcfile,
260
256
  '--output-format', 'json',
261
257
  '--load-plugins', ','.join(sorted(load_plugins)),
262
- '--min-python-version-db', min_python_version_db_path,
258
+ '--py-version', min_python_version,
263
259
  ] + paths # fmt: skip
264
260
 
265
261
  if data_context().content.collection:
@@ -286,7 +282,7 @@ class PylintTest(SanitySingleVersion):
286
282
  env.update(PYLINTHOME=pylint_home)
287
283
 
288
284
  if paths:
289
- display.info('Checking %d file(s) in context "%s" with config: %s' % (len(paths), context, rcfile), verbosity=1)
285
+ display.info(f'Checking {len(paths)} file(s) in context {context!r} ({context_label}) with config: {rcfile}', verbosity=1)
290
286
 
291
287
  try:
292
288
  stdout, stderr = run_command(args, cmd, env=env, capture=True)
@@ -246,6 +246,8 @@ class PosixRemoteCompletionConfig(RemoteCompletionConfig, PythonCompletionConfig
246
246
  class WindowsRemoteCompletionConfig(RemoteCompletionConfig):
247
247
  """Configuration for remote Windows platforms."""
248
248
 
249
+ connection: str = ''
250
+
249
251
 
250
252
  TCompletionConfig = t.TypeVar('TCompletionConfig', bound=CompletionConfig)
251
253
 
@@ -37,7 +37,6 @@ SECCOMP_CHOICES = [
37
37
  ANSIBLE_BIN_SYMLINK_MAP = {
38
38
  'ansible': '../lib/ansible/cli/adhoc.py',
39
39
  'ansible-config': '../lib/ansible/cli/config.py',
40
- 'ansible-connection': '../lib/ansible/cli/scripts/ansible_connection_cli_stub.py',
41
40
  'ansible-console': '../lib/ansible/cli/console.py',
42
41
  'ansible-doc': '../lib/ansible/cli/doc.py',
43
42
  'ansible-galaxy': '../lib/ansible/cli/galaxy.py',
@@ -69,8 +69,7 @@ class CoverageVersion:
69
69
 
70
70
  COVERAGE_VERSIONS = (
71
71
  # IMPORTANT: Keep this in sync with the ansible-test.txt requirements file.
72
- CoverageVersion('7.3.2', 7, (3, 8), (3, 12)),
73
- CoverageVersion('6.5.0', 7, (3, 7), (3, 7)),
72
+ CoverageVersion('7.6.1', 7, (3, 8), (3, 13)),
74
73
  )
75
74
  """
76
75
  This tuple specifies the coverage version to use for Python version ranges.
@@ -47,7 +47,7 @@ DOCKER_COMMANDS = [
47
47
  'podman',
48
48
  ]
49
49
 
50
- UTILITY_IMAGE = 'quay.io/ansible/ansible-test-utility-container:2.0.0'
50
+ UTILITY_IMAGE = 'quay.io/ansible/ansible-test-utility-container:3.1.0'
51
51
 
52
52
  # Max number of open files in a docker container.
53
53
  # Passed with --ulimit option to the docker run command.
@@ -399,10 +399,20 @@ class WindowsConfig(HostConfig, metaclass=abc.ABCMeta):
399
399
  class WindowsRemoteConfig(RemoteConfig, WindowsConfig):
400
400
  """Configuration for a remote Windows host."""
401
401
 
402
+ connection: t.Optional[str] = None
403
+
402
404
  def get_defaults(self, context: HostContext) -> WindowsRemoteCompletionConfig:
403
405
  """Return the default settings."""
404
406
  return filter_completion(windows_completion()).get(self.name) or windows_completion().get(self.platform)
405
407
 
408
+ def apply_defaults(self, context: HostContext, defaults: CompletionConfig) -> None:
409
+ """Apply default settings."""
410
+ assert isinstance(defaults, WindowsRemoteCompletionConfig)
411
+
412
+ super().apply_defaults(context, defaults)
413
+
414
+ self.connection = self.connection or defaults.connection
415
+
406
416
 
407
417
  @dataclasses.dataclass
408
418
  class WindowsInventoryConfig(InventoryConfig, WindowsConfig):
@@ -56,6 +56,7 @@ from .util import (
56
56
  InternalError,
57
57
  HostConnectionError,
58
58
  ANSIBLE_TEST_TARGET_ROOT,
59
+ WINDOWS_CONNECTION_VARIABLES,
59
60
  )
60
61
 
61
62
  from .util_common import (
@@ -1367,23 +1368,18 @@ class WindowsRemoteProfile(RemoteProfile[WindowsRemoteConfig]):
1367
1368
  connection = core_ci.connection
1368
1369
 
1369
1370
  variables: dict[str, t.Optional[t.Union[str, int]]] = dict(
1370
- ansible_connection='winrm',
1371
- ansible_pipelining='yes',
1372
- ansible_winrm_server_cert_validation='ignore',
1373
1371
  ansible_host=connection.hostname,
1374
- ansible_port=connection.port,
1372
+ # ansible_port is intentionally not set using connection.port -- connection-specific variables can set this instead
1375
1373
  ansible_user=connection.username,
1376
- ansible_password=connection.password,
1377
- ansible_ssh_private_key_file=core_ci.ssh_key.key,
1374
+ ansible_ssh_private_key_file=core_ci.ssh_key.key, # required for scenarios which change the connection plugin to SSH
1375
+ ansible_test_connection_password=connection.password, # required for scenarios which change the connection plugin to require a password
1378
1376
  )
1379
1377
 
1380
- # HACK: force 2016 to use NTLM + HTTP message encryption
1381
- if self.config.version == '2016':
1382
- variables.update(
1383
- ansible_winrm_transport='ntlm',
1384
- ansible_winrm_scheme='http',
1385
- ansible_port='5985',
1386
- )
1378
+ variables.update(ansible_connection=self.config.connection.split('+')[0])
1379
+ variables.update(WINDOWS_CONNECTION_VARIABLES[self.config.connection])
1380
+
1381
+ if variables.pop('use_password'):
1382
+ variables.update(ansible_password=connection.password)
1387
1383
 
1388
1384
  return variables
1389
1385
 
@@ -69,7 +69,7 @@ def run_pypi_proxy(args: EnvironmentConfig, targets_use_pypi: bool) -> None:
69
69
  display.warning('Unable to use the PyPI proxy because Docker is not available. Installation of packages using `pip` may fail.')
70
70
  return
71
71
 
72
- image = 'quay.io/ansible/pypi-test-container:3.1.0'
72
+ image = 'quay.io/ansible/pypi-test-container:3.2.0'
73
73
  port = 3141
74
74
 
75
75
  run_support_container(
@@ -112,6 +112,8 @@ class PipBootstrap(PipCommand):
112
112
 
113
113
  pip_version: str
114
114
  packages: list[str]
115
+ setuptools: bool
116
+ wheel: bool
115
117
 
116
118
 
117
119
  # Entry Points
@@ -177,6 +179,8 @@ def collect_bootstrap(python: PythonConfig) -> list[PipCommand]:
177
179
  bootstrap = PipBootstrap(
178
180
  pip_version=pip_version,
179
181
  packages=packages,
182
+ setuptools=False,
183
+ wheel=False,
180
184
  )
181
185
 
182
186
  return [bootstrap]
@@ -218,17 +222,6 @@ def collect_requirements(
218
222
  # removing them reduces the size of environments cached in containers
219
223
  uninstall_packages = list(get_venv_packages(python))
220
224
 
221
- if not minimize:
222
- # installed packages may have run-time dependencies on setuptools
223
- uninstall_packages.remove('setuptools')
224
-
225
- # hack to allow the package-data sanity test to keep wheel in the venv
226
- install_commands = [command for command in commands if isinstance(command, PipInstall)]
227
- install_wheel = any(install.has_package('wheel') for install in install_commands)
228
-
229
- if install_wheel:
230
- uninstall_packages.remove('wheel')
231
-
232
225
  commands.extend(collect_uninstall(packages=uninstall_packages))
233
226
 
234
227
  return commands
@@ -412,9 +405,7 @@ def get_venv_packages(python: PythonConfig) -> dict[str, str]:
412
405
  # See: https://github.com/ansible/base-test-container/blob/main/files/installer.py
413
406
 
414
407
  default_packages = dict(
415
- pip='23.1.2',
416
- setuptools='67.7.2',
417
- wheel='0.37.1',
408
+ pip='24.2',
418
409
  )
419
410
 
420
411
  override_packages: dict[str, dict[str, str]] = {
@@ -118,7 +118,7 @@ def configure_test_timeout(args: TestConfig) -> None:
118
118
 
119
119
  raise TimeoutExpiredError(f'Tests aborted after exceeding the {timeout.duration} minute time limit.')
120
120
 
121
- def timeout_waiter(timeout_seconds: int) -> None:
121
+ def timeout_waiter(timeout_seconds: float) -> None:
122
122
  """Background thread which will kill the current process if the timeout elapses."""
123
123
  time.sleep(timeout_seconds)
124
124
  os.kill(os.getpid(), signal.SIGUSR1)
@@ -134,6 +134,46 @@ class Architecture:
134
134
  REMOTE_ARCHITECTURES = list(value for key, value in Architecture.__dict__.items() if not key.startswith('__'))
135
135
 
136
136
 
137
+ WINDOWS_CONNECTION_VARIABLES: dict[str, t.Any] = {
138
+ 'psrp+http': dict(
139
+ ansible_port=5985,
140
+ ansible_psrp_protocol='http',
141
+ use_password=True,
142
+ ),
143
+ 'psrp+https': dict(
144
+ ansible_port=5986,
145
+ ansible_psrp_protocol='https',
146
+ ansible_psrp_cert_validation='ignore',
147
+ use_password=True,
148
+ ),
149
+ 'ssh+key': dict(
150
+ ansible_port=22,
151
+ ansible_shell_type='powershell',
152
+ use_password=False,
153
+ ),
154
+ 'ssh+password': dict(
155
+ ansible_port=22,
156
+ ansible_shell_type='powershell',
157
+ use_password=True,
158
+ ),
159
+ 'winrm+http': dict(
160
+ ansible_port=5985,
161
+ ansible_winrm_scheme='http',
162
+ ansible_winrm_transport='ntlm',
163
+ use_password=True,
164
+ ),
165
+ 'winrm+https': dict(
166
+ ansible_port=5986,
167
+ ansible_winrm_scheme='https',
168
+ ansible_winrm_server_cert_validation='ignore',
169
+ use_password=True,
170
+ ),
171
+ }
172
+ """Dictionary of Windows connection types and variables required to use them."""
173
+
174
+ WINDOWS_CONNECTIONS = list(WINDOWS_CONNECTION_VARIABLES)
175
+
176
+
137
177
  def is_valid_identifier(value: str) -> bool:
138
178
  """Return True if the given value is a valid non-keyword Python identifier, otherwise return False."""
139
179
  return value.isidentifier() and not keyword.iskeyword(value)
@@ -269,7 +269,10 @@ def named_temporary_file(args: CommonConfig, prefix: str, suffix: str, directory
269
269
  tempfile_fd.write(to_bytes(content))
270
270
  tempfile_fd.flush()
271
271
 
272
- yield tempfile_fd.name
272
+ try:
273
+ yield tempfile_fd.name
274
+ finally:
275
+ pass
273
276
 
274
277
 
275
278
  def write_json_test_results(
@@ -300,6 +303,7 @@ def get_injector_path() -> str:
300
303
  injector_names = sorted(list(ANSIBLE_BIN_SYMLINK_MAP) + [
301
304
  'importer.py',
302
305
  'pytest',
306
+ 'ansible_connection_cli_stub.py',
303
307
  ])
304
308
 
305
309
  scripts = (
@@ -7,7 +7,9 @@
7
7
  "plugins/action/"
8
8
  ],
9
9
  "extensions": [
10
- ".py"
10
+ ".py",
11
+ ".yml",
12
+ ".yaml"
11
13
  ],
12
14
  "output": "path-message"
13
15
  }
@@ -28,13 +28,13 @@ def main():
28
28
  module_names.add(full_name)
29
29
 
30
30
  for path in paths:
31
- full_name = get_full_name(path, action_prefixes)
31
+ full_name = get_full_name(path, action_prefixes, extensions=('.py',))
32
32
 
33
33
  if full_name and full_name not in module_names:
34
34
  print('%s: action plugin has no matching module to provide documentation' % path)
35
35
 
36
36
 
37
- def get_full_name(path, prefixes):
37
+ def get_full_name(path: str, prefixes: dict[str, bool], extensions: tuple[str] | None = None) -> str | None:
38
38
  """Return the full name of the plugin at the given path by matching against the given path prefixes, or None if no match is found."""
39
39
  for prefix, flat in prefixes.items():
40
40
  if path.startswith(prefix):
@@ -45,13 +45,16 @@ def get_full_name(path, prefixes):
45
45
  else:
46
46
  full_name = relative_path
47
47
 
48
- full_name = os.path.splitext(full_name)[0]
48
+ full_name, file_ext = os.path.splitext(full_name)
49
49
 
50
50
  name = os.path.basename(full_name)
51
51
 
52
52
  if name == '__init__':
53
53
  return None
54
54
 
55
+ if extensions and file_ext not in extensions:
56
+ return None
57
+
55
58
  if name.startswith('_'):
56
59
  name = name[1:]
57
60
 
@@ -1,9 +1,7 @@
1
1
  {
2
2
  "prefixes": [
3
3
  "lib/ansible/modules/",
4
- "lib/ansible/module_utils/",
5
4
  "plugins/modules/",
6
- "plugins/module_utils/",
7
5
  "test/units/",
8
6
  "tests/unit/"
9
7
  ],
@@ -69,6 +69,7 @@ disable=
69
69
  pointless-statement,
70
70
  pointless-string-statement,
71
71
  possibly-unused-variable,
72
+ possibly-used-before-assignment,
72
73
  protected-access,
73
74
  raise-missing-from, # Python 2.x does not support raise from
74
75
  redefined-argument-from-local,
@@ -63,6 +63,7 @@ disable=
63
63
  not-an-iterable,
64
64
  not-callable,
65
65
  possibly-unused-variable,
66
+ possibly-used-before-assignment,
66
67
  protected-access,
67
68
  raise-missing-from, # Python 2.x does not support raise from
68
69
  redefined-argument-from-local,
@@ -5,8 +5,6 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  import datetime
8
- import functools
9
- import json
10
8
  import re
11
9
  import shlex
12
10
  import typing as t
@@ -326,15 +324,6 @@ class AnsibleDeprecatedCommentChecker(BaseTokenChecker):
326
324
  {'minversion': (2, 6)}),
327
325
  }
328
326
 
329
- options = (
330
- ('min-python-version-db', {
331
- 'default': None,
332
- 'type': 'string',
333
- 'metavar': '<path>',
334
- 'help': 'The path to the DB mapping paths to minimum Python versions.',
335
- }),
336
- )
337
-
338
327
  def process_tokens(self, tokens: list[TokenInfo]) -> None:
339
328
  for token in tokens:
340
329
  if token.type == COMMENT:
@@ -365,15 +354,8 @@ class AnsibleDeprecatedCommentChecker(BaseTokenChecker):
365
354
  )
366
355
  return data
367
356
 
368
- @functools.cached_property
369
- def _min_python_version_db(self) -> dict[str, str]:
370
- """A dictionary of absolute file paths and their minimum required Python version."""
371
- with open(self.linter.config.min_python_version_db) as db_file:
372
- return json.load(db_file)
373
-
374
357
  def _process_python_version(self, token: TokenInfo, data: dict[str, str]) -> None:
375
- current_file = self.linter.current_file
376
- check_version = self._min_python_version_db[current_file]
358
+ check_version = '.'.join(map(str, self.linter.config.py_version))
377
359
 
378
360
  try:
379
361
  if LooseVersion(data['python_version']) < LooseVersion(check_version):
@@ -1,2 +1,3 @@
1
1
  SC1090
2
2
  SC1091
3
+ SC2028
@@ -838,6 +838,46 @@ class ModuleValidator(Validator):
838
838
  msg='%s: %s' % (combined_path, error_message)
839
839
  )
840
840
 
841
+ def _validate_option_docs(self, options, context=None):
842
+ if not isinstance(options, dict):
843
+ return
844
+ if context is None:
845
+ context = []
846
+
847
+ normalized_option_alias_names = dict()
848
+
849
+ def add_option_alias_name(name, option_name):
850
+ normalized_name = str(name).lower()
851
+ normalized_option_alias_names.setdefault(normalized_name, {}).setdefault(option_name, set()).add(name)
852
+
853
+ for option, data in options.items():
854
+ if 'suboptions' in data:
855
+ self._validate_option_docs(data.get('suboptions'), context + [option])
856
+ add_option_alias_name(option, option)
857
+ if 'aliases' in data and isinstance(data['aliases'], list):
858
+ for alias in data['aliases']:
859
+ add_option_alias_name(alias, option)
860
+
861
+ for normalized_name, options in normalized_option_alias_names.items():
862
+ if len(options) < 2:
863
+ continue
864
+
865
+ what = []
866
+ for option_name, names in sorted(options.items()):
867
+ if option_name in names:
868
+ what.append("option '%s'" % option_name)
869
+ else:
870
+ what.append("alias '%s' of option '%s'" % (sorted(names)[0], option_name))
871
+ msg = "Multiple options/aliases"
872
+ if context:
873
+ msg += " found in %s" % " -> ".join(context)
874
+ msg += " are equal up to casing: %s" % ", ".join(what)
875
+ self.reporter.error(
876
+ path=self.object_path,
877
+ code='option-equal-up-to-casing',
878
+ msg=msg,
879
+ )
880
+
841
881
  def _validate_docs(self):
842
882
  doc = None
843
883
  # We have three ways of marking deprecated/removed files. Have to check each one
@@ -1015,6 +1055,9 @@ class ModuleValidator(Validator):
1015
1055
  'invalid-documentation',
1016
1056
  )
1017
1057
 
1058
+ if doc:
1059
+ self._validate_option_docs(doc.get('options'))
1060
+
1018
1061
  self._validate_all_semantic_markup(doc, returns)
1019
1062
 
1020
1063
  if not self.collection:
@@ -1235,7 +1278,7 @@ class ModuleValidator(Validator):
1235
1278
  self._validate_semantic_markup(entry.get(key))
1236
1279
 
1237
1280
  if isinstance(docs.get('deprecated'), dict):
1238
- for key in ('why', 'alternative'):
1281
+ for key in ('why', 'alternative', 'alternatives'):
1239
1282
  self._validate_semantic_markup(docs.get('deprecated').get(key))
1240
1283
 
1241
1284
  self._validate_semantic_markup_options(docs.get('options'))
@@ -1876,8 +1919,10 @@ class ModuleValidator(Validator):
1876
1919
  if len(doc_options_args) == 0:
1877
1920
  # Undocumented arguments will be handled later (search for undocumented-parameter)
1878
1921
  doc_options_arg = {}
1922
+ doc_option_name = None
1879
1923
  else:
1880
- doc_options_arg = doc_options[doc_options_args[0]]
1924
+ doc_option_name = doc_options_args[0]
1925
+ doc_options_arg = doc_options[doc_option_name]
1881
1926
  if len(doc_options_args) > 1:
1882
1927
  msg = "Argument '%s' in argument_spec" % arg
1883
1928
  if context:
@@ -1892,6 +1937,26 @@ class ModuleValidator(Validator):
1892
1937
  msg=msg
1893
1938
  )
1894
1939
 
1940
+ all_aliases = set(aliases + [arg])
1941
+ all_docs_aliases = set(
1942
+ ([doc_option_name] if doc_option_name is not None else [])
1943
+ +
1944
+ (doc_options_arg['aliases'] if isinstance(doc_options_arg.get('aliases'), list) else [])
1945
+ )
1946
+ if all_docs_aliases and all_aliases != all_docs_aliases:
1947
+ msg = "Argument '%s' in argument_spec" % arg
1948
+ if context:
1949
+ msg += " found in %s" % " -> ".join(context)
1950
+ msg += " has names %s, but its documentation has names %s" % (
1951
+ ", ".join([("'%s'" % alias) for alias in sorted(all_aliases)]),
1952
+ ", ".join([("'%s'" % alias) for alias in sorted(all_docs_aliases)])
1953
+ )
1954
+ self.reporter.error(
1955
+ path=self.object_path,
1956
+ code='parameter-documented-aliases-differ',
1957
+ msg=msg
1958
+ )
1959
+
1895
1960
  try:
1896
1961
  doc_default = None
1897
1962
  if 'default' in doc_options_arg and doc_options_arg['default'] is not None:
@@ -84,6 +84,22 @@ def date(error_code=None):
84
84
  return Any(isodate, error_code=error_code)
85
85
 
86
86
 
87
+ def require_only_one(keys):
88
+ def f(obj):
89
+ found = None
90
+ for k in obj.keys():
91
+ if k in keys:
92
+ if found is None:
93
+ found = k
94
+ else:
95
+ raise Invalid('Found conflicting keys, must contain only one of {}'.format(keys))
96
+ if found is None:
97
+ raise Invalid('Must contain one of {}'.format(keys))
98
+
99
+ return obj
100
+ return f
101
+
102
+
87
103
  # Roles can also be referenced by semantic markup
88
104
  _VALID_PLUGIN_TYPES = set(DOCUMENTABLE_PLUGINS + ('role', ))
89
105
 
@@ -568,7 +584,9 @@ def list_dict_option_schema(for_collection, plugin_type):
568
584
  {
569
585
  # This definition makes sure everything has the correct types/values
570
586
  'why': doc_string,
571
- 'alternatives': doc_string,
587
+ # TODO: phase out either plural or singular, 'alt' is exclusive group
588
+ Exclusive('alternative', 'alt'): doc_string,
589
+ Exclusive('alternatives', 'alt'): doc_string,
572
590
  # vod stands for 'version or date'; this is the name of the exclusive group
573
591
  Exclusive('removed_at_date', 'vod'): date(),
574
592
  Exclusive('version', 'vod'): version(for_collection),
@@ -577,7 +595,7 @@ def list_dict_option_schema(for_collection, plugin_type):
577
595
  {
578
596
  # This definition makes sure that everything we require is there
579
597
  Required('why'): Any(*string_types),
580
- 'alternatives': Any(*string_types),
598
+ Required(Any('alternatives', 'alternative')): Any(*string_types),
581
599
  Required(Any('removed_at_date', 'version')): Any(*string_types),
582
600
  Required('collection_name'): Any(*string_types),
583
601
  },
@@ -761,13 +779,16 @@ def return_schema(for_collection, plugin_type='module'):
761
779
 
762
780
 
763
781
  def deprecation_schema(for_collection):
782
+
764
783
  main_fields = {
765
784
  Required('why'): doc_string,
766
- Required('alternative'): doc_string,
767
- Required('removed_from_collection'): collection_name,
768
- 'removed': Any(True),
785
+ 'alternative': doc_string,
786
+ 'alternatives': doc_string,
769
787
  }
770
788
 
789
+ if for_collection:
790
+ main_fields.update({Required('removed_from_collection'): collection_name, 'removed': Any(True)})
791
+
771
792
  date_schema = {
772
793
  Required('removed_at_date'): date(),
773
794
  }
@@ -791,6 +812,7 @@ def deprecation_schema(for_collection):
791
812
  if for_collection:
792
813
  result = All(
793
814
  result,
815
+ require_only_one(['alternative', 'alternatives']),
794
816
  partial(check_removal_version,
795
817
  version_field='removed_in',
796
818
  collection_name_field='removed_from_collection',
File without changes
@@ -5,13 +5,13 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  REMOTE_ONLY_PYTHON_VERSIONS = (
8
- '3.7',
9
8
  '3.8',
10
9
  '3.9',
10
+ '3.10',
11
11
  )
12
12
 
13
13
  CONTROLLER_PYTHON_VERSIONS = (
14
- '3.10',
15
14
  '3.11',
16
15
  '3.12',
16
+ '3.13',
17
17
  )