Version source tagging (automatic and manual) for version_added and deprecation versions (#69680)

* Track collection for version_added.
Validate *all* version numbers in validate-modules.
For tagged version numbers (i.e. version_added), consider source collection to chose validation.

* Make tagging/untagging functions more flexible.

* Tag all versions in doc fragments.

* Tag all deprecation versions issued by code.

* Make Display.deprecated() understand tagged versions.

* Extend validation to enforce tagged version numbers.

* Tag versions in tests.

* Lint and fix test.

* Mention collection name in collection loader's deprecation/removal messages.

* Fix error IDs.

* Handle tagged dates in Display.deprecated().

* Also require that removed_at_date and deprecated_aliases.date are tagged.

* Also automatically tag/untag removed_at_date; fix sanity module removal version check.

* Improve error message when invalid version number is used (like '2.14' in collections).
This commit is contained in:
Felix Fontein 2020-05-29 07:46:16 +02:00 committed by GitHub
parent f5f3ba7ab5
commit 40f21dfd3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 601 additions and 302 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- "ansible-doc - now indicates if an option is added by a doc fragment from another collection by prepending the collection name, or ``ansible.builtin`` for ansible-base, to the version number."

View file

@ -0,0 +1,2 @@
minor_changes:
- "ansible-test - ``validate-modules`` now validates all version numbers in documentation and argument spec. Version numbers for collections are checked for being valid semantic versioning version number strings."

View file

@ -95,8 +95,10 @@ Codes
invalid-extension Naming Error Official Ansible modules must have a ``.py`` extension for python modules or a ``.ps1`` for powershell modules invalid-extension Naming Error Official Ansible modules must have a ``.py`` extension for python modules or a ``.ps1`` for powershell modules
invalid-metadata-status Documentation Error ``ANSIBLE_METADATA.status`` of deprecated or removed can't include other statuses invalid-metadata-status Documentation Error ``ANSIBLE_METADATA.status`` of deprecated or removed can't include other statuses
invalid-metadata-type Documentation Error ``ANSIBLE_METADATA`` was not provided as a dict, YAML not supported, Invalid ``ANSIBLE_METADATA`` schema invalid-metadata-type Documentation Error ``ANSIBLE_METADATA`` was not provided as a dict, YAML not supported, Invalid ``ANSIBLE_METADATA`` schema
invalid-module-deprecation-source Documentation Error The deprecated version for the module must not be from a documentation fragment from another collection or Ansible-base
invalid-module-schema Documentation Error ``AnsibleModule`` schema validation error invalid-module-schema Documentation Error ``AnsibleModule`` schema validation error
invalid-requires-extension Naming Error Module ``#AnsibleRequires -CSharpUtil`` should not end in .cs, Module ``#Requires`` should not end in .psm1 invalid-requires-extension Naming Error Module ``#AnsibleRequires -CSharpUtil`` should not end in .cs, Module ``#Requires`` should not end in .psm1
invalid-tagged-version Documentation Error All version numbers specified in code have to be explicitly tagged with the collection name, i.e. ``community.general:1.2.3`` or ``ansible.builtin:2.10``
last-line-main-call Syntax Error Call to ``main()`` not the last line (or ``removed_module()`` in the case of deprecated & docs only modules) last-line-main-call Syntax Error Call to ``main()`` not the last line (or ``removed_module()`` in the case of deprecated & docs only modules)
metadata-changed Documentation Error ``ANSIBLE_METADATA`` cannot be changed in a point release for a stable branch metadata-changed Documentation Error ``ANSIBLE_METADATA`` cannot be changed in a point release for a stable branch
missing-doc-fragment Documentation Error ``DOCUMENTATION`` fragment missing missing-doc-fragment Documentation Error ``DOCUMENTATION`` fragment missing

View file

@ -353,7 +353,7 @@ class CLI(with_metaclass(ABCMeta, object)):
verbosity_arg = next(iter([arg for arg in self.args if arg.startswith('-v')]), None) verbosity_arg = next(iter([arg for arg in self.args if arg.startswith('-v')]), None)
if verbosity_arg: if verbosity_arg:
display.deprecated("Setting verbosity before the arg sub command is deprecated, set the verbosity " display.deprecated("Setting verbosity before the arg sub command is deprecated, set the verbosity "
"after the sub command", "2.13") "after the sub command", "ansible.builtin:2.13")
options.verbosity = verbosity_arg.count('v') options.verbosity = verbosity_arg.count('v')
return options return options

View file

@ -397,7 +397,7 @@ class ConsoleCLI(CLI, cmd.Cmd):
def module_args(self, module_name): def module_args(self, module_name):
in_path = module_loader.find_plugin(module_name) in_path = module_loader.find_plugin(module_name)
oc, a, _, _ = plugin_docs.get_docstring(in_path, fragment_loader) oc, a, _, _ = plugin_docs.get_docstring(in_path, fragment_loader, is_module=True)
return list(oc['options'].keys()) return list(oc['options'].keys())
def run(self): def run(self):

View file

@ -31,7 +31,7 @@ from ansible.plugins.loader import action_loader, fragment_loader
from ansible.utils.collection_loader import AnsibleCollectionConfig from ansible.utils.collection_loader import AnsibleCollectionConfig
from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path
from ansible.utils.display import Display from ansible.utils.display import Display
from ansible.utils.plugin_docs import BLACKLIST, get_docstring, get_versioned_doclink from ansible.utils.plugin_docs import BLACKLIST, untag_versions_and_dates, get_docstring, get_versioned_doclink
display = Display() display = Display()
@ -218,7 +218,7 @@ class DocCLI(CLI):
plugin_docs = {} plugin_docs = {}
for plugin in context.CLIARGS['args']: for plugin in context.CLIARGS['args']:
try: try:
doc, plainexamples, returndocs, metadata = DocCLI._get_plugin_doc(plugin, loader, search_paths) doc, plainexamples, returndocs, metadata = DocCLI._get_plugin_doc(plugin, plugin_type, loader, search_paths)
except PluginNotFound: except PluginNotFound:
display.warning("%s %s not found in:\n%s\n" % (plugin_type, plugin, search_paths)) display.warning("%s %s not found in:\n%s\n" % (plugin_type, plugin, search_paths))
continue continue
@ -281,8 +281,13 @@ class DocCLI(CLI):
if filename is None: if filename is None:
raise AnsibleError("unable to load {0} plugin named {1} ".format(plugin_type, plugin_name)) raise AnsibleError("unable to load {0} plugin named {1} ".format(plugin_type, plugin_name))
collection_name = 'ansible.builtin'
if plugin_name.startswith('ansible_collections.'):
collection_name = '.'.join(plugin_name.split('.')[1:3])
try: try:
doc, __, __, metadata = get_docstring(filename, fragment_loader, verbose=(context.CLIARGS['verbosity'] > 0)) doc, __, __, metadata = get_docstring(filename, fragment_loader, verbose=(context.CLIARGS['verbosity'] > 0),
collection_name=collection_name, is_module=(plugin_type == 'module'))
except Exception: except Exception:
display.vvv(traceback.format_exc()) display.vvv(traceback.format_exc())
raise AnsibleError( raise AnsibleError(
@ -319,13 +324,20 @@ class DocCLI(CLI):
return clean_ns return clean_ns
@staticmethod @staticmethod
def _get_plugin_doc(plugin, loader, search_paths): def _get_plugin_doc(plugin, plugin_type, loader, search_paths):
# if the plugin lives in a non-python file (eg, win_X.ps1), require the corresponding python file for docs # if the plugin lives in a non-python file (eg, win_X.ps1), require the corresponding python file for docs
filename = loader.find_plugin(plugin, mod_type='.py', ignore_deprecated=True, check_aliases=True) result = loader.find_plugin_with_context(plugin, mod_type='.py', ignore_deprecated=True, check_aliases=True)
if filename is None: if result is None:
raise PluginNotFound('%s was not found in %s' % (plugin, search_paths)) raise PluginNotFound('%s was not found in %s' % (plugin, search_paths))
plugin_name, filename = result.plugin_resolved_name, result.plugin_resolved_path
doc, plainexamples, returndocs, metadata = get_docstring(filename, fragment_loader, verbose=(context.CLIARGS['verbosity'] > 0)) collection_name = 'ansible.builtin'
if plugin_name.startswith('ansible_collections.'):
collection_name = '.'.join(plugin_name.split('.')[1:3])
doc, plainexamples, returndocs, metadata = get_docstring(
filename, fragment_loader, verbose=(context.CLIARGS['verbosity'] > 0),
collection_name=collection_name, is_module=(plugin_type == 'module'))
# If the plugin existed but did not have a DOCUMENTATION element and was not removed, it's # If the plugin existed but did not have a DOCUMENTATION element and was not removed, it's
# an error # an error
@ -346,6 +358,7 @@ class DocCLI(CLI):
raise ValueError('%s did not contain a DOCUMENTATION attribute' % plugin) raise ValueError('%s did not contain a DOCUMENTATION attribute' % plugin)
doc['filename'] = filename doc['filename'] = filename
untag_versions_and_dates(doc, '%s:' % (collection_name, ), is_module=(plugin_type == 'module'))
return doc, plainexamples, returndocs, metadata return doc, plainexamples, returndocs, metadata
@staticmethod @staticmethod

View file

@ -487,7 +487,7 @@ class TaskExecutor:
'Invoking "%s" only once while using a loop via squash_actions is deprecated. ' 'Invoking "%s" only once while using a loop via squash_actions is deprecated. '
'Instead of using a loop to supply multiple items and specifying `%s: "%s"`, ' 'Instead of using a loop to supply multiple items and specifying `%s: "%s"`, '
'please use `%s: %s` and remove the loop' % (self._task.action, found, name, found, value_text), 'please use `%s: %s` and remove the loop' % (self._task.action, found, name, found, value_text),
version='2.11' version='ansible.builtin:2.11'
) )
for item in items: for item in items:
variables[loop_var] = item variables[loop_var] = item

View file

@ -1528,7 +1528,8 @@ def url_argument_spec():
''' '''
return dict( return dict(
url=dict(type='str'), url=dict(type='str'),
force=dict(type='bool', default=False, aliases=['thirsty'], deprecated_aliases=[dict(name='thirsty', version='2.13')]), force=dict(type='bool', default=False, aliases=['thirsty'],
deprecated_aliases=[dict(name='thirsty', version='ansible.builtin:2.13')]),
http_agent=dict(type='str', default='ansible-httpget'), http_agent=dict(type='str', default='ansible-httpget'),
use_proxy=dict(type='bool', default=True), use_proxy=dict(type='bool', default=True),
validate_certs=dict(type='bool', default=True), validate_certs=dict(type='bool', default=True),

View file

@ -517,7 +517,7 @@ def main():
) )
if module.params.get('thirsty'): if module.params.get('thirsty'):
module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='2.13') module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='ansible.builtin:2.13')
src = module.params['src'] src = module.params['src']
b_src = to_bytes(src, errors='surrogate_or_strict') b_src = to_bytes(src, errors='surrogate_or_strict')

View file

@ -621,12 +621,12 @@ def main():
if not name: if not name:
module.deprecate( module.deprecate(
msg="The 'name' parameter will be required in future releases.", msg="The 'name' parameter will be required in future releases.",
version='2.12' version='ansible.builtin:2.12'
) )
if reboot: if reboot:
module.deprecate( module.deprecate(
msg="The 'reboot' parameter will be removed in future releases. Use 'special_time' option instead.", msg="The 'reboot' parameter will be removed in future releases. Use 'special_time' option instead.",
version='2.12' version='ansible.builtin:2.12'
) )
if module._diff: if module._diff:

View file

@ -449,10 +449,10 @@ def main():
) )
if module.params.get('thirsty'): if module.params.get('thirsty'):
module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='2.13') module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='ansible.builtin:2.13')
if module.params.get('sha256sum'): if module.params.get('sha256sum'):
module.deprecate('The parameter "sha256sum" has been deprecated and will be removed, use "checksum" instead', version='2.14') module.deprecate('The parameter "sha256sum" has been deprecated and will be removed, use "checksum" instead', version='ansible.builtin:2.14')
url = module.params['url'] url = module.params['url']
dest = module.params['dest'] dest = module.params['dest']

View file

@ -360,7 +360,7 @@ def main():
''' Set CLI options depending on params ''' ''' Set CLI options depending on params '''
if module.params['user'] is not None: if module.params['user'] is not None:
# handle user deprecation, mutually exclusive with scope # handle user deprecation, mutually exclusive with scope
module.deprecate("The 'user' option is being replaced by 'scope'", version='2.11') module.deprecate("The 'user' option is being replaced by 'scope'", version='ansible.builtin:2.11')
if module.params['user']: if module.params['user']:
module.params['scope'] = 'user' module.params['scope'] = 'user'
else: else:

View file

@ -615,7 +615,7 @@ def main():
) )
if module.params.get('thirsty'): if module.params.get('thirsty'):
module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='2.13') module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='ansible.builtin:2.13')
url = module.params['url'] url = module.params['url']
body = module.params['body'] body = module.params['body']

View file

@ -79,7 +79,7 @@ class Playbook:
self._loader.set_basedir(cur_basedir) self._loader.set_basedir(cur_basedir)
raise AnsibleParserError("A playbook must be a list of plays, got a %s instead" % type(ds), obj=ds) raise AnsibleParserError("A playbook must be a list of plays, got a %s instead" % type(ds), obj=ds)
elif not ds: elif not ds:
display.deprecated("Empty plays will currently be skipped, in the future they will cause a syntax error", version='2.12') display.deprecated("Empty plays will currently be skipped, in the future they will cause a syntax error", version='ansible.builtin:2.12')
# Parse the playbook entries. For plays, we simply parse them # Parse the playbook entries. For plays, we simply parse them
# using the Play() object, and includes are parsed using the # using the Play() object, and includes are parsed using the
@ -92,7 +92,7 @@ class Playbook:
if any(action in entry for action in ('import_playbook', 'include')): if any(action in entry for action in ('import_playbook', 'include')):
if 'include' in entry: if 'include' in entry:
display.deprecated("'include' for playbook includes. You should use 'import_playbook' instead", version="2.12") display.deprecated("'include' for playbook includes. You should use 'import_playbook' instead", version="ansible.builtin:2.12")
pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader) pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader)
if pb is not None: if pb is not None:
self._entries.extend(pb._entries) self._entries.extend(pb._entries)

View file

@ -134,7 +134,7 @@ class Conditional:
conditional = templar.template(conditional, disable_lookups=disable_lookups) conditional = templar.template(conditional, disable_lookups=disable_lookups)
if bare_vars_warning and not isinstance(conditional, bool): if bare_vars_warning and not isinstance(conditional, bool):
display.deprecated('evaluating %r as a bare variable, this behaviour will go away and you might need to add |bool' display.deprecated('evaluating %r as a bare variable, this behaviour will go away and you might need to add |bool'
' to the expression in the future. Also see CONDITIONAL_BARE_VARS configuration toggle' % original, "2.12") ' to the expression in the future. Also see CONDITIONAL_BARE_VARS configuration toggle' % original, "ansible.builtin:2.12")
if not isinstance(conditional, text_type) or conditional == "": if not isinstance(conditional, text_type) or conditional == "":
return conditional return conditional

View file

@ -156,7 +156,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
is_static = True is_static = True
elif t.static is not None: elif t.static is not None:
display.deprecated("The use of 'static' has been deprecated. " display.deprecated("The use of 'static' has been deprecated. "
"Use 'import_tasks' for static inclusion, or 'include_tasks' for dynamic inclusion", version='2.12') "Use 'import_tasks' for static inclusion, or 'include_tasks' for dynamic inclusion", version='ansible.builtin:2.12')
is_static = t.static is_static = t.static
else: else:
is_static = C.DEFAULT_TASK_INCLUDES_STATIC or \ is_static = C.DEFAULT_TASK_INCLUDES_STATIC or \
@ -257,7 +257,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
"later. In the future, this will be an error unless 'static: no' is used " "later. In the future, this will be an error unless 'static: no' is used "
"on the include task. If you do not want missing includes to be considered " "on the include task. If you do not want missing includes to be considered "
"dynamic, use 'static: yes' on the include or set the global ansible.cfg " "dynamic, use 'static: yes' on the include or set the global ansible.cfg "
"options to make all includes static for tasks and/or handlers" % include_file, version="2.12" "options to make all includes static for tasks and/or handlers" % include_file, version="ansible.builtin:2.12"
) )
task_list.append(t) task_list.append(t)
continue continue
@ -294,7 +294,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
suppress_extended_error=True, suppress_extended_error=True,
) )
display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option", display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option",
version="2.12") version="ansible.builtin:2.12")
else: else:
tags = ti_copy.tags[:] tags = ti_copy.tags[:]
@ -332,7 +332,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
elif ir.static is not None: elif ir.static is not None:
display.deprecated("The use of 'static' for 'include_role' has been deprecated. " display.deprecated("The use of 'static' for 'include_role' has been deprecated. "
"Use 'import_role' for static inclusion, or 'include_role' for dynamic inclusion", version='2.12') "Use 'import_role' for static inclusion, or 'include_role' for dynamic inclusion", version='ansible.builtin:2.12')
is_static = ir.static is_static = ir.static
if is_static: if is_static:

View file

@ -342,7 +342,7 @@ class PlayContext(Base):
""" helper function to create privilege escalation commands """ """ helper function to create privilege escalation commands """
display.deprecated( display.deprecated(
"PlayContext.make_become_cmd should not be used, the calling code should be using become plugins instead", "PlayContext.make_become_cmd should not be used, the calling code should be using become plugins instead",
version="2.12" version="ansible.builtin:2.12"
) )
if not cmd or not self.become: if not cmd or not self.become:

View file

@ -163,7 +163,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
raise AnsibleError("you must specify a value when using %s" % k, obj=ds) raise AnsibleError("you must specify a value when using %s" % k, obj=ds)
new_ds['loop_with'] = loop_name new_ds['loop_with'] = loop_name
new_ds['loop'] = v new_ds['loop'] = v
# display.deprecated("with_ type loops are being phased out, use the 'loop' keyword instead", version="2.10") # display.deprecated("with_ type loops are being phased out, use the 'loop' keyword instead", version="ansible.builtin:2.10")
def preprocess_data(self, ds): def preprocess_data(self, ds):
''' '''
@ -258,7 +258,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
if action in ('include',) and k not in self._valid_attrs and k not in self.DEPRECATED_ATTRIBUTES: if action in ('include',) and k not in self._valid_attrs and k not in self.DEPRECATED_ATTRIBUTES:
display.deprecated("Specifying include variables at the top-level of the task is deprecated." display.deprecated("Specifying include variables at the top-level of the task is deprecated."
" Please see:\nhttps://docs.ansible.com/ansible/playbooks_roles.html#task-include-files-and-encouraging-reuse\n\n" " Please see:\nhttps://docs.ansible.com/ansible/playbooks_roles.html#task-include-files-and-encouraging-reuse\n\n"
" for currently supported syntax regarding included files and variables", version="2.12") " for currently supported syntax regarding included files and variables", version="ansible.builtin:2.12")
new_ds['vars'][k] = v new_ds['vars'][k] = v
elif C.INVALID_TASK_ATTRIBUTE_FAILED or k in self._valid_attrs: elif C.INVALID_TASK_ATTRIBUTE_FAILED or k in self._valid_attrs:
new_ds[k] = v new_ds[k] = v

View file

@ -822,7 +822,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
msg = "Setting the async dir from the environment keyword " \ msg = "Setting the async dir from the environment keyword " \
"ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \ "ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \
"shell option instead" "shell option instead"
self._display.deprecated(msg, "2.12") self._display.deprecated(msg, "ansible.builtin:2.12")
else: else:
# ANSIBLE_ASYNC_DIR is not set on the task, we get the value # ANSIBLE_ASYNC_DIR is not set on the task, we get the value
# from the shell option and temporarily add to the environment # from the shell option and temporarily add to the environment

View file

@ -33,7 +33,7 @@ class ActionModule(ActionBase):
msg = "Setting the async dir from the environment keyword " \ msg = "Setting the async dir from the environment keyword " \
"ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \ "ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \
"shell option instead" "shell option instead"
self._display.deprecated(msg, "2.12") self._display.deprecated(msg, "ansible.builtin:2.12")
else: else:
# inject the async directory based on the shell option into the # inject the async directory based on the shell option into the
# module args # module args

View file

@ -50,7 +50,7 @@ class FactCache(RealFactCache):
' ansible.vars.fact_cache.FactCache. If you are looking for the class' ' ansible.vars.fact_cache.FactCache. If you are looking for the class'
' to subclass for a cache plugin, you want' ' to subclass for a cache plugin, you want'
' ansible.plugins.cache.BaseCacheModule or one of its subclasses.', ' ansible.plugins.cache.BaseCacheModule or one of its subclasses.',
version='2.12') version='ansible.builtin:2.12')
super(FactCache, self).__init__(*args, **kwargs) super(FactCache, self).__init__(*args, **kwargs)
@ -62,7 +62,7 @@ class BaseCacheModule(AnsiblePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Third party code is not using cache_loader to load plugin - fall back to previous behavior # Third party code is not using cache_loader to load plugin - fall back to previous behavior
if not hasattr(self, '_load_name'): if not hasattr(self, '_load_name'):
display.deprecated('Rather than importing custom CacheModules directly, use ansible.plugins.loader.cache_loader', version='2.14') display.deprecated('Rather than importing custom CacheModules directly, use ansible.plugins.loader.cache_loader', version='ansible.builtin:2.14')
self._load_name = self.__module__.split('.')[-1] self._load_name = self.__module__.split('.')[-1]
self._load_name = resource_from_fqcr(self.__module__) self._load_name = resource_from_fqcr(self.__module__)
super(BaseCacheModule, self).__init__() super(BaseCacheModule, self).__init__()

View file

@ -242,7 +242,7 @@ class CallbackBase(AnsiblePlugin):
def _get_item(self, result): def _get_item(self, result):
''' here for backwards compat, really should have always been named: _get_item_label''' ''' here for backwards compat, really should have always been named: _get_item_label'''
cback = getattr(self, 'NAME', os.path.basename(__file__)) cback = getattr(self, 'NAME', os.path.basename(__file__))
self._display.deprecated("The %s callback plugin should be updated to use the _get_item_label method instead" % cback, version="2.11") self._display.deprecated("The %s callback plugin should be updated to use the _get_item_label method instead" % cback, version="ansible.builtin:2.11")
return self._get_item_label(result) return self._get_item_label(result)
def _process_items(self, result): def _process_items(self, result):

View file

@ -237,28 +237,28 @@ class ConnectionBase(AnsiblePlugin):
def check_become_success(self, b_output): def check_become_success(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_become_success is deprecated, calling code should be using become plugins instead", "Connection.check_become_success is deprecated, calling code should be using become plugins instead",
version="2.12" version="ansible.builtin:2.12"
) )
return self.become.check_success(b_output) return self.become.check_success(b_output)
def check_password_prompt(self, b_output): def check_password_prompt(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_password_prompt is deprecated, calling code should be using become plugins instead", "Connection.check_password_prompt is deprecated, calling code should be using become plugins instead",
version="2.12" version="ansible.builtin:2.12"
) )
return self.become.check_password_prompt(b_output) return self.become.check_password_prompt(b_output)
def check_incorrect_password(self, b_output): def check_incorrect_password(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_incorrect_password is deprecated, calling code should be using become plugins instead", "Connection.check_incorrect_password is deprecated, calling code should be using become plugins instead",
version="2.12" version="ansible.builtin:2.12"
) )
return self.become.check_incorrect_password(b_output) return self.become.check_incorrect_password(b_output)
def check_missing_password(self, b_output): def check_missing_password(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_missing_password is deprecated, calling code should be using become plugins instead", "Connection.check_missing_password is deprecated, calling code should be using become plugins instead",
version="2.12" version="ansible.builtin:2.12"
) )
return self.become.check_missing_password(b_output) return self.become.check_missing_password(b_output)

View file

@ -291,19 +291,19 @@ class DeprecatedCache(object):
display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. ' display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. '
'When expecting a KeyError, use self._cache[key] instead of using self.cache.get(key). ' 'When expecting a KeyError, use self._cache[key] instead of using self.cache.get(key). '
'self._cache is a dictionary and will return a default value instead of raising a KeyError ' 'self._cache is a dictionary and will return a default value instead of raising a KeyError '
'when the key does not exist', version='2.12') 'when the key does not exist', version='ansible.builtin:2.12')
return self.real_cacheable._cache[key] return self.real_cacheable._cache[key]
def set(self, key, value): def set(self, key, value):
display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. ' display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. '
'To set the self._cache dictionary, use self._cache[key] = value instead of self.cache.set(key, value). ' 'To set the self._cache dictionary, use self._cache[key] = value instead of self.cache.set(key, value). '
'To force update the underlying cache plugin with the contents of self._cache before parse() is complete, ' 'To force update the underlying cache plugin with the contents of self._cache before parse() is complete, '
'call self.set_cache_plugin and it will use the self._cache dictionary to update the cache plugin', version='2.12') 'call self.set_cache_plugin and it will use the self._cache dictionary to update the cache plugin', version='ansible.builtin:2.12')
self.real_cacheable._cache[key] = value self.real_cacheable._cache[key] = value
self.real_cacheable.set_cache_plugin() self.real_cacheable.set_cache_plugin()
def __getattr__(self, name): def __getattr__(self, name):
display.deprecated('InventoryModule should utilize self._cache instead of self.cache', version='2.12') display.deprecated('InventoryModule should utilize self._cache instead of self.cache', version='ansible.builtin:2.12')
return self.real_cacheable._cache.__getattribute__(name) return self.real_cacheable._cache.__getattribute__(name)

View file

@ -97,7 +97,7 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
display.deprecated( display.deprecated(
msg="The 'cache' option is deprecated for the script inventory plugin. " msg="The 'cache' option is deprecated for the script inventory plugin. "
"External scripts implement their own caching and this option has never been used", "External scripts implement their own caching and this option has never been used",
version="2.12" version="ansible.builtin:2.12"
) )
# Support inventory scripts that are not prefixed with some # Support inventory scripts that are not prefixed with some

View file

@ -130,7 +130,7 @@ class PluginLoadContext(object):
self.deprecation_warnings = [] self.deprecation_warnings = []
self.resolved = False self.resolved = False
def record_deprecation(self, name, deprecation): def record_deprecation(self, name, deprecation, collection_name):
if not deprecation: if not deprecation:
return self return self
@ -142,11 +142,14 @@ class PluginLoadContext(object):
removal_version = None removal_version = None
if not warning_text: if not warning_text:
if removal_date: if removal_date:
warning_text = '{0} has been deprecated and will be removed in a release after {1}'.format(name, removal_date) warning_text = '{0} has been deprecated and will be removed in a release of {2} after {1}'.format(
name, removal_date, collection_name)
elif removal_version: elif removal_version:
warning_text = '{0} has been deprecated and will be removed in version {1}'.format(name, removal_version) warning_text = '{0} has been deprecated and will be removed in version {1} of {2}'.format(
name, removal_version, collection_name)
else: else:
warning_text = '{0} has been deprecated and will be removed in a future release'.format(name) warning_text = '{0} has been deprecated and will be removed in a future release of {2}'.format(
name, collection_name)
self.deprecated = True self.deprecated = True
if removal_date: if removal_date:
@ -374,7 +377,7 @@ class PluginLoader:
if type_name in C.CONFIGURABLE_PLUGINS: if type_name in C.CONFIGURABLE_PLUGINS:
dstring = AnsibleLoader(getattr(module, 'DOCUMENTATION', ''), file_name=path).get_single_data() dstring = AnsibleLoader(getattr(module, 'DOCUMENTATION', ''), file_name=path).get_single_data()
if dstring: if dstring:
add_fragments(dstring, path, fragment_loader=fragment_loader) add_fragments(dstring, path, fragment_loader=fragment_loader, is_module=(type_name == 'module'))
if dstring and 'options' in dstring and isinstance(dstring['options'], dict): if dstring and 'options' in dstring and isinstance(dstring['options'], dict):
C.config.initialize_plugin_configuration_definitions(type_name, name, dstring['options']) C.config.initialize_plugin_configuration_definitions(type_name, name, dstring['options'])
@ -444,7 +447,7 @@ class PluginLoader:
deprecation = routing_metadata.get('deprecation', None) deprecation = routing_metadata.get('deprecation', None)
# this will no-op if there's no deprecation metadata for this plugin # this will no-op if there's no deprecation metadata for this plugin
plugin_load_context.record_deprecation(fq_name, deprecation) plugin_load_context.record_deprecation(fq_name, deprecation, acr.collection)
tombstone = routing_metadata.get('tombstone', None) tombstone = routing_metadata.get('tombstone', None)
@ -453,12 +456,12 @@ class PluginLoader:
removal_date = tombstone.get('removal_date') removal_date = tombstone.get('removal_date')
removal_version = tombstone.get('removal_version') removal_version = tombstone.get('removal_version')
if removal_date: if removal_date:
removed_msg = '{0} was removed on {1}'.format(fq_name, removal_date) removed_msg = '{0} was removed from {2} on {1}'.format(fq_name, removal_date, acr.collection)
removal_version = None removal_version = None
elif removal_version: elif removal_version:
removed_msg = '{0} was removed in version {1}'.format(fq_name, removal_version) removed_msg = '{0} was removed in version {1} of {2}'.format(fq_name, removal_version, acr.collection)
else: else:
removed_msg = '{0} was removed in a previous release'.format(fq_name) removed_msg = '{0} was removed in a previous release of {1}'.format(fq_name, acr.collection)
plugin_load_context.removal_date = removal_date plugin_load_context.removal_date = removal_date
plugin_load_context.removal_version = removal_version plugin_load_context.removal_version = removal_version
plugin_load_context.resolved = True plugin_load_context.resolved = True

View file

@ -71,7 +71,7 @@ def SharedPluginLoaderObj():
'''This only exists for backwards compat, do not use. '''This only exists for backwards compat, do not use.
''' '''
display.deprecated('SharedPluginLoaderObj is deprecated, please directly use ansible.plugins.loader', display.deprecated('SharedPluginLoaderObj is deprecated, please directly use ansible.plugins.loader',
version='2.11') version='ansible.builtin:2.11')
return plugin_loader return plugin_loader
@ -905,7 +905,7 @@ class StrategyBase:
"Mixing tag specify styles is prohibited for whole import hierarchy, not only for single import statement", "Mixing tag specify styles is prohibited for whole import hierarchy, not only for single import statement",
obj=included_file._task._ds) obj=included_file._task._ds)
display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option", display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option",
version='2.12') version='ansible.builtin:2.12')
included_file._task.tags = tags included_file._task.tags = tags
block_list = load_list_of_blocks( block_list = load_list_of_blocks(

View file

@ -574,7 +574,7 @@ class Templar:
def set_available_variables(self, variables): def set_available_variables(self, variables):
display.deprecated( display.deprecated(
'set_available_variables is being deprecated. Use "@available_variables.setter" instead.', 'set_available_variables is being deprecated. Use "@available_variables.setter" instead.',
version='2.13' version='ansible.builtin:2.13'
) )
self.available_variables = variables self.available_variables = variables

View file

@ -26,6 +26,7 @@ import locale
import logging import logging
import os import os
import random import random
import re
import subprocess import subprocess
import sys import sys
import textwrap import textwrap
@ -36,8 +37,8 @@ from termios import TIOCGWINSZ
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleError, AnsibleAssertionError from ansible.errors import AnsibleError, AnsibleAssertionError
from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils._text import to_bytes, to_text, to_native
from ansible.module_utils.six import with_metaclass from ansible.module_utils.six import with_metaclass, string_types
from ansible.utils.color import stringc from ansible.utils.color import stringc
from ansible.utils.singleton import Singleton from ansible.utils.singleton import Singleton
from ansible.utils.unsafe_proxy import wrap_var from ansible.utils.unsafe_proxy import wrap_var
@ -50,6 +51,9 @@ except NameError:
pass pass
TAGGED_VERSION_RE = re.compile('^([^.]+.[^.]+):(.*)$')
class FilterBlackList(logging.Filter): class FilterBlackList(logging.Filter):
def __init__(self, blacklist): def __init__(self, blacklist):
self.blacklist = [logging.Filter(name) for name in blacklist] self.blacklist = [logging.Filter(name) for name in blacklist]
@ -258,9 +262,34 @@ class Display(with_metaclass(Singleton, object)):
if not removed: if not removed:
if date: if date:
new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in a release after %s." % (msg, date) m = None
if isinstance(date, string_types):
version = to_native(date)
m = TAGGED_VERSION_RE.match(date)
if m:
collection = m.group(1)
date = m.group(2)
if collection == 'ansible.builtin':
collection = 'Ansible-base'
new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in a release of %s after %s." % (
msg, collection, date)
else:
new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in a release after %s." % (
msg, date)
elif version: elif version:
new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in version %s." % (msg, version) m = None
if isinstance(version, string_types):
version = to_native(version)
m = TAGGED_VERSION_RE.match(version)
if m:
collection = m.group(1)
version = m.group(2)
if collection == 'ansible.builtin':
collection = 'Ansible-base'
new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in version %s of %s." % (msg, version,
collection)
else:
new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in version %s." % (msg, version)
else: else:
new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in a future release." % (msg) new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in a future release." % (msg)
new_msg = new_msg + " Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.\n\n" new_msg = new_msg + " Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.\n\n"

View file

@ -40,7 +40,70 @@ def merge_fragment(target, source):
target[key] = value target[key] = value
def add_fragments(doc, filename, fragment_loader): def _process_versions_and_dates(fragment, is_module, callback):
def process_deprecation(deprecation):
if is_module and 'removed_in' in deprecation: # used in module deprecations
callback(deprecation, 'removed_in')
if 'removed_at_date' in deprecation:
callback(deprecation, 'removed_at_date')
if not is_module and 'version' in deprecation: # used in plugin option deprecations
callback(deprecation, 'version')
def process_option_specifiers(specifiers):
for specifier in specifiers:
if 'version_added' in specifier:
callback(specifier, 'version_added')
if isinstance(specifier.get('deprecated'), dict):
process_deprecation(specifier['deprecated'])
def process_options(options):
for option in options.values():
if 'version_added' in option:
callback(option, 'version_added')
if not is_module:
if isinstance(option.get('env'), list):
process_option_specifiers(option['env'])
if isinstance(option.get('ini'), list):
process_option_specifiers(option['ini'])
if isinstance(option.get('vars'), list):
process_option_specifiers(option['vars'])
if isinstance(option.get('suboptions'), dict):
process_options(option['suboptions'])
def process_return_values(return_values):
for return_value in return_values.values():
if 'version_added' in return_value:
callback(return_value, 'version_added')
if isinstance(return_value.get('contains'), dict):
process_return_values(return_value['contains'])
if 'version_added' in fragment:
callback(fragment, 'version_added')
if isinstance(fragment.get('deprecated'), dict):
process_deprecation(fragment['deprecated'])
if isinstance(fragment.get('options'), dict):
process_options(fragment['options'])
def tag_versions_and_dates(fragment, prefix, is_module):
def tag(options, option):
options[option] = '%s%s' % (prefix, options[option])
_process_versions_and_dates(fragment, is_module, tag)
def untag_versions_and_dates(fragment, prefix, is_module):
def untag(options, option):
v = options[option]
if isinstance(v, string_types):
v = to_native(v)
if v.startswith(prefix):
options[option] = v[len(prefix):]
_process_versions_and_dates(fragment, is_module, untag)
def add_fragments(doc, filename, fragment_loader, is_module=False):
fragments = doc.pop('extends_documentation_fragment', []) fragments = doc.pop('extends_documentation_fragment', [])
@ -80,6 +143,12 @@ def add_fragments(doc, filename, fragment_loader):
fragment = AnsibleLoader(fragment_yaml, file_name=filename).get_single_data() fragment = AnsibleLoader(fragment_yaml, file_name=filename).get_single_data()
real_collection_name = 'ansible.builtin'
real_fragment_name = getattr(fragment_class, '_load_name')
if real_fragment_name.startswith('ansible_collections.'):
real_collection_name = '.'.join(real_fragment_name.split('.')[1:3])
tag_versions_and_dates(fragment, '%s:' % (real_collection_name, ), is_module=is_module)
if 'notes' in fragment: if 'notes' in fragment:
notes = fragment.pop('notes') notes = fragment.pop('notes')
if notes: if notes:
@ -116,16 +185,20 @@ def add_fragments(doc, filename, fragment_loader):
raise AnsibleError('unknown doc_fragment(s) in file {0}: {1}'.format(filename, to_native(', '.join(unknown_fragments)))) raise AnsibleError('unknown doc_fragment(s) in file {0}: {1}'.format(filename, to_native(', '.join(unknown_fragments))))
def get_docstring(filename, fragment_loader, verbose=False, ignore_errors=False): def get_docstring(filename, fragment_loader, verbose=False, ignore_errors=False, collection_name=None, is_module=False):
""" """
DOCUMENTATION can be extended using documentation fragments loaded by the PluginLoader from the doc_fragments plugins. DOCUMENTATION can be extended using documentation fragments loaded by the PluginLoader from the doc_fragments plugins.
""" """
data = read_docstring(filename, verbose=verbose, ignore_errors=ignore_errors) data = read_docstring(filename, verbose=verbose, ignore_errors=ignore_errors)
# add fragments to documentation
if data.get('doc', False): if data.get('doc', False):
add_fragments(data['doc'], filename, fragment_loader=fragment_loader) # tag version_added
if collection_name is not None:
tag_versions_and_dates(data['doc'], '%s:' % (collection_name, ), is_module=is_module)
# add fragments to documentation
add_fragments(data['doc'], filename, fragment_loader=fragment_loader, is_module=is_module)
return data['doc'], data['plainexamples'], data['returndocs'], data['metadata'] return data['doc'], data['plainexamples'], data['returndocs'], data['metadata']

View file

@ -83,7 +83,7 @@ class UnsafeProxy(object):
from ansible.utils.display import Display from ansible.utils.display import Display
Display().deprecated( Display().deprecated(
'UnsafeProxy is being deprecated. Use wrap_var or AnsibleUnsafeBytes/AnsibleUnsafeText directly instead', 'UnsafeProxy is being deprecated. Use wrap_var or AnsibleUnsafeBytes/AnsibleUnsafeText directly instead',
version='2.13' version='ansible.builtin:2.13'
) )
# In our usage we should only receive unicode strings. # In our usage we should only receive unicode strings.
# This conditional and conversion exists to sanity check the values # This conditional and conversion exists to sanity check the values

View file

@ -99,7 +99,7 @@ class FactCache(MutableMapping):
display.deprecated('Calling FactCache().update(key, value) is deprecated. Use' display.deprecated('Calling FactCache().update(key, value) is deprecated. Use'
' FactCache().first_order_merge(key, value) if you want the old' ' FactCache().first_order_merge(key, value) if you want the old'
' behaviour or use FactCache().update({key: value}) if you want' ' behaviour or use FactCache().update({key: value}) if you want'
' dict-like behaviour.', version='2.12') ' dict-like behaviour.', version='ansible.builtin:2.12')
return self.first_order_merge(*args) return self.first_order_merge(*args)
elif len(args) == 1: elif len(args) == 1:

View file

@ -36,7 +36,7 @@
- get_element_attribute_wrong.matches[0]['rating']['subjective'] == 'true' - get_element_attribute_wrong.matches[0]['rating']['subjective'] == 'true'
- get_element_attribute_wrong.deprecations is defined - get_element_attribute_wrong.deprecations is defined
- get_element_attribute_wrong.deprecations[0].msg == "Parameter 'attribute=subjective' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry." - get_element_attribute_wrong.deprecations[0].msg == "Parameter 'attribute=subjective' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry."
- get_element_attribute_wrong.deprecations[0].version == '2.12' - get_element_attribute_wrong.deprecations[0].version == 'ansible.builtin:2.12'
- name: Get element text - name: Get element text
xml: xml:

View file

@ -35,7 +35,7 @@ MSGS = {
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"ansible-invalid-deprecated-version", "ansible-invalid-deprecated-version",
"Used when a call to Display.deprecated specifies an invalid " "Used when a call to Display.deprecated specifies an invalid "
"Ansible version number", "tagged Ansible version number",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9504': ("Deprecated version (%r) found in call to Display.deprecated " 'E9504': ("Deprecated version (%r) found in call to Display.deprecated "
"or AnsibleModule.deprecate", "or AnsibleModule.deprecate",
@ -48,30 +48,59 @@ MSGS = {
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"collection-invalid-deprecated-version", "collection-invalid-deprecated-version",
"Used when a call to Display.deprecated specifies an invalid " "Used when a call to Display.deprecated specifies an invalid "
"collection version number", "tagged collection version number",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9506': ("Expired date (%r) found in call to Display.deprecated " 'E9506': ("Invalid tagged version (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"invalid-tagged-version",
"Used when a call to Display.deprecated specifies a version "
"number which has no collection name tag, for example "
"`community.general:1.2.3` or `ansible.builtin:2.10`",
{'minversion': (2, 6)}),
'E9507': ("Version tag for wrong collection (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"wrong-collection-deprecated-version-tag",
"Deprecation versions must be prefixed with the name of this "
"collection (`ansible.builtin:` for Ansible-base)",
{'minversion': (2, 6)}),
'E9508': ("Expired date (%r) found in call to Display.deprecated "
"or AnsibleModule.deprecate", "or AnsibleModule.deprecate",
"ansible-deprecated-date", "ansible-deprecated-date",
"Used when a call to Display.deprecated specifies a date " "Used when a call to Display.deprecated specifies a date "
"before today", "before today",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9507': ("Invalid deprecated date (%r) found in call to " 'E9509': ("Invalid deprecated date (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"ansible-invalid-deprecated-date", "ansible-invalid-deprecated-date",
"Used when a call to Display.deprecated specifies an invalid " "Used when a call to Display.deprecated specifies an invalid "
"date. It must be a string in format YYYY-MM-DD (ISO 8601)", "date. It must be a string in format `namespace.name:YYYY-MM-DD` "
"(collection identifier followed by ISO 8601)",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9508': ("Both version and date found in call to " 'E9510': ("Both version and date found in call to "
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"ansible-deprecated-both-version-and-date", "ansible-deprecated-both-version-and-date",
"Only one of version and date must be specified", "Only one of version and date must be specified",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9511': ("Invalid tagged date (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"invalid-tagged-date",
"Used when a call to Display.deprecated specifies a date "
"which has no collection name tag, for example "
"`community.general:2020-01-01` or `ansible.builtin:2020-12-31`",
{'minversion': (2, 6)}),
'E9512': ("Date tag for wrong collection (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"wrong-collection-deprecated-date-tag",
"Deprecation dates must be prefixed with the name of this "
"collection (`ansible.builtin:` for Ansible-base)",
{'minversion': (2, 6)}),
} }
ANSIBLE_VERSION = LooseVersion('.'.join(ansible_version_raw.split('.')[:3])) ANSIBLE_VERSION = LooseVersion('.'.join(ansible_version_raw.split('.')[:3]))
TAGGED_VERSION_RE = re.compile('^([^.]+.[^.]+):(.*)$')
def _get_expr_name(node): def _get_expr_name(node):
"""Funciton to get either ``attrname`` or ``name`` from ``node.func.expr`` """Funciton to get either ``attrname`` or ``name`` from ``node.func.expr``
@ -109,11 +138,11 @@ class AnsibleDeprecatedChecker(BaseChecker):
msgs = MSGS msgs = MSGS
options = ( options = (
('is-collection', { ('collection-name', {
'default': False, 'default': None,
'type': 'yn', 'type': 'string',
'metavar': '<y_or_n>', 'metavar': '<name>',
'help': 'Whether this is a collection or not.', 'help': 'The collection\'s name used to check tagged version numbers in deprecations.',
}), }),
('collection-version', { ('collection-version', {
'default': None, 'default': None,
@ -125,17 +154,71 @@ class AnsibleDeprecatedChecker(BaseChecker):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.collection_version = None self.collection_version = None
self.is_collection = False self.collection_name = None
self.version_constructor = LooseVersion
super(AnsibleDeprecatedChecker, self).__init__(*args, **kwargs) super(AnsibleDeprecatedChecker, self).__init__(*args, **kwargs)
def set_option(self, optname, value, action=None, optdict=None): def set_option(self, optname, value, action=None, optdict=None):
super(AnsibleDeprecatedChecker, self).set_option(optname, value, action, optdict) super(AnsibleDeprecatedChecker, self).set_option(optname, value, action, optdict)
if optname == 'collection-version' and value is not None: if optname == 'collection-version' and value is not None:
self.version_constructor = SemanticVersion self.collection_version = SemanticVersion(self.config.collection_version)
self.collection_version = self.version_constructor(self.config.collection_version) if optname == 'collection-name' and value is not None:
if optname == 'is-collection': self.collection_name = self.config.collection_name
self.is_collection = self.config.is_collection
def _check_date(self, node, date):
if not isinstance(date, str):
self.add_message('invalid-tagged-date', node=node, args=(date,))
return
matcher = TAGGED_VERSION_RE.match(date)
if not matcher:
self.add_message('invalid-tagged-date', node=node, args=(date,))
return
collection = matcher.group(1)
date_str = matcher.group(2)
if collection != (self.collection_name or 'ansible.builtin'):
self.add_message('wrong-collection-deprecated-date-tag', node=node, args=(date,))
try:
if parse_isodate(date_str) < datetime.date.today():
self.add_message('ansible-deprecated-date', node=node, args=(date,))
except ValueError:
self.add_message('ansible-invalid-deprecated-date', node=node, args=(date,))
def _check_version(self, node, version):
if not isinstance(version, str):
self.add_message('invalid-tagged-version', node=node, args=(version,))
return
matcher = TAGGED_VERSION_RE.match(version)
if not matcher:
self.add_message('invalid-tagged-version', node=node, args=(version,))
return
collection = matcher.group(1)
version_no = matcher.group(2)
if collection != (self.collection_name or 'ansible.builtin'):
self.add_message('wrong-collection-deprecated-version-tag', node=node, args=(version,))
if collection == 'ansible.builtin':
# Ansible-base
try:
loose_version = LooseVersion(str(version_no))
if ANSIBLE_VERSION >= loose_version:
self.add_message('ansible-deprecated-version', node=node, args=(version,))
except ValueError:
self.add_message('ansible-invalid-deprecated-version', node=node, args=(version,))
else:
# Collections
try:
semantic_version = SemanticVersion(version_no)
if collection == self.collection_name and self.collection_version is not None:
if self.collection_version >= semantic_version:
self.add_message('collection-deprecated-version', node=node, args=(version,))
except ValueError:
self.add_message('collection-invalid-deprecated-version', node=node, args=(version,))
@check_messages(*(MSGS.keys())) @check_messages(*(MSGS.keys()))
def visit_call(self, node): def visit_call(self, node):
@ -169,26 +252,11 @@ class AnsibleDeprecatedChecker(BaseChecker):
self.add_message('ansible-deprecated-both-version-and-date', node=node) self.add_message('ansible-deprecated-both-version-and-date', node=node)
return return
if version:
try:
loose_version = self.version_constructor(str(version))
if self.is_collection and self.collection_version is not None:
if self.collection_version >= loose_version:
self.add_message('collection-deprecated-version', node=node, args=(version,))
if not self.is_collection and ANSIBLE_VERSION >= loose_version:
self.add_message('ansible-deprecated-version', node=node, args=(version,))
except ValueError:
if self.is_collection:
self.add_message('collection-invalid-deprecated-version', node=node, args=(version,))
else:
self.add_message('ansible-invalid-deprecated-version', node=node, args=(version,))
if date: if date:
try: self._check_date(node, date)
if parse_isodate(date) < datetime.date.today():
self.add_message('ansible-deprecated-date', node=node, args=(date,)) if version:
except ValueError: self._check_version(node, version)
self.add_message('ansible-invalid-deprecated-date', node=node, args=(date,))
except AttributeError: except AttributeError:
# Not the type of node we are interested in # Not the type of node we are interested in
pass pass

View file

@ -41,10 +41,10 @@ import yaml
from ansible import __version__ as ansible_version from ansible import __version__ as ansible_version
from ansible.executor.module_common import REPLACER_WINDOWS from ansible.executor.module_common import REPLACER_WINDOWS
from ansible.module_utils.common._collections_compat import Mapping from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils._text import to_bytes from ansible.module_utils._text import to_bytes, to_native
from ansible.plugins.loader import fragment_loader from ansible.plugins.loader import fragment_loader
from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder
from ansible.utils.plugin_docs import BLACKLIST, add_fragments, get_docstring from ansible.utils.plugin_docs import BLACKLIST, tag_versions_and_dates, add_fragments, get_docstring
from ansible.utils.version import SemanticVersion from ansible.utils.version import SemanticVersion
from .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec from .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec
@ -258,9 +258,12 @@ class ModuleValidator(Validator):
self.StrictVersion = StrictVersion self.StrictVersion = StrictVersion
self.collection = collection self.collection = collection
self.collection_name = 'ansible.builtin'
if self.collection: if self.collection:
self.Version = SemanticVersion self.Version = SemanticVersion
self.StrictVersion = SemanticVersion self.StrictVersion = SemanticVersion
collection_namespace_path, collection_name = os.path.split(self.collection)
self.collection_name = '%s.%s' % (os.path.basename(collection_namespace_path), collection_name)
self.routing = routing self.routing = routing
self.collection_version = None self.collection_version = None
if collection_version is not None: if collection_version is not None:
@ -873,6 +876,8 @@ class ModuleValidator(Validator):
for error in errors: for error in errors:
path = [str(p) for p in error.path] path = [str(p) for p in error.path]
local_error_code = getattr(error, 'ansible_error_code', error_code)
if isinstance(error.data, dict): if isinstance(error.data, dict):
error_message = humanize_error(error.data, error) error_message = humanize_error(error.data, error)
else: else:
@ -885,10 +890,28 @@ class ModuleValidator(Validator):
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code=error_code, code=local_error_code,
msg='%s: %s' % (combined_path, error_message) msg='%s: %s' % (combined_path, error_message)
) )
@staticmethod
def _split_tagged_version(version_str):
if not isinstance(version_str, string_types):
raise ValueError('Tagged version must be string')
version_str = to_native(version_str)
if ':' not in version_str:
raise ValueError('Tagged version must have ":"')
return version_str.split(':', 1)
@staticmethod
def _extract_version_from_tag_for_msg(version_str):
if not isinstance(version_str, string_types):
return version_str
version_str = to_native(version_str)
if ':' not in version_str:
return version_str
return version_str.split(':', 1)[1]
def _validate_docs(self): def _validate_docs(self):
doc_info = self._get_docs() doc_info = self._get_docs()
doc = None doc = None
@ -966,6 +989,8 @@ class ModuleValidator(Validator):
doc_info['DOCUMENTATION']['lineno'], doc_info['DOCUMENTATION']['lineno'],
self.name, 'DOCUMENTATION' self.name, 'DOCUMENTATION'
) )
if doc:
tag_versions_and_dates(doc, '%s:' % (self.collection_name, ), is_module=True)
for error in errors: for error in errors:
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
@ -981,7 +1006,8 @@ class ModuleValidator(Validator):
missing_fragment = False missing_fragment = False
with CaptureStd(): with CaptureStd():
try: try:
get_docstring(self.path, fragment_loader, verbose=True) get_docstring(self.path, fragment_loader, verbose=True,
collection_name=self.collection_name, is_module=True)
except AssertionError: except AssertionError:
fragment = doc['extends_documentation_fragment'] fragment = doc['extends_documentation_fragment']
self.reporter.error( self.reporter.error(
@ -1002,7 +1028,7 @@ class ModuleValidator(Validator):
) )
if not missing_fragment: if not missing_fragment:
add_fragments(doc, self.object_path, fragment_loader=fragment_loader) add_fragments(doc, self.object_path, fragment_loader=fragment_loader, is_module=True)
if 'options' in doc and doc['options'] is None: if 'options' in doc and doc['options'] is None:
self.reporter.error( self.reporter.error(
@ -1024,9 +1050,8 @@ class ModuleValidator(Validator):
doc, doc,
doc_schema( doc_schema(
os.readlink(self.object_path).split('.')[0], os.readlink(self.object_path).split('.')[0],
version_added=not bool(self.collection),
deprecated_module=deprecated,
for_collection=bool(self.collection), for_collection=bool(self.collection),
deprecated_module=deprecated,
), ),
'DOCUMENTATION', 'DOCUMENTATION',
'invalid-documentation', 'invalid-documentation',
@ -1037,9 +1062,8 @@ class ModuleValidator(Validator):
doc, doc,
doc_schema( doc_schema(
self.object_name.split('.')[0], self.object_name.split('.')[0],
version_added=not bool(self.collection),
deprecated_module=deprecated,
for_collection=bool(self.collection), for_collection=bool(self.collection),
deprecated_module=deprecated,
), ),
'DOCUMENTATION', 'DOCUMENTATION',
'invalid-documentation', 'invalid-documentation',
@ -1088,7 +1112,8 @@ class ModuleValidator(Validator):
data, errors, traces = parse_yaml(doc_info['RETURN']['value'], data, errors, traces = parse_yaml(doc_info['RETURN']['value'],
doc_info['RETURN']['lineno'], doc_info['RETURN']['lineno'],
self.name, 'RETURN') self.name, 'RETURN')
self._validate_docs_schema(data, return_schema, 'RETURN', 'return-syntax-error') self._validate_docs_schema(data, return_schema(for_collection=bool(self.collection)),
'RETURN', 'return-syntax-error')
for error in errors: for error in errors:
self.reporter.error( self.reporter.error(
@ -1142,8 +1167,10 @@ class ModuleValidator(Validator):
# Make sure they give the same version or date. # Make sure they give the same version or date.
routing_date = routing_deprecation.get('removal_date') routing_date = routing_deprecation.get('removal_date')
routing_version = routing_deprecation.get('removal_version') routing_version = routing_deprecation.get('removal_version')
documentation_date = doc_deprecation.get('removed_at_date') # The versions and dates in the module documentation are auto-tagged, so remove the tag
documentation_version = doc_deprecation.get('removed_in') # to make comparison possible and to avoid confusing the user.
documentation_date = self._extract_version_from_tag_for_msg(doc_deprecation.get('removed_at_date'))
documentation_version = self._extract_version_from_tag_for_msg(doc_deprecation.get('removed_in'))
if routing_date != documentation_date: if routing_date != documentation_date:
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
@ -1166,23 +1193,26 @@ class ModuleValidator(Validator):
def _check_version_added(self, doc, existing_doc): def _check_version_added(self, doc, existing_doc):
version_added_raw = doc.get('version_added') version_added_raw = doc.get('version_added')
try: try:
version_added = self.StrictVersion(str(doc.get('version_added', '0.0') or '0.0')) version_added = self.StrictVersion(self._extract_version_from_tag_for_msg(str(doc.get('version_added', '0.0') or '0.0')))
except ValueError: except ValueError:
version_added = doc.get('version_added', '0.0') version_added = doc.get('version_added', '0.0')
if self._is_new_module() or version_added != 'historical': if self._is_new_module() or version_added != 'ansible.builtin:historical':
self.reporter.error( # already reported during schema validation, except:
path=self.object_path, if version_added == 'ansible.builtin:historical':
code='module-invalid-version-added', self.reporter.error(
msg='version_added is not a valid version number: %r' % version_added path=self.object_path,
) code='module-invalid-version-added',
msg='version_added is not a valid version number: %r' % 'historical'
)
return return
if existing_doc and str(version_added_raw) != str(existing_doc.get('version_added')): if existing_doc and str(version_added_raw) != str(existing_doc.get('version_added')):
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code='module-incorrect-version-added', code='module-incorrect-version-added',
msg='version_added should be %r. Currently %r' % (existing_doc.get('version_added'), msg='version_added should be %r. Currently %r' % (
version_added_raw) self._extract_version_from_tag_for_msg(existing_doc.get('version_added')),
self._extract_version_from_tag_for_msg(version_added_raw))
) )
if not self._is_new_module(): if not self._is_new_module():
@ -1196,10 +1226,11 @@ class ModuleValidator(Validator):
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code='module-incorrect-version-added', code='module-incorrect-version-added',
msg='version_added should be %r. Currently %r' % (should_be, version_added_raw) msg='version_added should be %r. Currently %r' % (
should_be, self._extract_version_from_tag_for_msg(version_added_raw))
) )
def _validate_ansible_module_call(self, docs): def _validate_ansible_module_call(self, docs, dates_tagged=True):
try: try:
spec, args, kwargs = get_argument_spec(self.path, self.collection) spec, args, kwargs = get_argument_spec(self.path, self.collection)
except AnsibleModuleNotInitialized: except AnsibleModuleNotInitialized:
@ -1221,7 +1252,9 @@ class ModuleValidator(Validator):
) )
return return
self._validate_docs_schema(kwargs, ansible_module_kwargs_schema(), 'AnsibleModule', 'invalid-ansiblemodule-schema') self._validate_docs_schema(kwargs, ansible_module_kwargs_schema(for_collection=bool(self.collection),
dates_tagged=dates_tagged),
'AnsibleModule', 'invalid-ansiblemodule-schema')
self._validate_argument_spec(docs, spec, kwargs) self._validate_argument_spec(docs, spec, kwargs)
@ -1433,7 +1466,7 @@ class ModuleValidator(Validator):
try: try:
if not context: if not context:
add_fragments(docs, self.object_path, fragment_loader=fragment_loader) add_fragments(docs, self.object_path, fragment_loader=fragment_loader, is_module=True)
except Exception: except Exception:
# Cannot merge fragments # Cannot merge fragments
return return
@ -1497,7 +1530,8 @@ class ModuleValidator(Validator):
removed_at_date = data.get('removed_at_date', None) removed_at_date = data.get('removed_at_date', None)
if removed_at_date is not None: if removed_at_date is not None:
try: try:
if parse_isodate(removed_at_date) < datetime.date.today(): date = self._extract_version_from_tag_for_msg(removed_at_date)
if parse_isodate(date) < datetime.date.today():
msg = "Argument '%s' in argument_spec" % arg msg = "Argument '%s' in argument_spec" % arg
if context: if context:
msg += " found in %s" % " -> ".join(context) msg += " found in %s" % " -> ".join(context)
@ -1517,7 +1551,8 @@ class ModuleValidator(Validator):
for deprecated_alias in deprecated_aliases: for deprecated_alias in deprecated_aliases:
if 'name' in deprecated_alias and 'date' in deprecated_alias: if 'name' in deprecated_alias and 'date' in deprecated_alias:
try: try:
if parse_isodate(deprecated_alias['date']) < datetime.date.today(): date = self._extract_version_from_tag_for_msg(deprecated_alias['date'])
if parse_isodate(date) < datetime.date.today():
msg = "Argument '%s' in argument_spec" % arg msg = "Argument '%s' in argument_spec" % arg
if context: if context:
msg += " found in %s" % " -> ".join(context) msg += " found in %s" % " -> ".join(context)
@ -1549,7 +1584,8 @@ class ModuleValidator(Validator):
removed_in_version = data.get('removed_in_version', None) removed_in_version = data.get('removed_in_version', None)
if removed_in_version is not None: if removed_in_version is not None:
try: try:
if compare_version >= self.Version(str(removed_in_version)): collection_name, removed_in_version = self._split_tagged_version(removed_in_version)
if collection_name == self.collection_name and compare_version >= self.Version(str(removed_in_version)):
msg = "Argument '%s' in argument_spec" % arg msg = "Argument '%s' in argument_spec" % arg
if context: if context:
msg += " found in %s" % " -> ".join(context) msg += " found in %s" % " -> ".join(context)
@ -1561,22 +1597,15 @@ class ModuleValidator(Validator):
msg=msg, msg=msg,
) )
except ValueError: except ValueError:
msg = "Argument '%s' in argument_spec" % arg # Has been caught in schema validation
if context: pass
msg += " found in %s" % " -> ".join(context)
msg += " has an invalid removed_in_version '%s'," % removed_in_version
msg += " i.e. %s" % version_parser_error
self.reporter.error(
path=self.object_path,
code=code_prefix + '-invalid-version',
msg=msg,
)
if deprecated_aliases is not None: if deprecated_aliases is not None:
for deprecated_alias in deprecated_aliases: for deprecated_alias in deprecated_aliases:
if 'name' in deprecated_alias and 'version' in deprecated_alias: if 'name' in deprecated_alias and 'version' in deprecated_alias:
try: try:
if compare_version >= self.Version(str(deprecated_alias['version'])): collection_name, version = self._split_tagged_version(deprecated_alias['version'])
if collection_name == self.collection_name and compare_version >= self.Version(str(version)):
msg = "Argument '%s' in argument_spec" % arg msg = "Argument '%s' in argument_spec" % arg
if context: if context:
msg += " found in %s" % " -> ".join(context) msg += " found in %s" % " -> ".join(context)
@ -1589,17 +1618,8 @@ class ModuleValidator(Validator):
msg=msg, msg=msg,
) )
except ValueError: except ValueError:
msg = "Argument '%s' in argument_spec" % arg # Has been caught in schema validation
if context: pass
msg += " found in %s" % " -> ".join(context)
msg += " has aliases '%s' with removal in invalid version '%s'," % (
deprecated_alias['name'], deprecated_alias['version'])
msg += " i.e. %s" % version_parser_error
self.reporter.error(
path=self.object_path,
code=code_prefix + '-invalid-version',
msg=msg,
)
aliases = data.get('aliases', []) aliases = data.get('aliases', [])
if arg in aliases: if arg in aliases:
@ -1978,7 +1998,8 @@ class ModuleValidator(Validator):
with CaptureStd(): with CaptureStd():
try: try:
existing_doc, dummy_examples, dummy_return, existing_metadata = get_docstring(self.base_module, fragment_loader, verbose=True) existing_doc, dummy_examples, dummy_return, existing_metadata = get_docstring(
self.base_module, fragment_loader, verbose=True, collection_name=self.collection_name, is_module=True)
existing_options = existing_doc.get('options', {}) or {} existing_options = existing_doc.get('options', {}) or {}
except AssertionError: except AssertionError:
fragment = doc['extends_documentation_fragment'] fragment = doc['extends_documentation_fragment']
@ -2031,6 +2052,7 @@ class ModuleValidator(Validator):
continue continue
if any(name in existing_options for name in names): if any(name in existing_options for name in names):
# The option already existed. Make sure version_added didn't change.
for name in names: for name in names:
existing_version = existing_options.get(name, {}).get('version_added') existing_version = existing_options.get(name, {}).get('version_added')
if existing_version: if existing_version:
@ -2052,19 +2074,7 @@ class ModuleValidator(Validator):
str(details.get('version_added', '0.0')) str(details.get('version_added', '0.0'))
) )
except ValueError: except ValueError:
version_added = details.get('version_added', '0.0') # already reported during schema validation
self.reporter.error(
path=self.object_path,
code='module-invalid-version-added-number',
msg=('version_added for new option (%s) '
'is not a valid version number: %r' %
(option, version_added))
)
continue
except Exception:
# If there is any other exception it should have been caught
# in schema validation, so we won't duplicate errors by
# listing it again
continue continue
if (strict_ansible_version != mod_version_added and if (strict_ansible_version != mod_version_added and
@ -2148,7 +2158,16 @@ class ModuleValidator(Validator):
pass pass
if 'removed_in' in docs['deprecated']: if 'removed_in' in docs['deprecated']:
try: try:
removed_in = self.StrictVersion(str(docs['deprecated']['removed_in'])) collection_name, version = self._split_tagged_version(docs['deprecated']['removed_in'])
if collection_name != self.collection_name:
self.reporter.error(
path=self.object_path,
code='invalid-module-deprecation-source',
msg=('The deprecation version for a module must be added in this collection')
)
# Treat the module as not to be removed:
raise ValueError('')
removed_in = self.StrictVersion(str(version))
except ValueError: except ValueError:
end_of_deprecation_should_be_removed_only = False end_of_deprecation_should_be_removed_only = False
else: else:
@ -2182,7 +2201,8 @@ class ModuleValidator(Validator):
if re.search(pattern, self.text) and self.object_name not in self.PS_ARG_VALIDATE_BLACKLIST: if re.search(pattern, self.text) and self.object_name not in self.PS_ARG_VALIDATE_BLACKLIST:
with ModuleValidator(docs_path, base_branch=self.base_branch, git_cache=self.git_cache) as docs_mv: with ModuleValidator(docs_path, base_branch=self.base_branch, git_cache=self.git_cache) as docs_mv:
docs = docs_mv._validate_docs()[1] docs = docs_mv._validate_docs()[1]
self._validate_ansible_module_call(docs) # Don't expect tagged dates!
self._validate_ansible_module_call(docs, dates_tagged=False)
self._check_gpl3_header() self._check_gpl3_header()
if not self._just_docs() and not end_of_deprecation_should_be_removed_only: if not self._just_docs() and not end_of_deprecation_should_be_removed_only:

View file

@ -9,9 +9,13 @@ __metaclass__ = type
import datetime import datetime
import re import re
from functools import partial
from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.module_utils.common.collections import is_iterable from ansible.module_utils.common.collections import is_iterable
from ansible.utils.version import SemanticVersion
from distutils.version import StrictVersion
from .utils import parse_isodate from .utils import parse_isodate
@ -28,6 +32,86 @@ any_string_types = Any(*string_types)
author_line = re.compile(r'^\w.*(\(@([\w-]+)\)|!UNKNOWN)(?![\w.])|^Ansible Core Team$|^Michael DeHaan$') author_line = re.compile(r'^\w.*(\(@([\w-]+)\)|!UNKNOWN)(?![\w.])|^Ansible Core Team$|^Michael DeHaan$')
def _add_ansible_error_code(exception, error_code):
setattr(exception, 'ansible_error_code', error_code)
return exception
def semantic_version(v, error_code=None):
if not isinstance(v, string_types):
raise _add_ansible_error_code(Invalid('Semantic version must be a string'), error_code or 'collection-invalid-version')
try:
SemanticVersion(v)
except ValueError as e:
raise _add_ansible_error_code(Invalid(str(e)), error_code or 'collection-invalid-version')
return v
def ansible_version(v, error_code=None):
# Assumes argument is a string or float
if 'historical' == v:
return v
try:
StrictVersion(str(v))
except ValueError as e:
raise _add_ansible_error_code(Invalid(str(e)), error_code or 'ansible-invalid-version')
return v
def isodate(v, error_code=None):
try:
parse_isodate(v)
except ValueError as e:
raise _add_ansible_error_code(Invalid(str(e)), error_code or 'ansible-invalid-date')
return v
TAGGED_VERSION_RE = re.compile('^([^.]+.[^.]+):(.*)$')
def tagged_version(v, error_code=None):
if not isinstance(v, string_types):
# Should never happen to versions tagged by code
raise _add_ansible_error_code(Invalid('Tagged version must be a string'), 'invalid-tagged-version')
m = TAGGED_VERSION_RE.match(v)
if not m:
# Should never happen to versions tagged by code
raise _add_ansible_error_code(Invalid('Tagged version does not match format'), 'invalid-tagged-version')
collection = m.group(1)
version = m.group(2)
if collection != 'ansible.builtin':
semantic_version(version, error_code=error_code)
else:
ansible_version(version, error_code=error_code)
return v
def tagged_isodate(v, error_code=None):
if not isinstance(v, string_types):
# Should never happen to dates tagged by code
raise _add_ansible_error_code(Invalid('Tagged date must be a string'), 'invalid-tagged-date')
m = TAGGED_VERSION_RE.match(v)
if not m:
# Should never happen to dates tagged by code
raise _add_ansible_error_code(Invalid('Tagged date does not match format'), 'invalid-tagged-date')
isodate(m.group(2), error_code=error_code)
return v
def version(for_collection=False, tagged='never', error_code=None):
if tagged == 'always':
return Any(partial(tagged_version, error_code=error_code))
if for_collection:
return Any(partial(semantic_version, error_code=error_code))
return All(Any(float, *string_types), partial(ansible_version, error_code=error_code))
def date(tagged='never', error_code=None):
if tagged == 'always':
return Any(partial(tagged_isodate, error_code=error_code))
return Any(isodate)
def is_callable(v): def is_callable(v):
if not callable(v): if not callable(v):
raise ValueInvalid('not a valid value') raise ValueInvalid('not a valid value')
@ -101,15 +185,8 @@ def options_with_apply_defaults(v):
return v return v
def isodate(v): def argument_spec_schema(for_collection, dates_tagged=True):
try: dates_tagged = 'always' if dates_tagged else 'never'
parse_isodate(v)
except ValueError as e:
raise Invalid(str(e))
return v
def argument_spec_schema():
any_string_types = Any(*string_types) any_string_types = Any(*string_types)
schema = { schema = {
any_string_types: { any_string_types: {
@ -125,17 +202,17 @@ def argument_spec_schema():
'no_log': bool, 'no_log': bool,
'aliases': Any(list_string_types, tuple(list_string_types)), 'aliases': Any(list_string_types, tuple(list_string_types)),
'apply_defaults': bool, 'apply_defaults': bool,
'removed_in_version': Any(float, *string_types), 'removed_in_version': version(for_collection, tagged='always'),
'removed_at_date': Any(isodate), 'removed_at_date': date(tagged=dates_tagged),
'options': Self, 'options': Self,
'deprecated_aliases': Any([Any( 'deprecated_aliases': Any([Any(
{ {
Required('name'): Any(*string_types), Required('name'): Any(*string_types),
Required('date'): Any(isodate), Required('date'): date(tagged=dates_tagged),
}, },
{ {
Required('name'): Any(*string_types), Required('name'): Any(*string_types),
Required('version'): Any(float, *string_types), Required('version'): version(for_collection, tagged='always'),
}, },
)]), )]),
} }
@ -150,9 +227,9 @@ def argument_spec_schema():
return Schema(schemas) return Schema(schemas)
def ansible_module_kwargs_schema(): def ansible_module_kwargs_schema(for_collection, dates_tagged=True):
schema = { schema = {
'argument_spec': argument_spec_schema(), 'argument_spec': argument_spec_schema(for_collection, dates_tagged=dates_tagged),
'bypass_checks': bool, 'bypass_checks': bool,
'no_log': bool, 'no_log': bool,
'check_invalid_arguments': Any(None, bool), 'check_invalid_arguments': Any(None, bool),
@ -172,48 +249,49 @@ json_value = Schema(Any(
)) ))
suboption_schema = Schema( def list_dict_option_schema(for_collection):
{ suboption_schema = Schema(
Required('description'): Any(list_string_types, *string_types), {
'required': bool, Required('description'): Any(list_string_types, *string_types),
'choices': list, 'required': bool,
'aliases': Any(list_string_types), 'choices': list,
'version_added': Any(float, *string_types), 'aliases': Any(list_string_types),
'default': json_value, 'version_added': version(for_collection, tagged='always', error_code='option-invalid-version-added'),
# Note: Types are strings, not literal bools, such as True or False 'default': json_value,
'type': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'), # Note: Types are strings, not literal bools, such as True or False
# in case of type='list' elements define type of individual item in list 'type': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'), # in case of type='list' elements define type of individual item in list
# Recursive suboptions 'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
'suboptions': Any(None, *list({str_type: Self} for str_type in string_types)), # Recursive suboptions
}, 'suboptions': Any(None, *list({str_type: Self} for str_type in string_types)),
extra=PREVENT_EXTRA },
) extra=PREVENT_EXTRA
)
# This generates list of dicts with keys from string_types and suboption_schema value # This generates list of dicts with keys from string_types and suboption_schema value
# for example in Python 3: {str: suboption_schema} # for example in Python 3: {str: suboption_schema}
list_dict_suboption_schema = [{str_type: suboption_schema} for str_type in string_types] list_dict_suboption_schema = [{str_type: suboption_schema} for str_type in string_types]
option_schema = Schema( option_schema = Schema(
{ {
Required('description'): Any(list_string_types, *string_types), Required('description'): Any(list_string_types, *string_types),
'required': bool, 'required': bool,
'choices': list, 'choices': list,
'aliases': Any(list_string_types), 'aliases': Any(list_string_types),
'version_added': Any(float, *string_types), 'version_added': version(for_collection, tagged='always', error_code='option-invalid-version-added'),
'default': json_value, 'default': json_value,
'suboptions': Any(None, *list_dict_suboption_schema), 'suboptions': Any(None, *list_dict_suboption_schema),
# Note: Types are strings, not literal bools, such as True or False # Note: Types are strings, not literal bools, such as True or False
'type': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'), 'type': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
# in case of type='list' elements define type of individual item in list # in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'), 'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
}, },
extra=PREVENT_EXTRA extra=PREVENT_EXTRA
) )
# This generates list of dicts with keys from string_types and option_schema value # This generates list of dicts with keys from string_types and option_schema value
# for example in Python 3: {str: option_schema} # for example in Python 3: {str: option_schema}
list_dict_option_schema = [{str_type: option_schema} for str_type in string_types] return [{str_type: option_schema} for str_type in string_types]
def return_contains(v): def return_contains(v):
@ -228,51 +306,52 @@ def return_contains(v):
return v return v
return_contains_schema = Any( def return_schema(for_collection):
All( return_contains_schema = Any(
Schema( All(
{ Schema(
Required('description'): Any(list_string_types, *string_types), {
'returned': Any(*string_types), # only returned on top level
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': Any(float, *string_types),
'sample': json_value,
'example': json_value,
'contains': Any(None, *list({str_type: Self} for str_type in string_types)),
# in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
}
),
Schema(return_contains)
),
Schema(type(None)),
)
# This generates list of dicts with keys from string_types and return_contains_schema value
# for example in Python 3: {str: return_contains_schema}
list_dict_return_contains_schema = [{str_type: return_contains_schema} for str_type in string_types]
return_schema = Any(
All(
Schema(
{
any_string_types: {
Required('description'): Any(list_string_types, *string_types), Required('description'): Any(list_string_types, *string_types),
Required('returned'): Any(*string_types), 'returned': Any(*string_types), # only returned on top level
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'), Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': Any(float, *string_types), 'version_added': version(for_collection, error_code='return-invalid-version-added'),
'sample': json_value, 'sample': json_value,
'example': json_value, 'example': json_value,
'contains': Any(None, *list_dict_return_contains_schema), 'contains': Any(None, *list({str_type: Self} for str_type in string_types)),
# in case of type='list' elements define type of individual item in list # in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'), 'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
} }
} ),
Schema(return_contains)
), ),
Schema({any_string_types: return_contains}) Schema(type(None)),
), )
Schema(type(None)),
) # This generates list of dicts with keys from string_types and return_contains_schema value
# for example in Python 3: {str: return_contains_schema}
list_dict_return_contains_schema = [{str_type: return_contains_schema} for str_type in string_types]
return Any(
All(
Schema(
{
any_string_types: {
Required('description'): Any(list_string_types, *string_types),
Required('returned'): Any(*string_types),
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': version(for_collection, error_code='return-invalid-version-added'),
'sample': json_value,
'example': json_value,
'contains': Any(None, *list_dict_return_contains_schema),
# in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
}
}
),
Schema({any_string_types: return_contains})
),
Schema(type(None)),
)
def deprecation_schema(for_collection): def deprecation_schema(for_collection):
@ -283,13 +362,13 @@ def deprecation_schema(for_collection):
} }
date_schema = { date_schema = {
Required('removed_at_date'): Any(isodate), Required('removed_at_date'): date(tagged='always'),
} }
date_schema.update(main_fields) date_schema.update(main_fields)
if for_collection: if for_collection:
version_schema = { version_schema = {
Required('removed_in'): Any(float, *string_types), Required('removed_in'): version(for_collection, tagged='always'),
} }
else: else:
version_schema = { version_schema = {
@ -297,13 +376,16 @@ def deprecation_schema(for_collection):
# Deprecation cycle changed at 2.4 (though not retroactively) # Deprecation cycle changed at 2.4 (though not retroactively)
# 2.3 -> removed_in: "2.5" + n for docs stub # 2.3 -> removed_in: "2.5" + n for docs stub
# 2.4 -> removed_in: "2.8" + n for docs stub # 2.4 -> removed_in: "2.8" + n for docs stub
Required('removed_in'): Any("2.2", "2.3", "2.4", "2.5", "2.6", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13", "2.14"), Required('removed_in'): Any(
"ansible.builtin:2.2", "ansible.builtin:2.3", "ansible.builtin:2.4", "ansible.builtin:2.5",
"ansible.builtin:2.6", "ansible.builtin:2.8", "ansible.builtin:2.9", "ansible.builtin:2.10",
"ansible.builtin:2.11", "ansible.builtin:2.12", "ansible.builtin:2.13", "ansible.builtin:2.14"),
} }
version_schema.update(main_fields) version_schema.update(main_fields)
return Any( return Any(
Schema(date_schema, extra=PREVENT_EXTRA),
Schema(version_schema, extra=PREVENT_EXTRA), Schema(version_schema, extra=PREVENT_EXTRA),
Schema(date_schema, extra=PREVENT_EXTRA),
) )
@ -318,7 +400,7 @@ def author(value):
raise Invalid("Invalid author") raise Invalid("Invalid author")
def doc_schema(module_name, version_added=True, deprecated_module=False, for_collection=False): def doc_schema(module_name, for_collection=False, deprecated_module=False):
if module_name.startswith('_'): if module_name.startswith('_'):
module_name = module_name[1:] module_name = module_name[1:]
@ -332,15 +414,17 @@ def doc_schema(module_name, version_added=True, deprecated_module=False, for_col
'seealso': Any(None, seealso_schema), 'seealso': Any(None, seealso_schema),
'requirements': list_string_types, 'requirements': list_string_types,
'todo': Any(None, list_string_types, *string_types), 'todo': Any(None, list_string_types, *string_types),
'options': Any(None, *list_dict_option_schema), 'options': Any(None, *list_dict_option_schema(for_collection)),
'extends_documentation_fragment': Any(list_string_types, *string_types) 'extends_documentation_fragment': Any(list_string_types, *string_types)
} }
if version_added: if for_collection:
doc_schema_dict[Required('version_added')] = Any(float, *string_types)
else:
# Optional # Optional
doc_schema_dict['version_added'] = Any(float, *string_types) doc_schema_dict['version_added'] = version(
for_collection=True, tagged='always', error_code='module-invalid-version-added')
else:
doc_schema_dict[Required('version_added')] = version(
for_collection=False, tagged='always', error_code='module-invalid-version-added')
if deprecated_module: if deprecated_module:
deprecation_required_scheme = { deprecation_required_scheme = {

View file

@ -239,12 +239,10 @@ class PylintTest(SanitySingleVersion):
] + paths ] + paths
if data_context().content.collection: if data_context().content.collection:
cmd.extend(['--is-collection', 'yes']) cmd.extend(['--collection-name', data_context().content.collection.full_name])
if collection_detail and collection_detail.version: if collection_detail and collection_detail.version:
cmd.extend(['--collection-version', collection_detail.version]) cmd.extend(['--collection-version', collection_detail.version])
else:
cmd.extend(['--enable', 'ansible-deprecated-version'])
append_python_path = [plugin_dir] append_python_path = [plugin_dir]

View file

@ -86,7 +86,7 @@ def main():
module = AnsibleAWSModule(argument_spec=argument_spec) module = AnsibleAWSModule(argument_spec=argument_spec)
if module._name == 'aws_az_facts': if module._name == 'aws_az_facts':
module.deprecate("The 'aws_az_facts' module has been renamed to 'aws_az_info'", version='2.14') module.deprecate("The 'aws_az_facts' module has been renamed to 'aws_az_info'", version='ansible.builtin:2.14')
connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff())

View file

@ -136,7 +136,7 @@ class AzureRMFunctionAppInfo(AzureRMModuleBase):
is_old_facts = self.module._name == 'azure_rm_functionapp_facts' is_old_facts = self.module._name == 'azure_rm_functionapp_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_functionapp_facts' module has been renamed to 'azure_rm_functionapp_info'", version='2.13') self.module.deprecate("The 'azure_rm_functionapp_facts' module has been renamed to 'azure_rm_functionapp_info'", version='ansible.builtin:2.13')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

View file

@ -139,7 +139,8 @@ class AzureRMMariaDbConfigurationInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_mariadbconfiguration_facts' is_old_facts = self.module._name == 'azure_rm_mariadbconfiguration_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbconfiguration_facts' module has been renamed to 'azure_rm_mariadbconfiguration_info'", version='2.13') self.module.deprecate("The 'azure_rm_mariadbconfiguration_facts' module has been renamed to 'azure_rm_mariadbconfiguration_info'",
version='ansible.builtin:2.13')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

View file

@ -145,7 +145,8 @@ class AzureRMMariaDbDatabaseInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_mariadbdatabase_facts' is_old_facts = self.module._name == 'azure_rm_mariadbdatabase_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbdatabase_facts' module has been renamed to 'azure_rm_mariadbdatabase_info'", version='2.13') self.module.deprecate("The 'azure_rm_mariadbdatabase_facts' module has been renamed to 'azure_rm_mariadbdatabase_info'",
version='ansible.builtin:2.13')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

View file

@ -141,7 +141,8 @@ class AzureRMMariaDbFirewallRuleInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_mariadbfirewallrule_facts' is_old_facts = self.module._name == 'azure_rm_mariadbfirewallrule_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbfirewallrule_facts' module has been renamed to 'azure_rm_mariadbfirewallrule_info'", version='2.13') self.module.deprecate("The 'azure_rm_mariadbfirewallrule_facts' module has been renamed to 'azure_rm_mariadbfirewallrule_info'",
version='ansible.builtin:2.13')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

View file

@ -193,7 +193,7 @@ class AzureRMMariaDbServerInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_mariadbserver_facts' is_old_facts = self.module._name == 'azure_rm_mariadbserver_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbserver_facts' module has been renamed to 'azure_rm_mariadbserver_info'", version='2.13') self.module.deprecate("The 'azure_rm_mariadbserver_facts' module has been renamed to 'azure_rm_mariadbserver_info'", version='ansible.builtin:2.13')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

View file

@ -336,7 +336,7 @@ class AzureRMResourceInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_resource_facts' is_old_facts = self.module._name == 'azure_rm_resource_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_resource_facts' module has been renamed to 'azure_rm_resource_info'", version='2.13') self.module.deprecate("The 'azure_rm_resource_facts' module has been renamed to 'azure_rm_resource_info'", version='ansible.builtin:2.13')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

View file

@ -265,7 +265,7 @@ class AzureRMWebAppInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_webapp_facts' is_old_facts = self.module._name == 'azure_rm_webapp_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_webapp_facts' module has been renamed to 'azure_rm_webapp_info'", version='2.13') self.module.deprecate("The 'azure_rm_webapp_facts' module has been renamed to 'azure_rm_webapp_info'", version='ansible.builtin:2.13')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

View file

@ -302,7 +302,7 @@ def main():
is_old_facts = module._name == 'cloudformation_facts' is_old_facts = module._name == 'cloudformation_facts'
if is_old_facts: if is_old_facts:
module.deprecate("The 'cloudformation_facts' module has been renamed to 'cloudformation_info', " module.deprecate("The 'cloudformation_facts' module has been renamed to 'cloudformation_info', "
"and the renamed one no longer returns ansible_facts", version='2.13') "and the renamed one no longer returns ansible_facts", version='ansible.builtin:2.13')
service_mgr = CloudFormationServiceManager(module) service_mgr = CloudFormationServiceManager(module)

View file

@ -450,7 +450,7 @@ class SwarmManager(DockerBaseClass):
if self.state == 'inspect': if self.state == 'inspect':
self.client.module.deprecate( self.client.module.deprecate(
"The 'inspect' state is deprecated, please use 'docker_swarm_info' to inspect swarm cluster", "The 'inspect' state is deprecated, please use 'docker_swarm_info' to inspect swarm cluster",
version='2.12') version='ansible.builtin:2.12')
choice_map.get(self.state)() choice_map.get(self.state)()

View file

@ -1695,7 +1695,7 @@ def main():
module.deprecate( module.deprecate(
msg='Support for passing both group and group_id has been deprecated. ' msg='Support for passing both group and group_id has been deprecated. '
'Currently group_id is ignored, in future passing both will result in an error', 'Currently group_id is ignored, in future passing both will result in an error',
version='2.14') version='ansible.builtin:2.14')
if not HAS_BOTO: if not HAS_BOTO:
module.fail_json(msg='boto required for this module') module.fail_json(msg='boto required for this module')

View file

@ -270,7 +270,7 @@ def main():
module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
if module._module._name == 'ec2_ami_facts': if module._module._name == 'ec2_ami_facts':
module._module.deprecate("The 'ec2_ami_facts' module has been renamed to 'ec2_ami_info'", version='2.13') module._module.deprecate("The 'ec2_ami_facts' module has been renamed to 'ec2_ami_info'", version='ansible.builtin:2.13')
ec2_client = module.client('ec2') ec2_client = module.client('ec2')

View file

@ -259,7 +259,7 @@ def main():
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'ec2_eni_facts': if module._name == 'ec2_eni_facts':
module.deprecate("The 'ec2_eni_facts' module has been renamed to 'ec2_eni_info'", version='2.13') module.deprecate("The 'ec2_eni_facts' module has been renamed to 'ec2_eni_info'", version='ansible.builtin:2.13')
if not HAS_BOTO3: if not HAS_BOTO3:
module.fail_json(msg='boto3 required for this module') module.fail_json(msg='boto3 required for this module')

View file

@ -552,7 +552,7 @@ def main():
supports_check_mode=True supports_check_mode=True
) )
if module._name == 'ec2_instance_facts': if module._name == 'ec2_instance_facts':
module.deprecate("The 'ec2_instance_facts' module has been renamed to 'ec2_instance_info'", version='2.13') module.deprecate("The 'ec2_instance_facts' module has been renamed to 'ec2_instance_info'", version='ansible.builtin:2.13')
if not HAS_BOTO3: if not HAS_BOTO3:
module.fail_json(msg='boto3 required for this module') module.fail_json(msg='boto3 required for this module')

View file

@ -637,7 +637,7 @@ def main():
if module.params.get('purge_policies') is None: if module.params.get('purge_policies') is None:
module.deprecate('In Ansible 2.14 the default value of purge_policies will change from true to false.' module.deprecate('In Ansible 2.14 the default value of purge_policies will change from true to false.'
' To maintain the existing behaviour explicity set purge_policies=true', version='2.14') ' To maintain the existing behaviour explicity set purge_policies=true', version='ansible.builtin:2.14')
if module.params.get('boundary'): if module.params.get('boundary'):
if module.params.get('create_instance_profile'): if module.params.get('create_instance_profile'):

View file

@ -142,7 +142,7 @@ class KubernetesInfoModule(KubernetesAnsibleModule):
supports_check_mode=True, supports_check_mode=True,
**kwargs) **kwargs)
if self._name == 'k8s_facts': if self._name == 'k8s_facts':
self.deprecate("The 'k8s_facts' module has been renamed to 'k8s_info'", version='2.13') self.deprecate("The 'k8s_facts' module has been renamed to 'k8s_info'", version='ansible.builtin:2.13')
def execute_module(self): def execute_module(self):
self.client = self.get_api_client() self.client = self.get_api_client()

View file

@ -2654,7 +2654,7 @@ def main():
if provider == 'assertonly': if provider == 'assertonly':
module.deprecate("The 'assertonly' provider is deprecated; please see the examples of " module.deprecate("The 'assertonly' provider is deprecated; please see the examples of "
"the 'openssl_certificate' module on how to replace it with other modules", "the 'openssl_certificate' module on how to replace it with other modules",
version='2.13') version='ansible.builtin:2.13')
elif provider == 'selfsigned': elif provider == 'selfsigned':
if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None: if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None:
module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.') module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.')
@ -2702,7 +2702,7 @@ def main():
except AttributeError: except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15') module.fail_json(msg='You need to have PyOpenSSL>=0.15')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13')
if provider == 'selfsigned': if provider == 'selfsigned':
certificate = SelfSignedCertificate(module) certificate = SelfSignedCertificate(module)
elif provider == 'acme': elif provider == 'acme':

View file

@ -845,7 +845,7 @@ def main():
except AttributeError: except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15') module.fail_json(msg='You need to have PyOpenSSL>=0.15')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13')
certificate = CertificateInfoPyOpenSSL(module) certificate = CertificateInfoPyOpenSSL(module)
elif backend == 'cryptography': elif backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:

View file

@ -1091,7 +1091,7 @@ def main():
if module.params['version'] != 1: if module.params['version'] != 1:
module.deprecate('The version option will only support allowed values from Ansible 2.14 on. ' module.deprecate('The version option will only support allowed values from Ansible 2.14 on. '
'Currently, only the value 1 is allowed by RFC 2986', version='2.14') 'Currently, only the value 1 is allowed by RFC 2986', version='ansible.builtin:2.14')
base_dir = os.path.dirname(module.params['path']) or '.' base_dir = os.path.dirname(module.params['path']) or '.'
if not os.path.isdir(base_dir): if not os.path.isdir(base_dir):
@ -1125,7 +1125,7 @@ def main():
except AttributeError: except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs') module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13')
csr = CertificateSigningRequestPyOpenSSL(module) csr = CertificateSigningRequestPyOpenSSL(module)
elif backend == 'cryptography': elif backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:

View file

@ -908,7 +908,7 @@ def main():
if not PYOPENSSL_FOUND: if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR) exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13')
private_key = PrivateKeyPyOpenSSL(module) private_key = PrivateKeyPyOpenSSL(module)
elif backend == 'cryptography': elif backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:

View file

@ -118,7 +118,7 @@ def main():
supports_check_mode=True, supports_check_mode=True,
) )
if module._name == 'python_requirements_facts': if module._name == 'python_requirements_facts':
module.deprecate("The 'python_requirements_facts' module has been renamed to 'python_requirements_info'", version='2.13') module.deprecate("The 'python_requirements_facts' module has been renamed to 'python_requirements_info'", version='ansible.builtin:2.13')
if not HAS_DISTUTILS: if not HAS_DISTUTILS:
module.fail_json( module.fail_json(
msg='Could not import "distutils" and "pkg_resources" libraries to introspect python environment.', msg='Could not import "distutils" and "pkg_resources" libraries to introspect python environment.',

View file

@ -885,7 +885,8 @@ def main():
# Report wrongly used attribute parameter when using content=attribute # Report wrongly used attribute parameter when using content=attribute
# TODO: Remove this in Ansible v2.12 (and reinstate strict parameter test above) and remove the integration test example # TODO: Remove this in Ansible v2.12 (and reinstate strict parameter test above) and remove the integration test example
if content == 'attribute' and attribute is not None: if content == 'attribute' and attribute is not None:
module.deprecate("Parameter 'attribute=%s' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry." % attribute, '2.12') module.deprecate("Parameter 'attribute=%s' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry." % attribute,
'ansible.builtin:2.12')
# Check if the file exists # Check if the file exists
if xml_string: if xml_string:

View file

@ -22,23 +22,23 @@ def test_warn(am, capfd):
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
def test_deprecate(am, capfd): def test_deprecate(am, capfd):
am.deprecate('deprecation1') am.deprecate('deprecation1')
am.deprecate('deprecation2', '2.3') am.deprecate('deprecation2', 'ansible.builtin:2.3')
am.deprecate('deprecation3', version='2.4') am.deprecate('deprecation3', version='ansible.builtin:2.4')
am.deprecate('deprecation4', date='2020-03-10') am.deprecate('deprecation4', date='ansible.builtin:2020-03-10')
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
am.exit_json(deprecations=['deprecation5', ('deprecation6', '2.4')]) am.exit_json(deprecations=['deprecation5', ('deprecation6', 'ansible.builtin:2.4')])
out, err = capfd.readouterr() out, err = capfd.readouterr()
output = json.loads(out) output = json.loads(out)
assert ('warnings' not in output or output['warnings'] == []) assert ('warnings' not in output or output['warnings'] == [])
assert output['deprecations'] == [ assert output['deprecations'] == [
{u'msg': u'deprecation1', u'version': None}, {u'msg': u'deprecation1', u'version': None},
{u'msg': u'deprecation2', u'version': '2.3'}, {u'msg': u'deprecation2', u'version': 'ansible.builtin:2.3'},
{u'msg': u'deprecation3', u'version': '2.4'}, {u'msg': u'deprecation3', u'version': 'ansible.builtin:2.4'},
{u'msg': u'deprecation4', u'date': '2020-03-10'}, {u'msg': u'deprecation4', u'date': 'ansible.builtin:2020-03-10'},
{u'msg': u'deprecation5', u'version': None}, {u'msg': u'deprecation5', u'version': None},
{u'msg': u'deprecation6', u'version': '2.4'}, {u'msg': u'deprecation6', u'version': 'ansible.builtin:2.4'},
] ]