ansible-core 2.15.0b1__py3-none-any.whl → 2.15.0b3__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 (28) hide show
  1. ansible/cli/arguments/option_helpers.py +0 -2
  2. ansible/cli/playbook.py +2 -0
  3. ansible/executor/play_iterator.py +12 -0
  4. ansible/executor/task_executor.py +12 -0
  5. ansible/galaxy/api.py +1 -2
  6. ansible/galaxy/collection/__init__.py +3 -0
  7. ansible/galaxy/dependency_resolution/dataclasses.py +11 -1
  8. ansible/galaxy/dependency_resolution/providers.py +0 -1
  9. ansible/module_utils/ansible_release.py +1 -1
  10. ansible/module_utils/basic.py +47 -41
  11. ansible/modules/dnf5.py +7 -6
  12. ansible/plugins/strategy/__init__.py +80 -84
  13. ansible/plugins/strategy/linear.py +1 -5
  14. ansible/release.py +1 -1
  15. {ansible_core-2.15.0b1.dist-info → ansible_core-2.15.0b3.dist-info}/METADATA +1 -1
  16. {ansible_core-2.15.0b1.dist-info → ansible_core-2.15.0b3.dist-info}/RECORD +28 -28
  17. ansible_test/_data/completion/docker.txt +2 -2
  18. ansible_test/_data/requirements/sanity.validate-modules.in +1 -0
  19. ansible_test/_data/requirements/sanity.validate-modules.txt +1 -0
  20. ansible_test/_internal/cli/argparsing/argcompletion.py +20 -5
  21. ansible_test/_util/controller/sanity/mypy/ansible-test.ini +3 -0
  22. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +27 -27
  23. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +56 -139
  24. {ansible_core-2.15.0b1.data → ansible_core-2.15.0b3.data}/scripts/ansible-test +0 -0
  25. {ansible_core-2.15.0b1.dist-info → ansible_core-2.15.0b3.dist-info}/COPYING +0 -0
  26. {ansible_core-2.15.0b1.dist-info → ansible_core-2.15.0b3.dist-info}/WHEEL +0 -0
  27. {ansible_core-2.15.0b1.dist-info → ansible_core-2.15.0b3.dist-info}/entry_points.txt +0 -0
  28. {ansible_core-2.15.0b1.dist-info → ansible_core-2.15.0b3.dist-info}/top_level.txt +0 -0
@@ -235,8 +235,6 @@ def add_check_options(parser):
235
235
  """Add options for commands which can run with diagnostic information of tasks"""
236
236
  parser.add_argument("-C", "--check", default=False, dest='check', action='store_true',
237
237
  help="don't make any changes; instead, try to predict some of the changes that may occur")
238
- parser.add_argument('--syntax-check', dest='syntax', action='store_true',
239
- help="perform a syntax check on the playbook, but do not execute it")
240
238
  parser.add_argument("-D", "--diff", default=C.DIFF_ALWAYS, dest='diff', action='store_true',
241
239
  help="when changing (small) files and templates, show the differences in those"
242
240
  " files; works great with --check")
ansible/cli/playbook.py CHANGED
@@ -54,6 +54,8 @@ class PlaybookCLI(CLI):
54
54
  opt_help.add_module_options(self.parser)
55
55
 
56
56
  # ansible playbook specific opts
57
+ self.parser.add_argument('--syntax-check', dest='syntax', action='store_true',
58
+ help="perform a syntax check on the playbook, but do not execute it")
57
59
  self.parser.add_argument('--list-tasks', dest='listtasks', action='store_true',
58
60
  help="list all tasks that would be executed")
59
61
  self.parser.add_argument('--list-tags', dest='listtags', action='store_true',
@@ -60,6 +60,8 @@ class HostState:
60
60
  self._blocks = blocks[:]
61
61
  self.handlers = []
62
62
 
63
+ self.handler_notifications = []
64
+
63
65
  self.cur_block = 0
64
66
  self.cur_regular_task = 0
65
67
  self.cur_rescue_task = 0
@@ -120,6 +122,7 @@ class HostState:
120
122
  def copy(self):
121
123
  new_state = HostState(self._blocks)
122
124
  new_state.handlers = self.handlers[:]
125
+ new_state.handler_notifications = self.handler_notifications[:]
123
126
  new_state.cur_block = self.cur_block
124
127
  new_state.cur_regular_task = self.cur_regular_task
125
128
  new_state.cur_rescue_task = self.cur_rescue_task
@@ -650,3 +653,12 @@ class PlayIterator:
650
653
  if not isinstance(fail_state, FailedStates):
651
654
  raise AnsibleAssertionError('Expected fail_state to be a FailedStates but was %s' % (type(fail_state)))
652
655
  self._host_states[hostname].fail_state = fail_state
656
+
657
+ def add_notification(self, hostname: str, notification: str) -> None:
658
+ # preserve order
659
+ host_state = self._host_states[hostname]
660
+ if notification not in host_state.handler_notifications:
661
+ host_state.handler_notifications.append(notification)
662
+
663
+ def clear_notification(self, hostname: str, notification: str) -> None:
664
+ self._host_states[hostname].handler_notifications.remove(notification)
@@ -137,6 +137,12 @@ class TaskExecutor:
137
137
  self._task.ignore_errors = item_ignore
138
138
  elif self._task.ignore_errors and not item_ignore:
139
139
  self._task.ignore_errors = item_ignore
140
+ if 'unreachable' in item and item['unreachable']:
141
+ item_ignore_unreachable = item.pop('_ansible_ignore_unreachable')
142
+ if not res.get('unreachable'):
143
+ self._task.ignore_unreachable = item_ignore_unreachable
144
+ elif self._task.ignore_unreachable and not item_ignore_unreachable:
145
+ self._task.ignore_unreachable = item_ignore_unreachable
140
146
 
141
147
  # ensure to accumulate these
142
148
  for array in ['warnings', 'deprecations']:
@@ -277,6 +283,7 @@ class TaskExecutor:
277
283
  u" to something else to avoid variable collisions and unexpected behavior." % (self._task, loop_var))
278
284
 
279
285
  ran_once = False
286
+ task_fields = None
280
287
  no_log = False
281
288
  items_len = len(items)
282
289
  results = []
@@ -348,6 +355,7 @@ class TaskExecutor:
348
355
 
349
356
  res['_ansible_item_result'] = True
350
357
  res['_ansible_ignore_errors'] = task_fields.get('ignore_errors')
358
+ res['_ansible_ignore_unreachable'] = task_fields.get('ignore_unreachable')
351
359
 
352
360
  # gets templated here unlike rest of loop_control fields, depends on loop_var above
353
361
  try:
@@ -392,6 +400,10 @@ class TaskExecutor:
392
400
  del task_vars[var]
393
401
 
394
402
  self._task.no_log = no_log
403
+ # NOTE: run_once cannot contain loop vars because it's templated earlier also
404
+ # This is saving the post-validated field from the last loop so the strategy can use the templated value post task execution
405
+ self._task.run_once = task_fields.get('run_once')
406
+ self._task.action = task_fields.get('action')
395
407
 
396
408
  return results
397
409
 
ansible/galaxy/api.py CHANGED
@@ -926,8 +926,7 @@ class GalaxyAPI:
926
926
  try:
927
927
  signatures = data["signatures"]
928
928
  except KeyError:
929
- # Noisy since this is used by the dep resolver, so require more verbosity than Galaxy calls
930
- display.vvvvvv(f"Server {self.api_server} has not signed {namespace}.{name}:{version}")
929
+ display.vvvv(f"Server {self.api_server} has not signed {namespace}.{name}:{version}")
931
930
  return []
932
931
  else:
933
932
  return [signature_info["signature"] for signature_info in signatures]
@@ -769,6 +769,9 @@ def install_collections(
769
769
  "Skipping signature verification."
770
770
  )
771
771
 
772
+ if concrete_coll_pin.type == 'galaxy':
773
+ concrete_coll_pin = concrete_coll_pin.with_signatures_repopulated()
774
+
772
775
  try:
773
776
  install(concrete_coll_pin, output_path, artifacts_manager)
774
777
  except AnsibleError as err:
@@ -27,7 +27,7 @@ if t.TYPE_CHECKING:
27
27
  )
28
28
 
29
29
 
30
- from ansible.errors import AnsibleError
30
+ from ansible.errors import AnsibleError, AnsibleAssertionError
31
31
  from ansible.galaxy.api import GalaxyAPI
32
32
  from ansible.galaxy.collection import HAS_PACKAGING, PkgReq
33
33
  from ansible.module_utils._text import to_bytes, to_native, to_text
@@ -584,3 +584,13 @@ class Candidate(
584
584
 
585
585
  def __init__(self, *args, **kwargs):
586
586
  super(Candidate, self).__init__()
587
+
588
+ def with_signatures_repopulated(self): # type: (Candidate) -> Candidate
589
+ """Populate a new Candidate instance with Galaxy signatures.
590
+ :raises AnsibleAssertionError: If the supplied candidate is not sourced from a Galaxy-like index.
591
+ """
592
+ if self.type != 'galaxy':
593
+ raise AnsibleAssertionError(f"Invalid collection type for {self!r}: unable to get signatures from a galaxy server.")
594
+
595
+ signatures = self.src.get_collection_signatures(self.namespace, self.name, self.ver)
596
+ return self.__class__(self.fqcn, self.ver, self.src, self.type, frozenset([*self.signatures, *signatures]))
@@ -392,7 +392,6 @@ class CollectionDependencyProviderBase(AbstractProvider):
392
392
 
393
393
  if not unsatisfied:
394
394
  if self._include_signatures:
395
- signatures = src_server.get_collection_signatures(first_req.namespace, first_req.name, version)
396
395
  for extra_source in extra_signature_sources:
397
396
  signatures.append(get_signature_from_source(extra_source))
398
397
  latest_matches.append(
@@ -19,6 +19,6 @@
19
19
  from __future__ import (absolute_import, division, print_function)
20
20
  __metaclass__ = type
21
21
 
22
- __version__ = '2.15.0b1'
22
+ __version__ = '2.15.0b3'
23
23
  __author__ = 'Ansible, Inc.'
24
24
  __codename__ = "Ten Years Gone"
@@ -2043,53 +2043,59 @@ class AnsibleModule(object):
2043
2043
  # Select PollSelector which is supported by major platforms
2044
2044
  selector = selectors.PollSelector()
2045
2045
 
2046
- selector.register(cmd.stdout, selectors.EVENT_READ)
2047
- selector.register(cmd.stderr, selectors.EVENT_READ)
2048
- if os.name == 'posix':
2049
- fcntl.fcntl(cmd.stdout.fileno(), fcntl.F_SETFL, fcntl.fcntl(cmd.stdout.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
2050
- fcntl.fcntl(cmd.stderr.fileno(), fcntl.F_SETFL, fcntl.fcntl(cmd.stderr.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
2051
-
2052
2046
  if data:
2053
2047
  if not binary_data:
2054
2048
  data += '\n'
2055
2049
  if isinstance(data, text_type):
2056
2050
  data = to_bytes(data)
2057
- cmd.stdin.write(data)
2058
- cmd.stdin.close()
2059
-
2060
- while True:
2061
- events = selector.select(1)
2062
- for key, event in events:
2063
- b_chunk = key.fileobj.read()
2064
- if b_chunk == b(''):
2065
- selector.unregister(key.fileobj)
2066
- if key.fileobj == cmd.stdout:
2067
- stdout += b_chunk
2068
- elif key.fileobj == cmd.stderr:
2069
- stderr += b_chunk
2070
- # if we're checking for prompts, do it now
2071
- if prompt_re:
2072
- if prompt_re.search(stdout) and not data:
2073
- if encoding:
2074
- stdout = to_native(stdout, encoding=encoding, errors=errors)
2075
- return (257, stdout, "A prompt was encountered while running a command, but no input data was specified")
2076
- # only break out if no pipes are left to read or
2077
- # the pipes are completely read and
2078
- # the process is terminated
2079
- if (not events or not selector.get_map()) and cmd.poll() is not None:
2080
- break
2081
- # No pipes are left to read but process is not yet terminated
2082
- # Only then it is safe to wait for the process to be finished
2083
- # NOTE: Actually cmd.poll() is always None here if no selectors are left
2084
- elif not selector.get_map() and cmd.poll() is None:
2085
- cmd.wait()
2086
- # The process is terminated. Since no pipes to read from are
2087
- # left, there is no need to call select() again.
2088
- break
2089
2051
 
2090
- cmd.stdout.close()
2091
- cmd.stderr.close()
2092
- selector.close()
2052
+ if not prompt_re:
2053
+ stdout, stderr = cmd.communicate(input=data)
2054
+ else:
2055
+ # We only need this to look for a prompt, to abort instead of hanging
2056
+ selector.register(cmd.stdout, selectors.EVENT_READ)
2057
+ selector.register(cmd.stderr, selectors.EVENT_READ)
2058
+ if os.name == 'posix':
2059
+ fcntl.fcntl(cmd.stdout.fileno(), fcntl.F_SETFL, fcntl.fcntl(cmd.stdout.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
2060
+ fcntl.fcntl(cmd.stderr.fileno(), fcntl.F_SETFL, fcntl.fcntl(cmd.stderr.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
2061
+
2062
+ if data:
2063
+ cmd.stdin.write(data)
2064
+ cmd.stdin.close()
2065
+
2066
+ while True:
2067
+ events = selector.select(1)
2068
+ for key, event in events:
2069
+ b_chunk = key.fileobj.read()
2070
+ if b_chunk == b(''):
2071
+ selector.unregister(key.fileobj)
2072
+ if key.fileobj == cmd.stdout:
2073
+ stdout += b_chunk
2074
+ elif key.fileobj == cmd.stderr:
2075
+ stderr += b_chunk
2076
+ # if we're checking for prompts, do it now
2077
+ if prompt_re:
2078
+ if prompt_re.search(stdout) and not data:
2079
+ if encoding:
2080
+ stdout = to_native(stdout, encoding=encoding, errors=errors)
2081
+ return (257, stdout, "A prompt was encountered while running a command, but no input data was specified")
2082
+ # only break out if no pipes are left to read or
2083
+ # the pipes are completely read and
2084
+ # the process is terminated
2085
+ if (not events or not selector.get_map()) and cmd.poll() is not None:
2086
+ break
2087
+ # No pipes are left to read but process is not yet terminated
2088
+ # Only then it is safe to wait for the process to be finished
2089
+ # NOTE: Actually cmd.poll() is always None here if no selectors are left
2090
+ elif not selector.get_map() and cmd.poll() is None:
2091
+ cmd.wait()
2092
+ # The process is terminated. Since no pipes to read from are
2093
+ # left, there is no need to call select() again.
2094
+ break
2095
+
2096
+ cmd.stdout.close()
2097
+ cmd.stderr.close()
2098
+ selector.close()
2093
2099
 
2094
2100
  rc = cmd.returncode
2095
2101
  except (OSError, IOError) as e:
ansible/modules/dnf5.py CHANGED
@@ -445,8 +445,8 @@ class Dnf5Module(YumDnf):
445
445
 
446
446
  # done all we can do, something is just broken (auto-install isn't useful anymore with respawn, so it was removed)
447
447
  self.module.fail_json(
448
- msg="Could not import the dnf python module using {0} ({1}). "
449
- "Please install `python3-dnf` or `python2-dnf` package or ensure you have specified the "
448
+ msg="Could not import the libdnf5 python module using {0} ({1}). "
449
+ "Please install python3-libdnf5 package or ensure you have specified the "
450
450
  "correct ansible_python_interpreter. (attempted {2})".format(
451
451
  sys.executable, sys.version.replace("\n", ""), system_interpreters
452
452
  ),
@@ -629,11 +629,12 @@ class Dnf5Module(YumDnf):
629
629
 
630
630
  if transaction.get_problems():
631
631
  failures = []
632
- for log in transaction.get_resolve_logs_as_strings():
633
- if log.startswith("No match for argument") and self.state in {"install", "present", "latest"}:
634
- failures.append("No package {} available.".format(log.rsplit(' ', 1)[-1]))
632
+ for log_event in transaction.get_resolve_logs():
633
+ if log_event.get_problem() == libdnf5.base.GoalProblem_NOT_FOUND and self.state in {"install", "present", "latest"}:
634
+ # NOTE dnf module compat
635
+ failures.append("No package {} available.".format(log_event.get_spec()))
635
636
  else:
636
- failures.append(log)
637
+ failures.append(log_event.to_string())
637
638
 
638
639
  if transaction.get_problems() & libdnf5.base.GoalProblem_SOLVER_ERROR != 0:
639
640
  msg = "Depsolve Error occurred"
@@ -27,6 +27,7 @@ import queue
27
27
  import sys
28
28
  import threading
29
29
  import time
30
+ import typing as t
30
31
 
31
32
  from collections import deque
32
33
  from multiprocessing import Lock
@@ -37,7 +38,7 @@ from ansible import constants as C
37
38
  from ansible import context
38
39
  from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleUndefinedVariable, AnsibleParserError
39
40
  from ansible.executor import action_write_locks
40
- from ansible.executor.play_iterator import IteratingStates
41
+ from ansible.executor.play_iterator import IteratingStates, PlayIterator
41
42
  from ansible.executor.process.worker import WorkerProcess
42
43
  from ansible.executor.task_result import TaskResult
43
44
  from ansible.executor.task_queue_manager import CallbackSend, DisplaySend, PromptSend
@@ -506,6 +507,57 @@ class StrategyBase:
506
507
 
507
508
  return task_result
508
509
 
510
+ def search_handlers_by_notification(self, notification: str, iterator: PlayIterator) -> t.Generator[Handler, None, None]:
511
+ templar = Templar(None)
512
+ # iterate in reversed order since last handler loaded with the same name wins
513
+ for handler in (h for b in reversed(iterator._play.handlers) for h in b.block if h.name):
514
+ if not handler.cached_name:
515
+ if templar.is_template(handler.name):
516
+ templar.available_variables = self._variable_manager.get_vars(
517
+ play=iterator._play,
518
+ task=handler,
519
+ _hosts=self._hosts_cache,
520
+ _hosts_all=self._hosts_cache_all
521
+ )
522
+ try:
523
+ handler.name = templar.template(handler.name)
524
+ except (UndefinedError, AnsibleUndefinedVariable) as e:
525
+ # We skip this handler due to the fact that it may be using
526
+ # a variable in the name that was conditionally included via
527
+ # set_fact or some other method, and we don't want to error
528
+ # out unnecessarily
529
+ if not handler.listen:
530
+ display.warning(
531
+ "Handler '%s' is unusable because it has no listen topics and "
532
+ "the name could not be templated (host-specific variables are "
533
+ "not supported in handler names). The error: %s" % (handler.name, to_text(e))
534
+ )
535
+ continue
536
+ handler.cached_name = True
537
+
538
+ # first we check with the full result of get_name(), which may
539
+ # include the role name (if the handler is from a role). If that
540
+ # is not found, we resort to the simple name field, which doesn't
541
+ # have anything extra added to it.
542
+ if notification in {
543
+ handler.name,
544
+ handler.get_name(include_role_fqcn=False),
545
+ handler.get_name(include_role_fqcn=True),
546
+ }:
547
+ yield handler
548
+ break
549
+
550
+ templar.available_variables = {}
551
+ for handler in (h for b in iterator._play.handlers for h in b.block):
552
+ if listeners := handler.listen:
553
+ if notification in handler.get_validated_value(
554
+ 'listen',
555
+ handler.fattributes.get('listen'),
556
+ listeners,
557
+ templar,
558
+ ):
559
+ yield handler
560
+
509
561
  @debug_closure
510
562
  def _process_pending_results(self, iterator, one_pass=False, max_passes=None):
511
563
  '''
@@ -516,46 +568,6 @@ class StrategyBase:
516
568
  ret_results = []
517
569
  handler_templar = Templar(self._loader)
518
570
 
519
- def search_handler_blocks_by_name(handler_name, handler_blocks):
520
- # iterate in reversed order since last handler loaded with the same name wins
521
- for handler_block in reversed(handler_blocks):
522
- for handler_task in handler_block.block:
523
- if handler_task.name:
524
- try:
525
- if not handler_task.cached_name:
526
- if handler_templar.is_template(handler_task.name):
527
- handler_templar.available_variables = self._variable_manager.get_vars(play=iterator._play,
528
- task=handler_task,
529
- _hosts=self._hosts_cache,
530
- _hosts_all=self._hosts_cache_all)
531
- handler_task.name = handler_templar.template(handler_task.name)
532
- handler_task.cached_name = True
533
-
534
- # first we check with the full result of get_name(), which may
535
- # include the role name (if the handler is from a role). If that
536
- # is not found, we resort to the simple name field, which doesn't
537
- # have anything extra added to it.
538
- candidates = (
539
- handler_task.name,
540
- handler_task.get_name(include_role_fqcn=False),
541
- handler_task.get_name(include_role_fqcn=True),
542
- )
543
-
544
- if handler_name in candidates:
545
- return handler_task
546
- except (UndefinedError, AnsibleUndefinedVariable) as e:
547
- # We skip this handler due to the fact that it may be using
548
- # a variable in the name that was conditionally included via
549
- # set_fact or some other method, and we don't want to error
550
- # out unnecessarily
551
- if not handler_task.listen:
552
- display.warning(
553
- "Handler '%s' is unusable because it has no listen topics and "
554
- "the name could not be templated (host-specific variables are "
555
- "not supported in handler names). The error: %s" % (handler_task.name, to_text(e))
556
- )
557
- continue
558
-
559
571
  cur_pass = 0
560
572
  while True:
561
573
  try:
@@ -636,49 +648,24 @@ class StrategyBase:
636
648
  result_items = [task_result._result]
637
649
 
638
650
  for result_item in result_items:
639
- if '_ansible_notify' in result_item:
640
- if task_result.is_changed():
641
- # The shared dictionary for notified handlers is a proxy, which
642
- # does not detect when sub-objects within the proxy are modified.
643
- # So, per the docs, we reassign the list so the proxy picks up and
644
- # notifies all other threads
645
- for handler_name in result_item['_ansible_notify']:
646
- found = False
647
- # Find the handler using the above helper. First we look up the
648
- # dependency chain of the current task (if it's from a role), otherwise
649
- # we just look through the list of handlers in the current play/all
650
- # roles and use the first one that matches the notify name
651
- target_handler = search_handler_blocks_by_name(handler_name, iterator._play.handlers)
652
- if target_handler is not None:
653
- found = True
654
- if target_handler.notify_host(original_host):
655
- self._tqm.send_callback('v2_playbook_on_notify', target_handler, original_host)
656
-
657
- for listening_handler_block in iterator._play.handlers:
658
- for listening_handler in listening_handler_block.block:
659
- listeners = getattr(listening_handler, 'listen', []) or []
660
- if not listeners:
661
- continue
662
-
663
- listeners = listening_handler.get_validated_value(
664
- 'listen', listening_handler.fattributes.get('listen'), listeners, handler_templar
665
- )
666
- if handler_name not in listeners:
667
- continue
668
- else:
669
- found = True
670
-
671
- if listening_handler.notify_host(original_host):
672
- self._tqm.send_callback('v2_playbook_on_notify', listening_handler, original_host)
673
-
674
- # and if none were found, then we raise an error
675
- if not found:
676
- msg = ("The requested handler '%s' was not found in either the main handlers list nor in the listening "
677
- "handlers list" % handler_name)
678
- if C.ERROR_ON_MISSING_HANDLER:
679
- raise AnsibleError(msg)
680
- else:
681
- display.warning(msg)
651
+ if '_ansible_notify' in result_item and task_result.is_changed():
652
+ # only ensure that notified handlers exist, if so save the notifications for when
653
+ # handlers are actually flushed so the last defined handlers are exexcuted,
654
+ # otherwise depending on the setting either error or warn
655
+ for notification in result_item['_ansible_notify']:
656
+ if any(self.search_handlers_by_notification(notification, iterator)):
657
+ iterator.add_notification(original_host.name, notification)
658
+ display.vv(f"Notification for handler {notification} has been saved.")
659
+ continue
660
+
661
+ msg = (
662
+ f"The requested handler '{notification}' was not found in either the main handlers"
663
+ " list nor in the listening handlers list"
664
+ )
665
+ if C.ERROR_ON_MISSING_HANDLER:
666
+ raise AnsibleError(msg)
667
+ else:
668
+ display.warning(msg)
682
669
 
683
670
  if 'add_host' in result_item:
684
671
  # this task added a new host (add_host module)
@@ -957,6 +944,15 @@ class StrategyBase:
957
944
  elif meta_action == 'flush_handlers':
958
945
  if _evaluate_conditional(target_host):
959
946
  host_state = iterator.get_state_for_host(target_host.name)
947
+ # actually notify proper handlers based on all notifications up to this point
948
+ for notification in list(host_state.handler_notifications):
949
+ for handler in self.search_handlers_by_notification(notification, iterator):
950
+ if not handler.notify_host(target_host):
951
+ # NOTE even with notifications deduplicated this can still happen in case of handlers being
952
+ # notified multiple times using different names, like role name or fqcn
953
+ self._tqm.send_callback('v2_playbook_on_notify', handler, target_host)
954
+ iterator.clear_notification(target_host.name, notification)
955
+
960
956
  if host_state.run_state == IteratingStates.HANDLERS:
961
957
  raise AnsibleError('flush_handlers cannot be used as a handler')
962
958
  if target_host.name not in self._tqm._unreachable_hosts:
@@ -35,7 +35,6 @@ from ansible import constants as C
35
35
  from ansible.errors import AnsibleError, AnsibleAssertionError, AnsibleParserError
36
36
  from ansible.executor.play_iterator import IteratingStates, FailedStates
37
37
  from ansible.module_utils._text import to_text
38
- from ansible.module_utils.parsing.convert_bool import boolean
39
38
  from ansible.playbook.handler import Handler
40
39
  from ansible.playbook.included_file import IncludedFile
41
40
  from ansible.playbook.task import Task
@@ -214,10 +213,7 @@ class StrategyModule(StrategyBase):
214
213
  skip_rest = True
215
214
  break
216
215
 
217
- if templar.is_template(task.run_once):
218
- setattr(task, 'run_once', boolean(templar.template(task.run_once), strict=True))
219
-
220
- run_once = task.run_once or action and getattr(action, 'BYPASS_HOST_LOOP', False)
216
+ run_once = templar.template(task.run_once) or action and getattr(action, 'BYPASS_HOST_LOOP', False)
221
217
 
222
218
  if (task.any_errors_fatal or run_once) and not task.ignore_errors:
223
219
  any_errors_fatal = True
ansible/release.py CHANGED
@@ -19,6 +19,6 @@
19
19
  from __future__ import (absolute_import, division, print_function)
20
20
  __metaclass__ = type
21
21
 
22
- __version__ = '2.15.0b1'
22
+ __version__ = '2.15.0b3'
23
23
  __author__ = 'Ansible, Inc.'
24
24
  __codename__ = "Ten Years Gone"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ansible-core
3
- Version: 2.15.0b1
3
+ Version: 2.15.0b3
4
4
  Summary: Radically simple IT automation
5
5
  Home-page: https://ansible.com/
6
6
  Author: Ansible, Inc.
@@ -3,7 +3,7 @@ ansible/__main__.py,sha256=IvyRvY64pT0on94qCLibxgDJ0-7_2CRoaZ5kfGOl54Q,1395
3
3
  ansible/constants.py,sha256=JLIDnuSz3_PbtXWsL4vnvVBbxlh3lSrJREd7T73atEI,8293
4
4
  ansible/context.py,sha256=OzSlaA_GgGRyyf5I209sy19_eGOX6HXn441W9w_FcvU,2018
5
5
  ansible/keyword_desc.yml,sha256=FYY0Ld1Xc3AxJ_Tefz78kRSYzIKGS8qcPtVk370J118,7367
6
- ansible/release.py,sha256=QGcWt9a89G2IlrC7FJFlSl5xOwkjeg7OmcJKUzM_Avc,920
6
+ ansible/release.py,sha256=P-fXbdy9zVITuZdHA46usN4gSEjuWpRFm8Vf8lcZFtc,920
7
7
  ansible/_vendor/__init__.py,sha256=wJRKH7kI9OzYVY9hgSchOsTNTmTnugpPLGYj9Y5akX0,2086
8
8
  ansible/cli/__init__.py,sha256=ZK8bKuMmeRqeAcePriGtJ0tMuoDur3sN-ySBmOzAF3c,28687
9
9
  ansible/cli/adhoc.py,sha256=pGW6eysaireovp4sVsUuntg-l1o7DSujuhxVhVC2zsM,8230
@@ -12,11 +12,11 @@ ansible/cli/console.py,sha256=rc-6s-Exf9b8lead40RyfugZdU1-cMoN-kA1iI8Uhs8,21941
12
12
  ansible/cli/doc.py,sha256=x7LNU10RiJJejxHxbZg0xd6cdJarxTEK5LfWmQMc3y0,64153
13
13
  ansible/cli/galaxy.py,sha256=BwKVIeErmdPIN7V77vzrGtlbO0n72_PYvh1bvVSuSLk,91107
14
14
  ansible/cli/inventory.py,sha256=6aZ9n8GrRHVPSYbrEezfIJO62pcdH8RUNIayA-iNTKs,17693
15
- ansible/cli/playbook.py,sha256=ttNHWeHUKvaF2u3ep2Q6O1IALru_RUGxJnYMjpy2LQk,10711
15
+ ansible/cli/playbook.py,sha256=2MNTSu99nKVO7b7ZeyA0PkR5ML8kuBCeCDMjd5YPh4g,10901
16
16
  ansible/cli/pull.py,sha256=TI3xfqcO-f8I60gRvVdiAEELghSq5Mlb8YPX6SdiitM,17010
17
17
  ansible/cli/vault.py,sha256=8od9BPi570xO7CqiG82G5HHa_6oFRGDxlQ5bdCZkkjQ,22645
18
18
  ansible/cli/arguments/__init__.py,sha256=CL2cOeYgVnD4r0iJTzEjjldSkJjGKPZc_t44UKSF4n8,221
19
- ansible/cli/arguments/option_helpers.py,sha256=cvnu8jwKm29SXV5_54KEBRjvVkwFqkwNjfNSjHia-MM,18279
19
+ ansible/cli/arguments/option_helpers.py,sha256=zu0VTuuUHCvw5X_nvcNe5qjCPmjJPNdHVmOUgDRsa7E,18107
20
20
  ansible/cli/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  ansible/cli/scripts/ansible_connection_cli_stub.py,sha256=bpaNmBnYGiTXVtZsM-Rw-jOE0LR9M5rL9WiZdX5X4vI,13505
22
22
  ansible/collections/__init__.py,sha256=t8x1TZiQQa9vaQAm7ECYoP3MCdzquEtAlPzG9HzEV50,814
@@ -34,10 +34,10 @@ ansible/executor/__init__.py,sha256=1lMXN1i2fFqslda4BmeI5tpYMFP95D5Wpr1AjDJi-SQ,
34
34
  ansible/executor/action_write_locks.py,sha256=Up2n3cwFCr4T4IvttHpe3QOxRBF_9NgWJ1tFm9CHpfM,1915
35
35
  ansible/executor/interpreter_discovery.py,sha256=0Pad_qYo8OSfA0U0AqHOXGt1HPKcqxBw-NfTKq4HmfQ,9926
36
36
  ansible/executor/module_common.py,sha256=6R58IqfOLzg0aDQWRWsi0cbohWMSf_Lvdhf_5hTavWg,65820
37
- ansible/executor/play_iterator.py,sha256=FUxYjkFU8s9RessNxdey66TWfXP1NvWXZbzBDFAJYdY,31013
37
+ ansible/executor/play_iterator.py,sha256=WmByZKIcBYx1gT5ybsIv9yiwkimmljR_NnbBVtYs2X8,31562
38
38
  ansible/executor/playbook_executor.py,sha256=VQHEIvZbfOFzp388XFD0KjG0e8Ye8yuNPnnHAZmi898,15069
39
39
  ansible/executor/stats.py,sha256=757UK8wDzLCXq4ltI9PqpoMNAdtRsd9D9-GS-5Al_Hs,3264
40
- ansible/executor/task_executor.py,sha256=Yfxrsd387IH3aEJgeXNCQSZhg5YVor-9PMaXkq2o5wI,59059
40
+ ansible/executor/task_executor.py,sha256=8LUxmZs5Ak3ciU_LayWB3LbCrzu4VkpCGnRkKGBCa-U,60003
41
41
  ansible/executor/task_queue_manager.py,sha256=DxmfDMeWAClNvp85qvc1uATor-hilv8KsYno3Pl_Ztk,18758
42
42
  ansible/executor/task_result.py,sha256=DvshMci5i9-qCXs0m_vScSa6BJMbPwwNQBV7L2DTCzE,5748
43
43
  ansible/executor/discovery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -56,11 +56,11 @@ ansible/executor/powershell/module_wrapper.ps1,sha256=JkCL_6aAZoXgQmFjU5hgEzTCMd
56
56
  ansible/executor/process/__init__.py,sha256=1lMXN1i2fFqslda4BmeI5tpYMFP95D5Wpr1AjDJi-SQ,833
57
57
  ansible/executor/process/worker.py,sha256=k5aTaoCUu_ZUmdnPgIGqGsD7013_o3srzwxNpAjD1SY,9415
58
58
  ansible/galaxy/__init__.py,sha256=_ccTedn8dUGGtkmHcQLIkeje_YD0TYSXlvCl1AOY5fE,2533
59
- ansible/galaxy/api.py,sha256=ZsnTh9ucCFxMDlBPCS9LuVxLmUPZJBDFD77yj0KQxXk,39978
59
+ ansible/galaxy/api.py,sha256=deSYsFinaJodT2Y9-XnOerWIwYY8V2AWQ_9kZI0pWCE,39872
60
60
  ansible/galaxy/role.py,sha256=roEhuloz2-UHLdNwK7pqRCYsOLpu_Xg6sC_nyE5A30w,19086
61
61
  ansible/galaxy/token.py,sha256=K0dAwD3Fjkn3Zs2N9sG98UesSWfAukie47QGyYpIf0M,6167
62
62
  ansible/galaxy/user_agent.py,sha256=x7cJzzpnTngHcwqSUd2hg0i28Dv0tbAyBdke5CSiNhM,813
63
- ansible/galaxy/collection/__init__.py,sha256=YgQ8dS5Z7jbuKE5bqaEZoRttrA_0YphQUldkPJnN8b4,77547
63
+ ansible/galaxy/collection/__init__.py,sha256=ImRjgWK5DTAI1BsIYwK3p4c7E5wkrtqTZnfPU1UGGPE,77683
64
64
  ansible/galaxy/collection/concrete_artifact_manager.py,sha256=Zw59WkR7kHaN1GnJDmzfiTSH31eZ4dEBhyzeOrvT5zs,28978
65
65
  ansible/galaxy/collection/galaxy_api_proxy.py,sha256=HWnMiWIEt1YW7srbnFXjRsgpSC-3Iwj7-wkrkmVtXkA,7972
66
66
  ansible/galaxy/collection/gpg.py,sha256=1wk22RJnX--FsB-4h_EdaT05PWlx9AMxhfH3H7db1i4,7312
@@ -126,9 +126,9 @@ ansible/galaxy/data/network/tests/inventory,sha256=4CIzgZsaCYREEFSRkYE_fMe6Ng8hK
126
126
  ansible/galaxy/data/network/tests/test.yml.j2,sha256=rjvtKhlkGT_CI_dgBLHX14eI41S6jFPypqESN8Uztgg,199
127
127
  ansible/galaxy/data/network/vars/main.yml.j2,sha256=3qtsgmeZTZ37OdmQNpmI9R6XxOEzJcOhjjGQYGFb85w,36
128
128
  ansible/galaxy/dependency_resolution/__init__.py,sha256=HDqJKqsDP4UHTCnoGq6c3rGpVwOI2DdYaH-4s7tkL0A,2196
129
- ansible/galaxy/dependency_resolution/dataclasses.py,sha256=ervw-YIKoJ_gy1I8DJ1IlariivLu0aQafacMgSztVCA,21795
129
+ ansible/galaxy/dependency_resolution/dataclasses.py,sha256=08Sjja6PupKzK0WdmRTRjmIt8Vo4e8seKR3TOuQNSGA,22456
130
130
  ansible/galaxy/dependency_resolution/errors.py,sha256=3YatCcrKVHeXTkMQBEFnTEHZBUTpd9ySEJ4r-3MbEak,750
131
- ansible/galaxy/dependency_resolution/providers.py,sha256=6kbBLeenv697L-QQ_lsTceXfrGiGE0QT7EBX4iaXRJY,25062
131
+ ansible/galaxy/dependency_resolution/providers.py,sha256=9p0-DGy_imfAQJ1xjo_tO_CwmfW6TVcvC7NM7G3Z4B0,24946
132
132
  ansible/galaxy/dependency_resolution/reporters.py,sha256=q-jyfsRu5qve8nikZ0_cBQar3dOX4_WMSAUXZd9xlqg,687
133
133
  ansible/galaxy/dependency_resolution/resolvers.py,sha256=XHBYqltTS7AV4mSiAF2ImXP53W6Yadog_rrqKxrpZU0,676
134
134
  ansible/galaxy/dependency_resolution/versioning.py,sha256=fGmuNhgwHaCKfHb6uH91A7ztuB86nDgiMW3htSI06DY,1779
@@ -140,9 +140,9 @@ ansible/inventory/host.py,sha256=wXJp6kpSaZtDr4JNsgdAuhi5MzQ9LTQzaAH10zoVbIA,505
140
140
  ansible/inventory/manager.py,sha256=tGwhBR6poLuG_i4jZ5RGOG-rH4gu4DBfT0-4iLLZZMs,29490
141
141
  ansible/module_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
142
  ansible/module_utils/_text.py,sha256=F_YfeaxhwmTI16HICAzQS9ZmlKgBDdQ4mqR-Kh--okg,597
143
- ansible/module_utils/ansible_release.py,sha256=QGcWt9a89G2IlrC7FJFlSl5xOwkjeg7OmcJKUzM_Avc,920
143
+ ansible/module_utils/ansible_release.py,sha256=P-fXbdy9zVITuZdHA46usN4gSEjuWpRFm8Vf8lcZFtc,920
144
144
  ansible/module_utils/api.py,sha256=BTo7stVOANbtd-ngZslaqx70r9t5gfvo44cKyu5SFjU,5837
145
- ansible/module_utils/basic.py,sha256=DdrhOpqSmlv6WoCRE0lacVyVAe7pXKjo9oSo_u5Pfu4,87116
145
+ ansible/module_utils/basic.py,sha256=KwFTKMws6bPfSEP1fIc7Srvan6b34EEbZ5nedfhhiTw,87493
146
146
  ansible/module_utils/connection.py,sha256=XHxMlyAdwLiXDSo8jBMkV61-lz_0FDJUYH1B152UGJU,8430
147
147
  ansible/module_utils/errors.py,sha256=LYv9EWkvBRAmYW6qQY4Vz2hMULqkAGkMG2Opf86Ep4M,3396
148
148
  ansible/module_utils/json_utils.py,sha256=IR_bSwrYK1Ie36dCQSHyN4mahkrZkzIIkH3DdwtIi6Q,3456
@@ -292,7 +292,7 @@ ansible/modules/deb822_repository.py,sha256=9XBk-w4zFTXi2A3hkivEMTbUnZSKqCmxh2wp
292
292
  ansible/modules/debconf.py,sha256=uCnyPYGhNPHq7cBUVzfxHp_5-N8RauBL8zlc19NHQic,7732
293
293
  ansible/modules/debug.py,sha256=_wSyvOmlhpDeiD40oC_uwbRisSJLVbfIbEN98TJxVfs,2958
294
294
  ansible/modules/dnf.py,sha256=OqX1KZlkop-pW5ye1kSUQqbn4y41wgDcVTdSf-VPJX0,59955
295
- ansible/modules/dnf5.py,sha256=AdfRDpBA7J65TyXJ5ArbTyQxdZwJblbPYrmTLviD-8o,26385
295
+ ansible/modules/dnf5.py,sha256=JlUeTq2iV8UNkJ5msd25bCJOFszeNOrMD5ihVDR76wQ,26452
296
296
  ansible/modules/dpkg_selections.py,sha256=X3owrI7nDVM2tYOiVgLPkzBEZxfwkfBuomjbAfzG1GQ,2408
297
297
  ansible/modules/expect.py,sha256=pdPmPmPLc0HbfcVTIIJFZq2NwcTwhP-O_S3bRwSpG70,8518
298
298
  ansible/modules/fail.py,sha256=AI4gNQC7E5U2Vs7QiIlFk7PcWozN0tXtiVPa_LJrQpk,1710
@@ -580,11 +580,11 @@ ansible/plugins/shell/__init__.py,sha256=Rj-H2AhfBZAWZ_Hy8D-1ypvjXTMP3pTbByOYlPM
580
580
  ansible/plugins/shell/cmd.py,sha256=fswLtU2XVNb1T5tF0BIM9msViObs5dXzo9k6sNN4dao,2207
581
581
  ansible/plugins/shell/powershell.py,sha256=qnpEZ9uOJF_4gExheFCAYT_tSR_KQtQeilU1WKXJymA,11376
582
582
  ansible/plugins/shell/sh.py,sha256=1nhiMv0_c8zu2MaDHvOCr--dG8b-iUVEPPnpMh_Hx8I,3952
583
- ansible/plugins/strategy/__init__.py,sha256=pxs6OaFwYm5dN995kaqH_hrK8nzePD5MRPYIiLG2C9A,57490
583
+ ansible/plugins/strategy/__init__.py,sha256=ecMkuKVsIZyZHJzcPiFqq-RU74_Ub8bYptw5njge9uI,56268
584
584
  ansible/plugins/strategy/debug.py,sha256=GxUS0bSiaWInIK8zgB7rMREEqvgrZhVlFUzOCJtnjFo,1258
585
585
  ansible/plugins/strategy/free.py,sha256=eXAvxTFloyb5x6VYANXDMdloWUYxMhbPr1lyY6UPBps,15773
586
586
  ansible/plugins/strategy/host_pinned.py,sha256=3-q5l-tpheMlU-BXGm6ZQNgHvQv5IMvOCDZBLibl1L4,1959
587
- ansible/plugins/strategy/linear.py,sha256=maTN_kZpC3UYg_ZW7wsim7WecG8lr6puK7i2CnHj2Ow,20389
587
+ ansible/plugins/strategy/linear.py,sha256=pTdVG7cWrcDK1pXViLmiGtEPKMrOcgBWaGUENkd2RYg,20172
588
588
  ansible/plugins/terminal/__init__.py,sha256=zGIuxlntye0FHk6Zbl57snHB5d3-w_pr0osRpCRy4co,4438
589
589
  ansible/plugins/test/__init__.py,sha256=6DY18LxzSdtO7-fDS6957bo61fg-xG3TDWvtFkhGYOQ,471
590
590
  ansible/plugins/test/abs.yml,sha256=-caY4vAMXbhukUTdMQvBa2WYvg6w1AWr8raEfAv0qa8,764
@@ -682,11 +682,11 @@ ansible/vars/hostvars.py,sha256=dg3jpVmNwSg8EJ4SIvYGT80uxMgRtrOW6vvtDfrQzDU,5152
682
682
  ansible/vars/manager.py,sha256=qsF6PgAYcon5n7HmXG56P4pmKLyrniuFpAtKWnNaFpw,38284
683
683
  ansible/vars/plugins.py,sha256=B7L3fXoSOoBZSXqJ2ulk0adx1g5SpAb8BxyLGPNA7d4,4695
684
684
  ansible/vars/reserved.py,sha256=FBD7n2dnA0CW4I0J1LtWwk2hQqvGW0KTRPcxaRtMKWo,2615
685
- ansible_core-2.15.0b1.data/scripts/ansible-test,sha256=CYIYL99IxWdVTtDIj3avilIJXhGAmtjuKPPWNuLWuc8,1690
685
+ ansible_core-2.15.0b3.data/scripts/ansible-test,sha256=CYIYL99IxWdVTtDIj3avilIJXhGAmtjuKPPWNuLWuc8,1690
686
686
  ansible_test/__init__.py,sha256=6e721yAyyyocRKzbCKtQXloAfFP7Aqv0L3zG70uh-4A,190
687
687
  ansible_test/_data/ansible.cfg,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
688
688
  ansible_test/_data/coveragerc,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
689
- ansible_test/_data/completion/docker.txt,sha256=1zGDg_jMhcZtgAEjCO75RrjzhpoQMj9nKZOPQRwskHA,822
689
+ ansible_test/_data/completion/docker.txt,sha256=AnhWoIqdBVSn-mPiZiY_87U1MXffmbRBXyTiTuQqCqQ,822
690
690
  ansible_test/_data/completion/network.txt,sha256=_-mi013-JeufshKMUmykkOmZPw1cVbakIMaAuweHet8,198
691
691
  ansible_test/_data/completion/remote.txt,sha256=q_sCM75PX_Q4lxz4SFRGQZwm885FehFitb9eROgGqcg,1044
692
692
  ansible_test/_data/completion/windows.txt,sha256=k02uwXJ2i7cVHZnXS4HRFlvK5QU5BzAV291Cw53FlXA,226
@@ -726,8 +726,8 @@ ansible_test/_data/requirements/sanity.pylint.in,sha256=CqgyF_s4K3o41RSc6KZVicBl
726
726
  ansible_test/_data/requirements/sanity.pylint.txt,sha256=VzyfimUEUboaL0g7XgPUx9pS6vf_dE9IQepRY8rradU,293
727
727
  ansible_test/_data/requirements/sanity.runtime-metadata.in,sha256=QzOCB5QxVHYuXHXQvkUsa5MwRQzPhI-ZDD-M2htj36s,18
728
728
  ansible_test/_data/requirements/sanity.runtime-metadata.txt,sha256=K0Kesnp-H7naekb-cXc0oMAvqNKLwUATh_DgwcIdL3Q,148
729
- ansible_test/_data/requirements/sanity.validate-modules.in,sha256=5gvF_IneKa_2LmPlSfFLOu_OhI_t3l8ny31d1avuIWg,88
730
- ansible_test/_data/requirements/sanity.validate-modules.txt,sha256=MbPw8Qzl8eg1I9qJaVD7uK9cnMUH_X9bGh1toghVA1I,180
729
+ ansible_test/_data/requirements/sanity.validate-modules.in,sha256=XDFnVqB9yhfWhwIOBkTw-Dw8hpMnxqO1hXtettetUIU,117
730
+ ansible_test/_data/requirements/sanity.validate-modules.txt,sha256=WzuOlP9NzdxowbQ1AcL09yt7zQ71n0R6GmcLyJ6cb20,209
731
731
  ansible_test/_data/requirements/sanity.yamllint.in,sha256=ivPsPeZUDHOuLbd603ZxKClOQ1bATyMYNx3GfHQmt4g,9
732
732
  ansible_test/_data/requirements/sanity.yamllint.txt,sha256=t_5YBcbqw7-c3GCbTE3jh3e5M4YfRENpG0eFtJYpY-A,147
733
733
  ansible_test/_data/requirements/units.txt,sha256=ah91xwwRFeY_fpi0WdRGw9GqEiAjm9BbVbnwTrdzn2g,125
@@ -791,7 +791,7 @@ ansible_test/_internal/cli/environments.py,sha256=M5Vcolpz-GBBUVlv8gVBl7_54RTSJL
791
791
  ansible_test/_internal/cli/epilog.py,sha256=kzCYlmqDccMZnSCV57iXUITo6Z9FMMUIagjWJHHA0yY,658
792
792
  ansible_test/_internal/cli/argparsing/__init__.py,sha256=ravr0Yv7tEOBFv2s2DuZtEl9BPAQNy-KMKcJNSk4dmc,8922
793
793
  ansible_test/_internal/cli/argparsing/actions.py,sha256=VplAf5K9G-loJmLXMAZwbRbIsuFJ-yDrRrP4El5p4RM,606
794
- ansible_test/_internal/cli/argparsing/argcompletion.py,sha256=bC2E7sBREAUufBSNTrrkSqJ9QtaDfLtaepugLhX9lvs,4562
794
+ ansible_test/_internal/cli/argparsing/argcompletion.py,sha256=zOZtYVDkqWIdbmuASkyJuMUKrFh4w3MJzYS2O9DoIQA,5166
795
795
  ansible_test/_internal/cli/argparsing/parsers.py,sha256=i7bEPWy7q2mcgiBb3sZ0EN5wQ0G5SetOMQKsOMSsw4M,21490
796
796
  ansible_test/_internal/cli/commands/__init__.py,sha256=d8FNvVbSVR2JlnyDUxnS-lZDIQqbdEEPU0cqJA9663Q,5436
797
797
  ansible_test/_internal/cli/commands/env.py,sha256=-3zKICX4STeo_lObMh6EmvBy3s2ORL53idmLKN2vCHk,1397
@@ -938,7 +938,7 @@ ansible_test/_util/controller/sanity/code-smell/use-compat-six.py,sha256=CkYomOt
938
938
  ansible_test/_util/controller/sanity/code-smell/changelog/sphinx.py,sha256=M3aEK_XugBtVJjfUZbeoVc10hzRylxRxNfEiNq1JVWQ,193
939
939
  ansible_test/_util/controller/sanity/integration-aliases/yaml_to_json.py,sha256=qxXHZboRVEqISZYOIXrutsAgobEyh6fiUibk133fzhI,299
940
940
  ansible_test/_util/controller/sanity/mypy/ansible-core.ini,sha256=B13dYyd5PGoN-BrFShPMhGBCGbV2oiTsBD8TdRDAh3Q,2327
941
- ansible_test/_util/controller/sanity/mypy/ansible-test.ini,sha256=g412ZxuLBxN22FM-jS49RA0JoT_M7q4S4g7x2LW9hf8,850
941
+ ansible_test/_util/controller/sanity/mypy/ansible-test.ini,sha256=lbBGGRhM-sL7iUdt5f0ctGSAvPmjCXIFeogm3Z22qkA,908
942
942
  ansible_test/_util/controller/sanity/mypy/modules.ini,sha256=48N2I3ubw3yAuE8layHQ_d0CTfH_eATuXt-K5Bq-ifw,1694
943
943
  ansible_test/_util/controller/sanity/pep8/current-ignore.txt,sha256=9VSaFOsdxN4_8GJVhnmpl5kXos2TPU3M08eC_NRI2Ks,196
944
944
  ansible_test/_util/controller/sanity/pslint/pslint.ps1,sha256=h0fLdkwF7JhGGjApvqAsCU87BKy0E_UiFJ_O7MARz6U,1089
@@ -954,10 +954,10 @@ ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py,sha256=WGG1z4M3N
954
954
  ansible_test/_util/controller/sanity/shellcheck/exclude.txt,sha256=-idybvpZeOHVfR8usoyCNdNLD5WpaKQeP9mgzganMpQ,21
955
955
  ansible_test/_util/controller/sanity/validate-modules/validate.py,sha256=jpNOhA5qJ5LdlWlSOJoJyTUh9H1tepjcSYZXeHdhJRY,114
956
956
  ansible_test/_util/controller/sanity/validate-modules/validate_modules/__init__.py,sha256=CRUAj-k-zJye4RAGZ8eR9HvP6weM6VKTwGmFYpI_0Bw,816
957
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py,sha256=goyWCvO3imrvj87M3PakVa-UjWICFCyU8IPdsZufRsI,113873
957
+ ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py,sha256=m5vrFrpIX1XVT6vNUZU9gLC2RHWAhNeNGnXVBBSXokY,114134
958
958
  ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py,sha256=TlRRjgB2ICrN_4bmQe4MgCTlgMn_su_9Iqi_l5t5OWQ,6611
959
959
  ansible_test/_util/controller/sanity/validate-modules/validate_modules/ps_argspec.ps1,sha256=wteIiuD7-UOEGkjdUArKqVVWBpa7A7FU_WwDuMtR2mY,4139
960
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py,sha256=Wp-HvJF9h9GybkfrRAwyBwr-VeO3Hcb0rBK1lEVUfS4,38563
960
+ ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py,sha256=uXiiQMOv6ej1_AYdP-edKRhIaQ51d24FwwtZLHmDJjA,36172
961
961
  ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py,sha256=9AKO67hQe1mEzyHUbKcCfe4f5ScKEPjWjvSG75HUFWs,7037
962
962
  ansible_test/_util/controller/sanity/yamllint/yamllinter.py,sha256=TW7moUDCDO2QnlzvNi_MJypQxnKV49AC7QVWaeMKLU4,8369
963
963
  ansible_test/_util/controller/sanity/yamllint/config/default.yml,sha256=19ITqd_UW6PVgoxX0C_N65x2zp4L5zQ5xWJ0Y0p4BI4,534
@@ -999,9 +999,9 @@ ansible_test/config/cloud-config-vultr.ini.template,sha256=XLKHk3lg_8ReQMdWfZzhh
999
999
  ansible_test/config/config.yml,sha256=wb3knoBmZewG3GWOMnRHoVPQWW4vPixKLPMNS6vJmTc,2620
1000
1000
  ansible_test/config/inventory.networking.template,sha256=bFNSk8zNQOaZ_twaflrY0XZ9mLwUbRLuNT0BdIFwvn4,1335
1001
1001
  ansible_test/config/inventory.winrm.template,sha256=1QU8W-GFLnYEw8yY9bVIvUAVvJYPM3hyoijf6-M7T00,1098
1002
- ansible_core-2.15.0b1.dist-info/COPYING,sha256=CuBIWlvTemPmNgNZZBfk6w5lMzT6bH-TLKOg6F1K8ic,35148
1003
- ansible_core-2.15.0b1.dist-info/METADATA,sha256=bdXTZiX6VBl1gsSGuvNJOH6O2EEi61l5JS_Vwtu2V6U,7506
1004
- ansible_core-2.15.0b1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
1005
- ansible_core-2.15.0b1.dist-info/entry_points.txt,sha256=0mpmsrIhODChxKl3eS-NcVQCaMetBn8KdPLtVxQgR64,453
1006
- ansible_core-2.15.0b1.dist-info/top_level.txt,sha256=IFbRLjAvih1DYzJWg3_F6t4sCzEMxRO7TOMNs6GkYHo,21
1007
- ansible_core-2.15.0b1.dist-info/RECORD,,
1002
+ ansible_core-2.15.0b3.dist-info/COPYING,sha256=CuBIWlvTemPmNgNZZBfk6w5lMzT6bH-TLKOg6F1K8ic,35148
1003
+ ansible_core-2.15.0b3.dist-info/METADATA,sha256=XrERIJGjWiZy6gmN7oZxVTmEko90rTP5JsgV9L17KAI,7506
1004
+ ansible_core-2.15.0b3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
1005
+ ansible_core-2.15.0b3.dist-info/entry_points.txt,sha256=0mpmsrIhODChxKl3eS-NcVQCaMetBn8KdPLtVxQgR64,453
1006
+ ansible_core-2.15.0b3.dist-info/top_level.txt,sha256=IFbRLjAvih1DYzJWg3_F6t4sCzEMxRO7TOMNs6GkYHo,21
1007
+ ansible_core-2.15.0b3.dist-info/RECORD,,
@@ -1,6 +1,6 @@
1
1
  base image=quay.io/ansible/base-test-container:4.1.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10
2
- default image=quay.io/ansible/default-test-container:7.10.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=collection
3
- default image=quay.io/ansible/ansible-core-test-container:7.10.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=ansible-core
2
+ default image=quay.io/ansible/default-test-container:7.11.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=collection
3
+ default image=quay.io/ansible/ansible-core-test-container:7.11.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=ansible-core
4
4
  alpine3 image=quay.io/ansible/alpine3-test-container:5.0.0 python=3.10 cgroup=none audit=none
5
5
  centos7 image=quay.io/ansible/centos7-test-container:5.0.0 python=2.7 cgroup=v1-only
6
6
  fedora37 image=quay.io/ansible/fedora37-test-container:5.0.0 python=3.11
@@ -1,3 +1,4 @@
1
1
  jinja2 # ansible-core requirement
2
2
  pyyaml # needed for collection_detail.py
3
3
  voluptuous
4
+ antsibull-docs-parser==0.2.0
@@ -1,4 +1,5 @@
1
1
  # edit "sanity.validate-modules.in" and generate with: hacking/update-sanity-requirements.py --test validate-modules
2
+ antsibull-docs-parser==0.2.0
2
3
  Jinja2==3.1.2
3
4
  MarkupSafe==2.1.2
4
5
  PyYAML==6.0
@@ -17,10 +17,19 @@ class Substitute:
17
17
  try:
18
18
  import argcomplete
19
19
 
20
- from argcomplete import (
21
- CompletionFinder,
22
- default_validator,
23
- )
20
+ try:
21
+ # argcomplete 3+
22
+ # see: https://github.com/kislyuk/argcomplete/commit/bd781cb08512b94966312377186ebc5550f46ae0
23
+ from argcomplete.finders import (
24
+ CompletionFinder,
25
+ default_validator,
26
+ )
27
+ except ImportError:
28
+ # argcomplete <3
29
+ from argcomplete import (
30
+ CompletionFinder,
31
+ default_validator,
32
+ )
24
33
 
25
34
  warn = argcomplete.warn # pylint: disable=invalid-name
26
35
  except ImportError:
@@ -72,7 +81,13 @@ class CompType(enum.Enum):
72
81
  def register_safe_action(action_type: t.Type[argparse.Action]) -> None:
73
82
  """Register the given action as a safe action for argcomplete to use during completion if it is not already registered."""
74
83
  if argcomplete and action_type not in argcomplete.safe_actions:
75
- argcomplete.safe_actions += (action_type,)
84
+ if isinstance(argcomplete.safe_actions, set):
85
+ # argcomplete 3+
86
+ # see: https://github.com/kislyuk/argcomplete/commit/bd781cb08512b94966312377186ebc5550f46ae0
87
+ argcomplete.safe_actions.add(action_type)
88
+ else:
89
+ # argcomplete <3
90
+ argcomplete.safe_actions += (action_type,)
76
91
 
77
92
 
78
93
  def get_comp_type() -> t.Optional[CompType]:
@@ -14,6 +14,9 @@ disable_error_code = type-abstract
14
14
  [mypy-argcomplete]
15
15
  ignore_missing_imports = True
16
16
 
17
+ [mypy-argcomplete.finders]
18
+ ignore_missing_imports = True
19
+
17
20
  [mypy-coverage]
18
21
  ignore_missing_imports = True
19
22
 
@@ -33,6 +33,9 @@ from collections.abc import Mapping
33
33
  from contextlib import contextmanager
34
34
  from fnmatch import fnmatch
35
35
 
36
+ from antsibull_docs_parser import dom
37
+ from antsibull_docs_parser.parser import parse, Context
38
+
36
39
  import yaml
37
40
 
38
41
  from voluptuous.humanize import humanize_error
@@ -79,10 +82,6 @@ from .schema import (
79
82
  ansible_module_kwargs_schema,
80
83
  doc_schema,
81
84
  return_schema,
82
- _SEM_OPTION_NAME,
83
- _SEM_RET_VALUE,
84
- _check_sem_quoting,
85
- _parse_prefix,
86
85
  )
87
86
 
88
87
  from .utils import CaptureStd, NoArgsAnsibleModule, compare_unordered_lists, parse_yaml, parse_isodate
@@ -1164,39 +1163,31 @@ class ModuleValidator(Validator):
1164
1163
 
1165
1164
  return doc_info, doc
1166
1165
 
1167
- def _check_sem_option(self, directive, content):
1168
- try:
1169
- content = _check_sem_quoting(directive, content)
1170
- plugin_fqcn, plugin_type, option_link, option, value = _parse_prefix(directive, content)
1171
- except Exception:
1172
- # Validation errors have already been covered in the schema check
1166
+ def _check_sem_option(self, part: dom.OptionNamePart, current_plugin: dom.PluginIdentifier) -> None:
1167
+ if part.plugin is None or part.plugin != current_plugin:
1173
1168
  return
1174
- if plugin_fqcn is not None:
1169
+ if part.entrypoint is not None:
1175
1170
  return
1176
- if tuple(option_link) not in self._all_options:
1171
+ if tuple(part.link) not in self._all_options:
1177
1172
  self.reporter.error(
1178
1173
  path=self.object_path,
1179
1174
  code='invalid-documentation-markup',
1180
- msg='Directive "%s" contains a non-existing option "%s"' % (directive, option)
1175
+ msg='Directive "%s" contains a non-existing option "%s"' % (part.source, part.name)
1181
1176
  )
1182
1177
 
1183
- def _check_sem_return_value(self, directive, content):
1184
- try:
1185
- content = _check_sem_quoting(directive, content)
1186
- plugin_fqcn, plugin_type, rv_link, rv, value = _parse_prefix(directive, content)
1187
- except Exception:
1188
- # Validation errors have already been covered in the schema check
1178
+ def _check_sem_return_value(self, part: dom.ReturnValuePart, current_plugin: dom.PluginIdentifier) -> None:
1179
+ if part.plugin is None or part.plugin != current_plugin:
1189
1180
  return
1190
- if plugin_fqcn is not None:
1181
+ if part.entrypoint is not None:
1191
1182
  return
1192
- if tuple(rv_link) not in self._all_return_values:
1183
+ if tuple(part.link) not in self._all_return_values:
1193
1184
  self.reporter.error(
1194
1185
  path=self.object_path,
1195
1186
  code='invalid-documentation-markup',
1196
- msg='Directive "%s" contains a non-existing return value "%s"' % (directive, rv)
1187
+ msg='Directive "%s" contains a non-existing return value "%s"' % (part.source, part.name)
1197
1188
  )
1198
1189
 
1199
- def _validate_semantic_markup(self, object):
1190
+ def _validate_semantic_markup(self, object) -> None:
1200
1191
  # Make sure we operate on strings
1201
1192
  if is_iterable(object):
1202
1193
  for entry in object:
@@ -1205,10 +1196,19 @@ class ModuleValidator(Validator):
1205
1196
  if not isinstance(object, string_types):
1206
1197
  return
1207
1198
 
1208
- for m in _SEM_OPTION_NAME.finditer(object):
1209
- self._check_sem_option(m.group(0), m.group(1))
1210
- for m in _SEM_RET_VALUE.finditer(object):
1211
- self._check_sem_return_value(m.group(0), m.group(1))
1199
+ if self.collection:
1200
+ fqcn = f'{self.collection_name}.{self.name}'
1201
+ else:
1202
+ fqcn = f'ansible.builtin.{self.name}'
1203
+ current_plugin = dom.PluginIdentifier(fqcn=fqcn, type=self.plugin_type)
1204
+ for par in parse(object, Context(current_plugin=current_plugin), errors='message', add_source=True):
1205
+ for part in par:
1206
+ # Errors are already covered during schema validation, we only check for option and
1207
+ # return value references
1208
+ if part.type == dom.PartType.OPTION_NAME:
1209
+ self._check_sem_option(part, current_plugin)
1210
+ if part.type == dom.PartType.RETURN_VALUE:
1211
+ self._check_sem_return_value(part, current_plugin)
1212
1212
 
1213
1213
  def _validate_semantic_markup_collect(self, destination, sub_key, data, all_paths):
1214
1214
  if not isinstance(data, dict):
@@ -11,7 +11,7 @@ from ansible.module_utils.compat.version import StrictVersion
11
11
  from functools import partial
12
12
  from urllib.parse import urlparse
13
13
 
14
- from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid, Exclusive
14
+ from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, MultipleInvalid, Required, Schema, Self, ValueInvalid, Exclusive
15
15
  from ansible.constants import DOCUMENTABLE_PLUGINS
16
16
  from ansible.module_utils.six import string_types
17
17
  from ansible.module_utils.common.collections import is_iterable
@@ -20,6 +20,9 @@ from ansible.parsing.quoting import unquote
20
20
  from ansible.utils.version import SemanticVersion
21
21
  from ansible.release import __version__
22
22
 
23
+ from antsibull_docs_parser import dom
24
+ from antsibull_docs_parser.parser import parse, Context
25
+
23
26
  from .utils import parse_isodate
24
27
 
25
28
  list_string_types = list(string_types)
@@ -81,57 +84,8 @@ def date(error_code=None):
81
84
  return Any(isodate, error_code=error_code)
82
85
 
83
86
 
84
- _MODULE = re.compile(r"\bM\(([^)]+)\)")
85
- _PLUGIN = re.compile(r"\bP\(([^)]+)\)")
86
- _LINK = re.compile(r"\bL\(([^)]+)\)")
87
- _URL = re.compile(r"\bU\(([^)]+)\)")
88
- _REF = re.compile(r"\bR\(([^)]+)\)")
89
-
90
- _SEM_PARAMETER_STRING = r"\(((?:[^\\)]+|\\.)+)\)"
91
- _SEM_OPTION_NAME = re.compile(r"\bO" + _SEM_PARAMETER_STRING)
92
- _SEM_OPTION_VALUE = re.compile(r"\bV" + _SEM_PARAMETER_STRING)
93
- _SEM_ENV_VARIABLE = re.compile(r"\bE" + _SEM_PARAMETER_STRING)
94
- _SEM_RET_VALUE = re.compile(r"\bRV" + _SEM_PARAMETER_STRING)
95
-
96
- _UNESCAPE = re.compile(r"\\(.)")
97
- _CONTENT_LINK_SPLITTER_RE = re.compile(r'(?:\[[^\]]*\])?\.')
98
- _CONTENT_LINK_END_STUB_RE = re.compile(r'\[[^\]]*\]$')
99
- _FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$')
100
- _IGNORE_MARKER = 'ignore:'
101
- _IGNORE_STRING = '(ignore)'
102
-
103
- _VALID_PLUGIN_TYPES = set(DOCUMENTABLE_PLUGINS)
104
-
105
-
106
- def _check_module_link(directive, content):
107
- if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(content):
108
- raise _add_ansible_error_code(
109
- Invalid('Directive "%s" must contain a FQCN' % directive), 'invalid-documentation-markup')
110
-
111
-
112
- def _check_plugin_link(directive, content):
113
- if '#' not in content:
114
- raise _add_ansible_error_code(
115
- Invalid('Directive "%s" must contain a "#"' % directive), 'invalid-documentation-markup')
116
- plugin_fqcn, plugin_type = content.split('#', 1)
117
- if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(plugin_fqcn):
118
- raise _add_ansible_error_code(
119
- Invalid('Directive "%s" must contain a FQCN; found "%s"' % (directive, plugin_fqcn)),
120
- 'invalid-documentation-markup')
121
- if plugin_type not in _VALID_PLUGIN_TYPES:
122
- raise _add_ansible_error_code(
123
- Invalid('Directive "%s" must contain a valid plugin type; found "%s"' % (directive, plugin_type)),
124
- 'invalid-documentation-markup')
125
-
126
-
127
- def _check_link(directive, content):
128
- if ',' not in content:
129
- raise _add_ansible_error_code(
130
- Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
131
- idx = content.rindex(',')
132
- title = content[:idx]
133
- url = content[idx + 1:].lstrip(' ')
134
- _check_url(directive, url)
87
+ # Roles can also be referenced by semantic markup
88
+ _VALID_PLUGIN_TYPES = set(DOCUMENTABLE_PLUGINS + ('role', ))
135
89
 
136
90
 
137
91
  def _check_url(directive, content):
@@ -139,67 +93,10 @@ def _check_url(directive, content):
139
93
  parsed_url = urlparse(content)
140
94
  if parsed_url.scheme not in ('', 'http', 'https'):
141
95
  raise ValueError('Schema must be HTTP, HTTPS, or not specified')
142
- except ValueError as exc:
143
- raise _add_ansible_error_code(
144
- Invalid('Directive "%s" must contain an URL' % directive), 'invalid-documentation-markup')
145
-
146
-
147
- def _check_ref(directive, content):
148
- if ',' not in content:
149
- raise _add_ansible_error_code(
150
- Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
151
-
152
-
153
- def _check_sem_quoting(directive, content):
154
- for m in _UNESCAPE.finditer(content):
155
- if m.group(1) not in ('\\', ')'):
156
- raise _add_ansible_error_code(
157
- Invalid('Directive "%s" contains unnecessarily quoted "%s"' % (directive, m.group(1))),
158
- 'invalid-documentation-markup')
159
- return _UNESCAPE.sub(r'\1', content)
160
-
161
-
162
- def _parse_prefix(directive, content):
163
- value = None
164
- if '=' in content:
165
- content, value = content.split('=', 1)
166
- m = _FQCN_TYPE_PREFIX_RE.match(content)
167
- if m:
168
- plugin_fqcn = m.group(1)
169
- plugin_type = m.group(2)
170
- content = m.group(3)
171
- if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(plugin_fqcn):
172
- raise _add_ansible_error_code(
173
- Invalid('Directive "%s" must contain a FQCN; found "%s"' % (directive, plugin_fqcn)),
174
- 'invalid-documentation-markup')
175
- if plugin_type not in _VALID_PLUGIN_TYPES:
176
- raise _add_ansible_error_code(
177
- Invalid('Directive "%s" must contain a valid plugin type; found "%s"' % (directive, plugin_type)),
178
- 'invalid-documentation-markup')
179
- elif content.startswith(_IGNORE_MARKER):
180
- content = content[len(_IGNORE_MARKER):]
181
- plugin_fqcn = plugin_type = _IGNORE_STRING
182
- else:
183
- plugin_fqcn = plugin_type = None
184
- if ':' in content or '#' in content:
185
- raise _add_ansible_error_code(
186
- Invalid('Directive "%s" contains wrongly specified FQCN/plugin type' % directive),
187
- 'invalid-documentation-markup')
188
- content_link = _CONTENT_LINK_SPLITTER_RE.split(content)
189
- for i, part in enumerate(content_link):
190
- if i == len(content_link) - 1:
191
- part = _CONTENT_LINK_END_STUB_RE.sub('', part)
192
- content_link[i] = part
193
- if '.' in part or '[' in part or ']' in part:
194
- raise _add_ansible_error_code(
195
- Invalid('Directive "%s" contains invalid name "%s"' % (directive, content)),
196
- 'invalid-documentation-markup')
197
- return plugin_fqcn, plugin_type, content_link, content, value
198
-
199
-
200
- def _check_sem_option_return_value(directive, content):
201
- content = _check_sem_quoting(directive, content)
202
- _parse_prefix(directive, content)
96
+ return []
97
+ except ValueError:
98
+ return [_add_ansible_error_code(
99
+ Invalid('Directive %s must contain a valid URL' % directive), 'invalid-documentation-markup')]
203
100
 
204
101
 
205
102
  def doc_string(v):
@@ -207,35 +104,55 @@ def doc_string(v):
207
104
  if not isinstance(v, string_types):
208
105
  raise _add_ansible_error_code(
209
106
  Invalid('Must be a string'), 'invalid-documentation')
210
- for m in _MODULE.finditer(v):
211
- _check_module_link(m.group(0), m.group(1))
212
- for m in _PLUGIN.finditer(v):
213
- _check_plugin_link(m.group(0), m.group(1))
214
- for m in _LINK.finditer(v):
215
- _check_link(m.group(0), m.group(1))
216
- for m in _URL.finditer(v):
217
- _check_url(m.group(0), m.group(1))
218
- for m in _REF.finditer(v):
219
- _check_ref(m.group(0), m.group(1))
220
- for m in _SEM_OPTION_NAME.finditer(v):
221
- _check_sem_option_return_value(m.group(0), m.group(1))
222
- for m in _SEM_OPTION_VALUE.finditer(v):
223
- _check_sem_quoting(m.group(0), m.group(1))
224
- for m in _SEM_ENV_VARIABLE.finditer(v):
225
- _check_sem_quoting(m.group(0), m.group(1))
226
- for m in _SEM_RET_VALUE.finditer(v):
227
- _check_sem_option_return_value(m.group(0), m.group(1))
107
+ errors = []
108
+ for par in parse(v, Context(), errors='message', strict=True, add_source=True):
109
+ for part in par:
110
+ if part.type == dom.PartType.ERROR:
111
+ errors.append(_add_ansible_error_code(Invalid(part.message), 'invalid-documentation-markup'))
112
+ if part.type == dom.PartType.URL:
113
+ errors.extend(_check_url('U()', part.url))
114
+ if part.type == dom.PartType.LINK:
115
+ errors.extend(_check_url('L()', part.url))
116
+ if part.type == dom.PartType.MODULE:
117
+ if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.fqcn):
118
+ errors.append(_add_ansible_error_code(Invalid(
119
+ 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.fqcn)),
120
+ 'invalid-documentation-markup'))
121
+ if part.type == dom.PartType.PLUGIN:
122
+ if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn):
123
+ errors.append(_add_ansible_error_code(Invalid(
124
+ 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)),
125
+ 'invalid-documentation-markup'))
126
+ if part.plugin.type not in _VALID_PLUGIN_TYPES:
127
+ errors.append(_add_ansible_error_code(Invalid(
128
+ 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)),
129
+ 'invalid-documentation-markup'))
130
+ if part.type == dom.PartType.OPTION_NAME:
131
+ if part.plugin is not None and not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn):
132
+ errors.append(_add_ansible_error_code(Invalid(
133
+ 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)),
134
+ 'invalid-documentation-markup'))
135
+ if part.plugin is not None and part.plugin.type not in _VALID_PLUGIN_TYPES:
136
+ errors.append(_add_ansible_error_code(Invalid(
137
+ 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)),
138
+ 'invalid-documentation-markup'))
139
+ if part.type == dom.PartType.RETURN_VALUE:
140
+ if part.plugin is not None and not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn):
141
+ errors.append(_add_ansible_error_code(Invalid(
142
+ 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)),
143
+ 'invalid-documentation-markup'))
144
+ if part.plugin is not None and part.plugin.type not in _VALID_PLUGIN_TYPES:
145
+ errors.append(_add_ansible_error_code(Invalid(
146
+ 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)),
147
+ 'invalid-documentation-markup'))
148
+ if len(errors) == 1:
149
+ raise errors[0]
150
+ if errors:
151
+ raise MultipleInvalid(errors)
228
152
  return v
229
153
 
230
154
 
231
- def doc_string_or_strings(v):
232
- """Match a documentation string, or list of strings."""
233
- if isinstance(v, string_types):
234
- return doc_string(v)
235
- if isinstance(v, (list, tuple)):
236
- return [doc_string(vv) for vv in v]
237
- raise _add_ansible_error_code(
238
- Invalid('Must be a string or list of strings'), 'invalid-documentation')
155
+ doc_string_or_strings = Any(doc_string, [doc_string])
239
156
 
240
157
 
241
158
  def is_callable(v):