From 40f21dfd3c7699f5e333fed257da5f452b78f4b4 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 29 May 2020 07:46:16 +0200 Subject: [PATCH] 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). --- .../ansible-doc-version_added-collection.yml | 2 + .../ansible-test-version-validation.yml | 2 + .../dev_guide/testing_validate-modules.rst | 2 + lib/ansible/cli/__init__.py | 2 +- lib/ansible/cli/console.py | 2 +- lib/ansible/cli/doc.py | 27 +- lib/ansible/executor/task_executor.py | 2 +- lib/ansible/module_utils/urls.py | 3 +- lib/ansible/modules/copy.py | 2 +- lib/ansible/modules/cron.py | 4 +- lib/ansible/modules/get_url.py | 4 +- lib/ansible/modules/systemd.py | 2 +- lib/ansible/modules/uri.py | 2 +- lib/ansible/playbook/__init__.py | 4 +- lib/ansible/playbook/conditional.py | 2 +- lib/ansible/playbook/helpers.py | 8 +- lib/ansible/playbook/play_context.py | 2 +- lib/ansible/playbook/task.py | 4 +- lib/ansible/plugins/action/__init__.py | 2 +- lib/ansible/plugins/action/async_status.py | 2 +- lib/ansible/plugins/cache/__init__.py | 4 +- lib/ansible/plugins/callback/__init__.py | 2 +- lib/ansible/plugins/connection/__init__.py | 8 +- lib/ansible/plugins/inventory/__init__.py | 6 +- lib/ansible/plugins/inventory/script.py | 2 +- lib/ansible/plugins/loader.py | 21 +- lib/ansible/plugins/strategy/__init__.py | 4 +- lib/ansible/template/__init__.py | 2 +- lib/ansible/utils/display.py | 37 ++- lib/ansible/utils/plugin_docs.py | 81 ++++- lib/ansible/utils/unsafe_proxy.py | 2 +- lib/ansible/vars/fact_cache.py | 2 +- .../tasks/test-get-element-content.yml | 2 +- .../_data/sanity/pylint/plugins/deprecated.py | 140 ++++++--- .../validate-modules/validate_modules/main.py | 152 ++++++---- .../validate_modules/schema.py | 286 +++++++++++------- .../ansible_test/_internal/sanity/pylint.py | 4 +- .../plugins/modules/aws_az_info.py | 2 +- .../modules/azure_rm_functionapp_info.py | 2 +- .../azure_rm_mariadbconfiguration_info.py | 3 +- .../modules/azure_rm_mariadbdatabase_info.py | 3 +- .../azure_rm_mariadbfirewallrule_info.py | 3 +- .../modules/azure_rm_mariadbserver_info.py | 2 +- .../plugins/modules/azure_rm_resource_info.py | 2 +- .../plugins/modules/azure_rm_webapp_info.py | 2 +- .../plugins/modules/cloudformation_info.py | 2 +- .../plugins/modules/docker_swarm.py | 2 +- .../integration/plugins/modules/ec2.py | 2 +- .../plugins/modules/ec2_ami_info.py | 2 +- .../plugins/modules/ec2_eni_info.py | 2 +- .../plugins/modules/ec2_instance_info.py | 2 +- .../integration/plugins/modules/iam_role.py | 2 +- .../integration/plugins/modules/k8s_info.py | 2 +- .../plugins/modules/openssl_certificate.py | 4 +- .../modules/openssl_certificate_info.py | 2 +- .../plugins/modules/openssl_csr.py | 4 +- .../plugins/modules/openssl_privatekey.py | 2 +- .../modules/python_requirements_info.py | 2 +- .../integration/plugins/modules/xml.py | 3 +- .../module_utils/basic/test_deprecate_warn.py | 16 +- 60 files changed, 601 insertions(+), 302 deletions(-) create mode 100644 changelogs/fragments/ansible-doc-version_added-collection.yml create mode 100644 changelogs/fragments/ansible-test-version-validation.yml diff --git a/changelogs/fragments/ansible-doc-version_added-collection.yml b/changelogs/fragments/ansible-doc-version_added-collection.yml new file mode 100644 index 00000000000..5c74c44e9e2 --- /dev/null +++ b/changelogs/fragments/ansible-doc-version_added-collection.yml @@ -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." diff --git a/changelogs/fragments/ansible-test-version-validation.yml b/changelogs/fragments/ansible-test-version-validation.yml new file mode 100644 index 00000000000..25c3475598b --- /dev/null +++ b/changelogs/fragments/ansible-test-version-validation.yml @@ -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." diff --git a/docs/docsite/rst/dev_guide/testing_validate-modules.rst b/docs/docsite/rst/dev_guide/testing_validate-modules.rst index 376ae6f155e..850b3bd71d2 100644 --- a/docs/docsite/rst/dev_guide/testing_validate-modules.rst +++ b/docs/docsite/rst/dev_guide/testing_validate-modules.rst @@ -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-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-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-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) 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 diff --git a/lib/ansible/cli/__init__.py b/lib/ansible/cli/__init__.py index 6979cb46a6a..3f5dacc0dd5 100644 --- a/lib/ansible/cli/__init__.py +++ b/lib/ansible/cli/__init__.py @@ -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) if verbosity_arg: 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') return options diff --git a/lib/ansible/cli/console.py b/lib/ansible/cli/console.py index a4f1f5dde04..3c600ee8488 100644 --- a/lib/ansible/cli/console.py +++ b/lib/ansible/cli/console.py @@ -397,7 +397,7 @@ class ConsoleCLI(CLI, cmd.Cmd): def module_args(self, 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()) def run(self): diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index bfb67e66ff7..039fc2ebfb7 100644 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -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._collection_finder import _get_collection_name_from_path 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() @@ -218,7 +218,7 @@ class DocCLI(CLI): plugin_docs = {} for plugin in context.CLIARGS['args']: 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: display.warning("%s %s not found in:\n%s\n" % (plugin_type, plugin, search_paths)) continue @@ -281,8 +281,13 @@ class DocCLI(CLI): if filename is None: 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: - 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: display.vvv(traceback.format_exc()) raise AnsibleError( @@ -319,13 +324,20 @@ class DocCLI(CLI): return clean_ns @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 - filename = loader.find_plugin(plugin, mod_type='.py', ignore_deprecated=True, check_aliases=True) - if filename is None: + result = loader.find_plugin_with_context(plugin, mod_type='.py', ignore_deprecated=True, check_aliases=True) + if result is None: 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 # an error @@ -346,6 +358,7 @@ class DocCLI(CLI): raise ValueError('%s did not contain a DOCUMENTATION attribute' % plugin) doc['filename'] = filename + untag_versions_and_dates(doc, '%s:' % (collection_name, ), is_module=(plugin_type == 'module')) return doc, plainexamples, returndocs, metadata @staticmethod diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index 477a36b7b07..f837cafa833 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -487,7 +487,7 @@ class TaskExecutor: '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"`, ' '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: variables[loop_var] = item diff --git a/lib/ansible/module_utils/urls.py b/lib/ansible/module_utils/urls.py index 69ae921c6f5..16fd82d847a 100644 --- a/lib/ansible/module_utils/urls.py +++ b/lib/ansible/module_utils/urls.py @@ -1528,7 +1528,8 @@ def url_argument_spec(): ''' return dict( 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'), use_proxy=dict(type='bool', default=True), validate_certs=dict(type='bool', default=True), diff --git a/lib/ansible/modules/copy.py b/lib/ansible/modules/copy.py index 0c15016bc83..382ac376fdb 100644 --- a/lib/ansible/modules/copy.py +++ b/lib/ansible/modules/copy.py @@ -517,7 +517,7 @@ def main(): ) 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'] b_src = to_bytes(src, errors='surrogate_or_strict') diff --git a/lib/ansible/modules/cron.py b/lib/ansible/modules/cron.py index 4b4f421c5e7..0e102b03875 100644 --- a/lib/ansible/modules/cron.py +++ b/lib/ansible/modules/cron.py @@ -621,12 +621,12 @@ def main(): if not name: module.deprecate( msg="The 'name' parameter will be required in future releases.", - version='2.12' + version='ansible.builtin:2.12' ) if reboot: module.deprecate( 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: diff --git a/lib/ansible/modules/get_url.py b/lib/ansible/modules/get_url.py index ea033d9cfec..d9f02217b4d 100644 --- a/lib/ansible/modules/get_url.py +++ b/lib/ansible/modules/get_url.py @@ -449,10 +449,10 @@ def main(): ) 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'): - 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'] dest = module.params['dest'] diff --git a/lib/ansible/modules/systemd.py b/lib/ansible/modules/systemd.py index 25ac688cb3b..0ecd8ea5887 100644 --- a/lib/ansible/modules/systemd.py +++ b/lib/ansible/modules/systemd.py @@ -360,7 +360,7 @@ def main(): ''' Set CLI options depending on params ''' if module.params['user'] is not None: # 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']: module.params['scope'] = 'user' else: diff --git a/lib/ansible/modules/uri.py b/lib/ansible/modules/uri.py index b357fd0e0a1..fec84d83203 100644 --- a/lib/ansible/modules/uri.py +++ b/lib/ansible/modules/uri.py @@ -615,7 +615,7 @@ def main(): ) 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'] body = module.params['body'] diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 17b5f2fe76f..bf02c5c6b49 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -79,7 +79,7 @@ class Playbook: self._loader.set_basedir(cur_basedir) raise AnsibleParserError("A playbook must be a list of plays, got a %s instead" % type(ds), obj=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 # 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 '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) if pb is not None: self._entries.extend(pb._entries) diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py index 2fadf77487e..a8b6765ba6e 100644 --- a/lib/ansible/playbook/conditional.py +++ b/lib/ansible/playbook/conditional.py @@ -134,7 +134,7 @@ class Conditional: conditional = templar.template(conditional, disable_lookups=disable_lookups) 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' - ' 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 == "": return conditional diff --git a/lib/ansible/playbook/helpers.py b/lib/ansible/playbook/helpers.py index 2a8c5aecbe5..6c4ccf9f9be 100644 --- a/lib/ansible/playbook/helpers.py +++ b/lib/ansible/playbook/helpers.py @@ -156,7 +156,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h is_static = True elif t.static is not None: 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 else: 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 " "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 " - "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) 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, ) 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: 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: 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 if is_static: diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py index b93cdbc6667..48beb746007 100644 --- a/lib/ansible/playbook/play_context.py +++ b/lib/ansible/playbook/play_context.py @@ -342,7 +342,7 @@ class PlayContext(Base): """ helper function to create privilege escalation commands """ display.deprecated( "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: diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index e9e5d876ef8..2475b61c7c8 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -163,7 +163,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch): raise AnsibleError("you must specify a value when using %s" % k, obj=ds) new_ds['loop_with'] = loop_name 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): ''' @@ -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: 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" - " 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 elif C.INVALID_TASK_ATTRIBUTE_FAILED or k in self._valid_attrs: new_ds[k] = v diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index e30ba0d9026..9e2b88b8683 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -822,7 +822,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): msg = "Setting the async dir from the environment keyword " \ "ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \ "shell option instead" - self._display.deprecated(msg, "2.12") + self._display.deprecated(msg, "ansible.builtin:2.12") else: # ANSIBLE_ASYNC_DIR is not set on the task, we get the value # from the shell option and temporarily add to the environment diff --git a/lib/ansible/plugins/action/async_status.py b/lib/ansible/plugins/action/async_status.py index 96888d98da3..40ce563e41a 100644 --- a/lib/ansible/plugins/action/async_status.py +++ b/lib/ansible/plugins/action/async_status.py @@ -33,7 +33,7 @@ class ActionModule(ActionBase): msg = "Setting the async dir from the environment keyword " \ "ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \ "shell option instead" - self._display.deprecated(msg, "2.12") + self._display.deprecated(msg, "ansible.builtin:2.12") else: # inject the async directory based on the shell option into the # module args diff --git a/lib/ansible/plugins/cache/__init__.py b/lib/ansible/plugins/cache/__init__.py index a7a1338ba44..3b337835d0a 100644 --- a/lib/ansible/plugins/cache/__init__.py +++ b/lib/ansible/plugins/cache/__init__.py @@ -50,7 +50,7 @@ class FactCache(RealFactCache): ' ansible.vars.fact_cache.FactCache. If you are looking for the class' ' to subclass for a cache plugin, you want' ' ansible.plugins.cache.BaseCacheModule or one of its subclasses.', - version='2.12') + version='ansible.builtin:2.12') super(FactCache, self).__init__(*args, **kwargs) @@ -62,7 +62,7 @@ class BaseCacheModule(AnsiblePlugin): def __init__(self, *args, **kwargs): # Third party code is not using cache_loader to load plugin - fall back to previous behavior 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 = resource_from_fqcr(self.__module__) super(BaseCacheModule, self).__init__() diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py index a4d1e30a08a..2ad7511ccc4 100644 --- a/lib/ansible/plugins/callback/__init__.py +++ b/lib/ansible/plugins/callback/__init__.py @@ -242,7 +242,7 @@ class CallbackBase(AnsiblePlugin): def _get_item(self, result): ''' here for backwards compat, really should have always been named: _get_item_label''' 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) def _process_items(self, result): diff --git a/lib/ansible/plugins/connection/__init__.py b/lib/ansible/plugins/connection/__init__.py index 818d3416922..230a6d777ee 100644 --- a/lib/ansible/plugins/connection/__init__.py +++ b/lib/ansible/plugins/connection/__init__.py @@ -237,28 +237,28 @@ class ConnectionBase(AnsiblePlugin): def check_become_success(self, b_output): display.deprecated( "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) def check_password_prompt(self, b_output): display.deprecated( "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) def check_incorrect_password(self, b_output): display.deprecated( "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) def check_missing_password(self, b_output): display.deprecated( "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) diff --git a/lib/ansible/plugins/inventory/__init__.py b/lib/ansible/plugins/inventory/__init__.py index c0ecb4c1cd1..b82dcdea95a 100644 --- a/lib/ansible/plugins/inventory/__init__.py +++ b/lib/ansible/plugins/inventory/__init__.py @@ -291,19 +291,19 @@ class DeprecatedCache(object): 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). ' '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] def set(self, key, value): 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 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.set_cache_plugin() 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) diff --git a/lib/ansible/plugins/inventory/script.py b/lib/ansible/plugins/inventory/script.py index d28dc3c4bf3..05bfb0cfd8c 100644 --- a/lib/ansible/plugins/inventory/script.py +++ b/lib/ansible/plugins/inventory/script.py @@ -97,7 +97,7 @@ class InventoryModule(BaseInventoryPlugin, Cacheable): display.deprecated( msg="The 'cache' option is deprecated for the script inventory plugin. " "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 diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py index eb07d044b11..056ff44284a 100644 --- a/lib/ansible/plugins/loader.py +++ b/lib/ansible/plugins/loader.py @@ -130,7 +130,7 @@ class PluginLoadContext(object): self.deprecation_warnings = [] self.resolved = False - def record_deprecation(self, name, deprecation): + def record_deprecation(self, name, deprecation, collection_name): if not deprecation: return self @@ -142,11 +142,14 @@ class PluginLoadContext(object): removal_version = None if not warning_text: 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: - 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: - 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 if removal_date: @@ -374,7 +377,7 @@ class PluginLoader: if type_name in C.CONFIGURABLE_PLUGINS: dstring = AnsibleLoader(getattr(module, 'DOCUMENTATION', ''), file_name=path).get_single_data() 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): C.config.initialize_plugin_configuration_definitions(type_name, name, dstring['options']) @@ -444,7 +447,7 @@ class PluginLoader: deprecation = routing_metadata.get('deprecation', None) # 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) @@ -453,12 +456,12 @@ class PluginLoader: removal_date = tombstone.get('removal_date') removal_version = tombstone.get('removal_version') 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 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: - 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_version = removal_version plugin_load_context.resolved = True diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index c18c3830000..d10ff6f1633 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -71,7 +71,7 @@ def SharedPluginLoaderObj(): '''This only exists for backwards compat, do not use. ''' display.deprecated('SharedPluginLoaderObj is deprecated, please directly use ansible.plugins.loader', - version='2.11') + version='ansible.builtin:2.11') 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", 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", - version='2.12') + version='ansible.builtin:2.12') included_file._task.tags = tags block_list = load_list_of_blocks( diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index aa779e82f30..3a46f199aea 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -574,7 +574,7 @@ class Templar: def set_available_variables(self, variables): display.deprecated( 'set_available_variables is being deprecated. Use "@available_variables.setter" instead.', - version='2.13' + version='ansible.builtin:2.13' ) self.available_variables = variables diff --git a/lib/ansible/utils/display.py b/lib/ansible/utils/display.py index c83f5142f4b..e305d859adb 100644 --- a/lib/ansible/utils/display.py +++ b/lib/ansible/utils/display.py @@ -26,6 +26,7 @@ import locale import logging import os import random +import re import subprocess import sys import textwrap @@ -36,8 +37,8 @@ from termios import TIOCGWINSZ from ansible import constants as C from ansible.errors import AnsibleError, AnsibleAssertionError -from ansible.module_utils._text import to_bytes, to_text -from ansible.module_utils.six import with_metaclass +from ansible.module_utils._text import to_bytes, to_text, to_native +from ansible.module_utils.six import with_metaclass, string_types from ansible.utils.color import stringc from ansible.utils.singleton import Singleton from ansible.utils.unsafe_proxy import wrap_var @@ -50,6 +51,9 @@ except NameError: pass +TAGGED_VERSION_RE = re.compile('^([^.]+.[^.]+):(.*)$') + + class FilterBlackList(logging.Filter): def __init__(self, blacklist): self.blacklist = [logging.Filter(name) for name in blacklist] @@ -258,9 +262,34 @@ class Display(with_metaclass(Singleton, object)): if not removed: 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: - 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: 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" diff --git a/lib/ansible/utils/plugin_docs.py b/lib/ansible/utils/plugin_docs.py index af789277084..b46e50de2c6 100644 --- a/lib/ansible/utils/plugin_docs.py +++ b/lib/ansible/utils/plugin_docs.py @@ -40,7 +40,70 @@ def merge_fragment(target, source): 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', []) @@ -80,6 +143,12 @@ def add_fragments(doc, filename, fragment_loader): 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: notes = fragment.pop('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)))) -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. """ data = read_docstring(filename, verbose=verbose, ignore_errors=ignore_errors) - # add fragments to documentation 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'] diff --git a/lib/ansible/utils/unsafe_proxy.py b/lib/ansible/utils/unsafe_proxy.py index a72479c793f..936d881b10c 100644 --- a/lib/ansible/utils/unsafe_proxy.py +++ b/lib/ansible/utils/unsafe_proxy.py @@ -83,7 +83,7 @@ class UnsafeProxy(object): from ansible.utils.display import Display Display().deprecated( '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. # This conditional and conversion exists to sanity check the values diff --git a/lib/ansible/vars/fact_cache.py b/lib/ansible/vars/fact_cache.py index 7b6021a1cbd..cab97d3840b 100644 --- a/lib/ansible/vars/fact_cache.py +++ b/lib/ansible/vars/fact_cache.py @@ -99,7 +99,7 @@ class FactCache(MutableMapping): display.deprecated('Calling FactCache().update(key, value) is deprecated. Use' ' FactCache().first_order_merge(key, value) if you want the old' ' 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) elif len(args) == 1: diff --git a/test/integration/targets/incidental_xml/tasks/test-get-element-content.yml b/test/integration/targets/incidental_xml/tasks/test-get-element-content.yml index 58ca7767e76..e1f29945c7c 100644 --- a/test/integration/targets/incidental_xml/tasks/test-get-element-content.yml +++ b/test/integration/targets/incidental_xml/tasks/test-get-element-content.yml @@ -36,7 +36,7 @@ - get_element_attribute_wrong.matches[0]['rating']['subjective'] == 'true' - 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].version == '2.12' + - get_element_attribute_wrong.deprecations[0].version == 'ansible.builtin:2.12' - name: Get element text xml: diff --git a/test/lib/ansible_test/_data/sanity/pylint/plugins/deprecated.py b/test/lib/ansible_test/_data/sanity/pylint/plugins/deprecated.py index a6f8973ae36..1ac2a74afda 100644 --- a/test/lib/ansible_test/_data/sanity/pylint/plugins/deprecated.py +++ b/test/lib/ansible_test/_data/sanity/pylint/plugins/deprecated.py @@ -35,7 +35,7 @@ MSGS = { "Display.deprecated or AnsibleModule.deprecate", "ansible-invalid-deprecated-version", "Used when a call to Display.deprecated specifies an invalid " - "Ansible version number", + "tagged Ansible version number", {'minversion': (2, 6)}), 'E9504': ("Deprecated version (%r) found in call to Display.deprecated " "or AnsibleModule.deprecate", @@ -48,30 +48,59 @@ MSGS = { "Display.deprecated or AnsibleModule.deprecate", "collection-invalid-deprecated-version", "Used when a call to Display.deprecated specifies an invalid " - "collection version number", + "tagged collection version number", {'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", "ansible-deprecated-date", "Used when a call to Display.deprecated specifies a date " "before today", {'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", "ansible-invalid-deprecated-date", "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)}), - 'E9508': ("Both version and date found in call to " + 'E9510': ("Both version and date found in call to " "Display.deprecated or AnsibleModule.deprecate", "ansible-deprecated-both-version-and-date", "Only one of version and date must be specified", {'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])) +TAGGED_VERSION_RE = re.compile('^([^.]+.[^.]+):(.*)$') + def _get_expr_name(node): """Funciton to get either ``attrname`` or ``name`` from ``node.func.expr`` @@ -109,11 +138,11 @@ class AnsibleDeprecatedChecker(BaseChecker): msgs = MSGS options = ( - ('is-collection', { - 'default': False, - 'type': 'yn', - 'metavar': '', - 'help': 'Whether this is a collection or not.', + ('collection-name', { + 'default': None, + 'type': 'string', + 'metavar': '', + 'help': 'The collection\'s name used to check tagged version numbers in deprecations.', }), ('collection-version', { 'default': None, @@ -125,17 +154,71 @@ class AnsibleDeprecatedChecker(BaseChecker): def __init__(self, *args, **kwargs): self.collection_version = None - self.is_collection = False - self.version_constructor = LooseVersion + self.collection_name = None super(AnsibleDeprecatedChecker, self).__init__(*args, **kwargs) def set_option(self, optname, value, action=None, optdict=None): super(AnsibleDeprecatedChecker, self).set_option(optname, value, action, optdict) if optname == 'collection-version' and value is not None: - self.version_constructor = SemanticVersion - self.collection_version = self.version_constructor(self.config.collection_version) - if optname == 'is-collection': - self.is_collection = self.config.is_collection + self.collection_version = SemanticVersion(self.config.collection_version) + if optname == 'collection-name' and value is not None: + self.collection_name = self.config.collection_name + + 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())) def visit_call(self, node): @@ -169,26 +252,11 @@ class AnsibleDeprecatedChecker(BaseChecker): self.add_message('ansible-deprecated-both-version-and-date', node=node) 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: - try: - if parse_isodate(date) < 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,)) + self._check_date(node, date) + + if version: + self._check_version(node, version) except AttributeError: # Not the type of node we are interested in pass diff --git a/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/main.py b/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/main.py index e05f7c4b62f..f45f59a8f77 100644 --- a/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/main.py +++ b/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/main.py @@ -41,10 +41,10 @@ import yaml from ansible import __version__ as ansible_version from ansible.executor.module_common import REPLACER_WINDOWS 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.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 .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec @@ -258,9 +258,12 @@ class ModuleValidator(Validator): self.StrictVersion = StrictVersion self.collection = collection + self.collection_name = 'ansible.builtin' if self.collection: self.Version = 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.collection_version = None if collection_version is not None: @@ -873,6 +876,8 @@ class ModuleValidator(Validator): for error in errors: path = [str(p) for p in error.path] + local_error_code = getattr(error, 'ansible_error_code', error_code) + if isinstance(error.data, dict): error_message = humanize_error(error.data, error) else: @@ -885,10 +890,28 @@ class ModuleValidator(Validator): self.reporter.error( path=self.object_path, - code=error_code, + code=local_error_code, 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): doc_info = self._get_docs() doc = None @@ -966,6 +989,8 @@ class ModuleValidator(Validator): doc_info['DOCUMENTATION']['lineno'], self.name, 'DOCUMENTATION' ) + if doc: + tag_versions_and_dates(doc, '%s:' % (self.collection_name, ), is_module=True) for error in errors: self.reporter.error( path=self.object_path, @@ -981,7 +1006,8 @@ class ModuleValidator(Validator): missing_fragment = False with CaptureStd(): 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: fragment = doc['extends_documentation_fragment'] self.reporter.error( @@ -1002,7 +1028,7 @@ class ModuleValidator(Validator): ) 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: self.reporter.error( @@ -1024,9 +1050,8 @@ class ModuleValidator(Validator): doc, doc_schema( os.readlink(self.object_path).split('.')[0], - version_added=not bool(self.collection), - deprecated_module=deprecated, for_collection=bool(self.collection), + deprecated_module=deprecated, ), 'DOCUMENTATION', 'invalid-documentation', @@ -1037,9 +1062,8 @@ class ModuleValidator(Validator): doc, doc_schema( self.object_name.split('.')[0], - version_added=not bool(self.collection), - deprecated_module=deprecated, for_collection=bool(self.collection), + deprecated_module=deprecated, ), 'DOCUMENTATION', 'invalid-documentation', @@ -1088,7 +1112,8 @@ class ModuleValidator(Validator): data, errors, traces = parse_yaml(doc_info['RETURN']['value'], doc_info['RETURN']['lineno'], 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: self.reporter.error( @@ -1142,8 +1167,10 @@ class ModuleValidator(Validator): # Make sure they give the same version or date. routing_date = routing_deprecation.get('removal_date') routing_version = routing_deprecation.get('removal_version') - documentation_date = doc_deprecation.get('removed_at_date') - documentation_version = doc_deprecation.get('removed_in') + # The versions and dates in the module documentation are auto-tagged, so remove the tag + # 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: self.reporter.error( path=self.object_path, @@ -1166,23 +1193,26 @@ class ModuleValidator(Validator): def _check_version_added(self, doc, existing_doc): version_added_raw = doc.get('version_added') 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: version_added = doc.get('version_added', '0.0') - if self._is_new_module() or version_added != 'historical': - self.reporter.error( - path=self.object_path, - code='module-invalid-version-added', - msg='version_added is not a valid version number: %r' % version_added - ) + if self._is_new_module() or version_added != 'ansible.builtin:historical': + # already reported during schema validation, except: + if version_added == 'ansible.builtin:historical': + self.reporter.error( + path=self.object_path, + code='module-invalid-version-added', + msg='version_added is not a valid version number: %r' % 'historical' + ) return if existing_doc and str(version_added_raw) != str(existing_doc.get('version_added')): self.reporter.error( path=self.object_path, code='module-incorrect-version-added', - msg='version_added should be %r. Currently %r' % (existing_doc.get('version_added'), - version_added_raw) + msg='version_added should be %r. Currently %r' % ( + 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(): @@ -1196,10 +1226,11 @@ class ModuleValidator(Validator): self.reporter.error( path=self.object_path, 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: spec, args, kwargs = get_argument_spec(self.path, self.collection) except AnsibleModuleNotInitialized: @@ -1221,7 +1252,9 @@ class ModuleValidator(Validator): ) 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) @@ -1433,7 +1466,7 @@ class ModuleValidator(Validator): try: 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: # Cannot merge fragments return @@ -1497,7 +1530,8 @@ class ModuleValidator(Validator): removed_at_date = data.get('removed_at_date', None) if removed_at_date is not None: 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 if context: msg += " found in %s" % " -> ".join(context) @@ -1517,7 +1551,8 @@ class ModuleValidator(Validator): for deprecated_alias in deprecated_aliases: if 'name' in deprecated_alias and 'date' in deprecated_alias: 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 if context: msg += " found in %s" % " -> ".join(context) @@ -1549,7 +1584,8 @@ class ModuleValidator(Validator): removed_in_version = data.get('removed_in_version', None) if removed_in_version is not None: 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 if context: msg += " found in %s" % " -> ".join(context) @@ -1561,22 +1597,15 @@ class ModuleValidator(Validator): msg=msg, ) except ValueError: - msg = "Argument '%s' in argument_spec" % arg - if context: - 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, - ) + # Has been caught in schema validation + pass if deprecated_aliases is not None: for deprecated_alias in deprecated_aliases: if 'name' in deprecated_alias and 'version' in deprecated_alias: 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 if context: msg += " found in %s" % " -> ".join(context) @@ -1589,17 +1618,8 @@ class ModuleValidator(Validator): msg=msg, ) except ValueError: - msg = "Argument '%s' in argument_spec" % arg - if context: - 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, - ) + # Has been caught in schema validation + pass aliases = data.get('aliases', []) if arg in aliases: @@ -1978,7 +1998,8 @@ class ModuleValidator(Validator): with CaptureStd(): 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 {} except AssertionError: fragment = doc['extends_documentation_fragment'] @@ -2031,6 +2052,7 @@ class ModuleValidator(Validator): continue 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: existing_version = existing_options.get(name, {}).get('version_added') if existing_version: @@ -2052,19 +2074,7 @@ class ModuleValidator(Validator): str(details.get('version_added', '0.0')) ) except ValueError: - version_added = details.get('version_added', '0.0') - 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 + # already reported during schema validation continue if (strict_ansible_version != mod_version_added and @@ -2148,7 +2158,16 @@ class ModuleValidator(Validator): pass if 'removed_in' in docs['deprecated']: 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: end_of_deprecation_should_be_removed_only = False 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: with ModuleValidator(docs_path, base_branch=self.base_branch, git_cache=self.git_cache) as docs_mv: 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() if not self._just_docs() and not end_of_deprecation_should_be_removed_only: diff --git a/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py b/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py index 78551a317a2..f6c7f584b57 100644 --- a/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py +++ b/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py @@ -9,9 +9,13 @@ __metaclass__ = type import datetime import re +from functools import partial + 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.common.collections import is_iterable +from ansible.utils.version import SemanticVersion +from distutils.version import StrictVersion 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$') +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): if not callable(v): raise ValueInvalid('not a valid value') @@ -101,15 +185,8 @@ def options_with_apply_defaults(v): return v -def isodate(v): - try: - parse_isodate(v) - except ValueError as e: - raise Invalid(str(e)) - return v - - -def argument_spec_schema(): +def argument_spec_schema(for_collection, dates_tagged=True): + dates_tagged = 'always' if dates_tagged else 'never' any_string_types = Any(*string_types) schema = { any_string_types: { @@ -125,17 +202,17 @@ def argument_spec_schema(): 'no_log': bool, 'aliases': Any(list_string_types, tuple(list_string_types)), 'apply_defaults': bool, - 'removed_in_version': Any(float, *string_types), - 'removed_at_date': Any(isodate), + 'removed_in_version': version(for_collection, tagged='always'), + 'removed_at_date': date(tagged=dates_tagged), 'options': Self, 'deprecated_aliases': Any([Any( { Required('name'): Any(*string_types), - Required('date'): Any(isodate), + Required('date'): date(tagged=dates_tagged), }, { 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) -def ansible_module_kwargs_schema(): +def ansible_module_kwargs_schema(for_collection, dates_tagged=True): schema = { - 'argument_spec': argument_spec_schema(), + 'argument_spec': argument_spec_schema(for_collection, dates_tagged=dates_tagged), 'bypass_checks': bool, 'no_log': bool, 'check_invalid_arguments': Any(None, bool), @@ -172,48 +249,49 @@ json_value = Schema(Any( )) -suboption_schema = Schema( - { - Required('description'): Any(list_string_types, *string_types), - 'required': bool, - 'choices': list, - 'aliases': Any(list_string_types), - 'version_added': Any(float, *string_types), - 'default': json_value, - # 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'), - # 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'), - # Recursive suboptions - 'suboptions': Any(None, *list({str_type: Self} for str_type in string_types)), - }, - extra=PREVENT_EXTRA -) +def list_dict_option_schema(for_collection): + suboption_schema = Schema( + { + Required('description'): Any(list_string_types, *string_types), + 'required': bool, + 'choices': list, + 'aliases': Any(list_string_types), + 'version_added': version(for_collection, tagged='always', error_code='option-invalid-version-added'), + 'default': json_value, + # 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'), + # 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'), + # Recursive suboptions + 'suboptions': Any(None, *list({str_type: Self} for str_type in string_types)), + }, + extra=PREVENT_EXTRA + ) -# This generates list of dicts with keys from string_types and suboption_schema value -# for example in Python 3: {str: suboption_schema} -list_dict_suboption_schema = [{str_type: suboption_schema} for str_type in string_types] + # This generates list of dicts with keys from string_types and suboption_schema value + # for example in Python 3: {str: suboption_schema} + list_dict_suboption_schema = [{str_type: suboption_schema} for str_type in string_types] -option_schema = Schema( - { - Required('description'): Any(list_string_types, *string_types), - 'required': bool, - 'choices': list, - 'aliases': Any(list_string_types), - 'version_added': Any(float, *string_types), - 'default': json_value, - 'suboptions': Any(None, *list_dict_suboption_schema), - # 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'), - # 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'), - }, - extra=PREVENT_EXTRA -) + option_schema = Schema( + { + Required('description'): Any(list_string_types, *string_types), + 'required': bool, + 'choices': list, + 'aliases': Any(list_string_types), + 'version_added': version(for_collection, tagged='always', error_code='option-invalid-version-added'), + 'default': json_value, + 'suboptions': Any(None, *list_dict_suboption_schema), + # 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'), + # 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'), + }, + extra=PREVENT_EXTRA + ) -# This generates list of dicts with keys from string_types and option_schema value -# for example in Python 3: {str: option_schema} -list_dict_option_schema = [{str_type: option_schema} for str_type in string_types] + # This generates list of dicts with keys from string_types and option_schema value + # for example in Python 3: {str: option_schema} + return [{str_type: option_schema} for str_type in string_types] def return_contains(v): @@ -228,51 +306,52 @@ def return_contains(v): return v -return_contains_schema = Any( - 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: { +def return_schema(for_collection): + return_contains_schema = Any( + All( + Schema( + { 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'), - 'version_added': Any(float, *string_types), + '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), + '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({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): @@ -283,13 +362,13 @@ def deprecation_schema(for_collection): } date_schema = { - Required('removed_at_date'): Any(isodate), + Required('removed_at_date'): date(tagged='always'), } date_schema.update(main_fields) if for_collection: version_schema = { - Required('removed_in'): Any(float, *string_types), + Required('removed_in'): version(for_collection, tagged='always'), } else: version_schema = { @@ -297,13 +376,16 @@ def deprecation_schema(for_collection): # Deprecation cycle changed at 2.4 (though not retroactively) # 2.3 -> removed_in: "2.5" + 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) return Any( - Schema(date_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") -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('_'): 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), 'requirements': list_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) } - if version_added: - doc_schema_dict[Required('version_added')] = Any(float, *string_types) - else: + if for_collection: # 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: deprecation_required_scheme = { diff --git a/test/lib/ansible_test/_internal/sanity/pylint.py b/test/lib/ansible_test/_internal/sanity/pylint.py index bcda7375055..1ccf436d277 100644 --- a/test/lib/ansible_test/_internal/sanity/pylint.py +++ b/test/lib/ansible_test/_internal/sanity/pylint.py @@ -239,12 +239,10 @@ class PylintTest(SanitySingleVersion): ] + paths 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: cmd.extend(['--collection-version', collection_detail.version]) - else: - cmd.extend(['--enable', 'ansible-deprecated-version']) append_python_path = [plugin_dir] diff --git a/test/support/integration/plugins/modules/aws_az_info.py b/test/support/integration/plugins/modules/aws_az_info.py index eccbf4d7d44..0685a0b5746 100644 --- a/test/support/integration/plugins/modules/aws_az_info.py +++ b/test/support/integration/plugins/modules/aws_az_info.py @@ -86,7 +86,7 @@ def main(): module = AnsibleAWSModule(argument_spec=argument_spec) 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()) diff --git a/test/support/integration/plugins/modules/azure_rm_functionapp_info.py b/test/support/integration/plugins/modules/azure_rm_functionapp_info.py index 0cd5b6f60be..19652577dd5 100644 --- a/test/support/integration/plugins/modules/azure_rm_functionapp_info.py +++ b/test/support/integration/plugins/modules/azure_rm_functionapp_info.py @@ -136,7 +136,7 @@ class AzureRMFunctionAppInfo(AzureRMModuleBase): is_old_facts = self.module._name == 'azure_rm_functionapp_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: setattr(self, key, kwargs[key]) diff --git a/test/support/integration/plugins/modules/azure_rm_mariadbconfiguration_info.py b/test/support/integration/plugins/modules/azure_rm_mariadbconfiguration_info.py index ad38f1255fe..df1b41da13d 100644 --- a/test/support/integration/plugins/modules/azure_rm_mariadbconfiguration_info.py +++ b/test/support/integration/plugins/modules/azure_rm_mariadbconfiguration_info.py @@ -139,7 +139,8 @@ class AzureRMMariaDbConfigurationInfo(AzureRMModuleBase): def exec_module(self, **kwargs): is_old_facts = self.module._name == 'azure_rm_mariadbconfiguration_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: setattr(self, key, kwargs[key]) diff --git a/test/support/integration/plugins/modules/azure_rm_mariadbdatabase_info.py b/test/support/integration/plugins/modules/azure_rm_mariadbdatabase_info.py index 61e33015b1c..dbbb2da970e 100644 --- a/test/support/integration/plugins/modules/azure_rm_mariadbdatabase_info.py +++ b/test/support/integration/plugins/modules/azure_rm_mariadbdatabase_info.py @@ -145,7 +145,8 @@ class AzureRMMariaDbDatabaseInfo(AzureRMModuleBase): def exec_module(self, **kwargs): is_old_facts = self.module._name == 'azure_rm_mariadbdatabase_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: setattr(self, key, kwargs[key]) diff --git a/test/support/integration/plugins/modules/azure_rm_mariadbfirewallrule_info.py b/test/support/integration/plugins/modules/azure_rm_mariadbfirewallrule_info.py index 45557b51133..c6857bea482 100644 --- a/test/support/integration/plugins/modules/azure_rm_mariadbfirewallrule_info.py +++ b/test/support/integration/plugins/modules/azure_rm_mariadbfirewallrule_info.py @@ -141,7 +141,8 @@ class AzureRMMariaDbFirewallRuleInfo(AzureRMModuleBase): def exec_module(self, **kwargs): is_old_facts = self.module._name == 'azure_rm_mariadbfirewallrule_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: setattr(self, key, kwargs[key]) diff --git a/test/support/integration/plugins/modules/azure_rm_mariadbserver_info.py b/test/support/integration/plugins/modules/azure_rm_mariadbserver_info.py index ffe52c5d37b..5e8e1a82da0 100644 --- a/test/support/integration/plugins/modules/azure_rm_mariadbserver_info.py +++ b/test/support/integration/plugins/modules/azure_rm_mariadbserver_info.py @@ -193,7 +193,7 @@ class AzureRMMariaDbServerInfo(AzureRMModuleBase): def exec_module(self, **kwargs): is_old_facts = self.module._name == 'azure_rm_mariadbserver_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: setattr(self, key, kwargs[key]) diff --git a/test/support/integration/plugins/modules/azure_rm_resource_info.py b/test/support/integration/plugins/modules/azure_rm_resource_info.py index 354cd795783..5d553a0d42c 100644 --- a/test/support/integration/plugins/modules/azure_rm_resource_info.py +++ b/test/support/integration/plugins/modules/azure_rm_resource_info.py @@ -336,7 +336,7 @@ class AzureRMResourceInfo(AzureRMModuleBase): def exec_module(self, **kwargs): is_old_facts = self.module._name == 'azure_rm_resource_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: setattr(self, key, kwargs[key]) diff --git a/test/support/integration/plugins/modules/azure_rm_webapp_info.py b/test/support/integration/plugins/modules/azure_rm_webapp_info.py index 4a3b4cd484b..e722ffdf165 100644 --- a/test/support/integration/plugins/modules/azure_rm_webapp_info.py +++ b/test/support/integration/plugins/modules/azure_rm_webapp_info.py @@ -265,7 +265,7 @@ class AzureRMWebAppInfo(AzureRMModuleBase): def exec_module(self, **kwargs): is_old_facts = self.module._name == 'azure_rm_webapp_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: setattr(self, key, kwargs[key]) diff --git a/test/support/integration/plugins/modules/cloudformation_info.py b/test/support/integration/plugins/modules/cloudformation_info.py index f62b80235dd..d10429326d1 100644 --- a/test/support/integration/plugins/modules/cloudformation_info.py +++ b/test/support/integration/plugins/modules/cloudformation_info.py @@ -302,7 +302,7 @@ def main(): is_old_facts = module._name == 'cloudformation_facts' if is_old_facts: 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) diff --git a/test/support/integration/plugins/modules/docker_swarm.py b/test/support/integration/plugins/modules/docker_swarm.py index 4fd4c875c41..e802e202457 100644 --- a/test/support/integration/plugins/modules/docker_swarm.py +++ b/test/support/integration/plugins/modules/docker_swarm.py @@ -450,7 +450,7 @@ class SwarmManager(DockerBaseClass): if self.state == 'inspect': self.client.module.deprecate( "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)() diff --git a/test/support/integration/plugins/modules/ec2.py b/test/support/integration/plugins/modules/ec2.py index 91503bbf8e2..b01c5a66d5e 100644 --- a/test/support/integration/plugins/modules/ec2.py +++ b/test/support/integration/plugins/modules/ec2.py @@ -1695,7 +1695,7 @@ def main(): module.deprecate( 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', - version='2.14') + version='ansible.builtin:2.14') if not HAS_BOTO: module.fail_json(msg='boto required for this module') diff --git a/test/support/integration/plugins/modules/ec2_ami_info.py b/test/support/integration/plugins/modules/ec2_ami_info.py index 41e1aa83f9c..e7cbe0532c9 100644 --- a/test/support/integration/plugins/modules/ec2_ami_info.py +++ b/test/support/integration/plugins/modules/ec2_ami_info.py @@ -270,7 +270,7 @@ def main(): module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) 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') diff --git a/test/support/integration/plugins/modules/ec2_eni_info.py b/test/support/integration/plugins/modules/ec2_eni_info.py index 99922a84d1d..31efbfdd389 100644 --- a/test/support/integration/plugins/modules/ec2_eni_info.py +++ b/test/support/integration/plugins/modules/ec2_eni_info.py @@ -259,7 +259,7 @@ def main(): module = AnsibleModule(argument_spec=argument_spec) 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: module.fail_json(msg='boto3 required for this module') diff --git a/test/support/integration/plugins/modules/ec2_instance_info.py b/test/support/integration/plugins/modules/ec2_instance_info.py index 7615b958d38..cd8e0cd7d54 100644 --- a/test/support/integration/plugins/modules/ec2_instance_info.py +++ b/test/support/integration/plugins/modules/ec2_instance_info.py @@ -552,7 +552,7 @@ def main(): supports_check_mode=True ) 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: module.fail_json(msg='boto3 required for this module') diff --git a/test/support/integration/plugins/modules/iam_role.py b/test/support/integration/plugins/modules/iam_role.py index 71a5b0377ed..1027232be03 100644 --- a/test/support/integration/plugins/modules/iam_role.py +++ b/test/support/integration/plugins/modules/iam_role.py @@ -637,7 +637,7 @@ def main(): 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.' - ' 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('create_instance_profile'): diff --git a/test/support/integration/plugins/modules/k8s_info.py b/test/support/integration/plugins/modules/k8s_info.py index 99a8fd8cecb..37e80668959 100644 --- a/test/support/integration/plugins/modules/k8s_info.py +++ b/test/support/integration/plugins/modules/k8s_info.py @@ -142,7 +142,7 @@ class KubernetesInfoModule(KubernetesAnsibleModule): supports_check_mode=True, **kwargs) 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): self.client = self.get_api_client() diff --git a/test/support/integration/plugins/modules/openssl_certificate.py b/test/support/integration/plugins/modules/openssl_certificate.py index 4bd5e5c4682..d8af142e1ba 100644 --- a/test/support/integration/plugins/modules/openssl_certificate.py +++ b/test/support/integration/plugins/modules/openssl_certificate.py @@ -2654,7 +2654,7 @@ def main(): if provider == 'assertonly': module.deprecate("The 'assertonly' provider is deprecated; please see the examples of " "the 'openssl_certificate' module on how to replace it with other modules", - version='2.13') + version='ansible.builtin:2.13') elif provider == 'selfsigned': 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.') @@ -2702,7 +2702,7 @@ def main(): except AttributeError: 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': certificate = SelfSignedCertificate(module) elif provider == 'acme': diff --git a/test/support/integration/plugins/modules/openssl_certificate_info.py b/test/support/integration/plugins/modules/openssl_certificate_info.py index 2d7459ae9df..2b947a87776 100644 --- a/test/support/integration/plugins/modules/openssl_certificate_info.py +++ b/test/support/integration/plugins/modules/openssl_certificate_info.py @@ -845,7 +845,7 @@ def main(): except AttributeError: 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) elif backend == 'cryptography': if not CRYPTOGRAPHY_FOUND: diff --git a/test/support/integration/plugins/modules/openssl_csr.py b/test/support/integration/plugins/modules/openssl_csr.py index ea2cf68c2a4..b184cabe9d7 100644 --- a/test/support/integration/plugins/modules/openssl_csr.py +++ b/test/support/integration/plugins/modules/openssl_csr.py @@ -1091,7 +1091,7 @@ def main(): if module.params['version'] != 1: 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 '.' if not os.path.isdir(base_dir): @@ -1125,7 +1125,7 @@ def main(): except AttributeError: 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) elif backend == 'cryptography': if not CRYPTOGRAPHY_FOUND: diff --git a/test/support/integration/plugins/modules/openssl_privatekey.py b/test/support/integration/plugins/modules/openssl_privatekey.py index 2fdfdab10c9..d0ce6ae7b0f 100644 --- a/test/support/integration/plugins/modules/openssl_privatekey.py +++ b/test/support/integration/plugins/modules/openssl_privatekey.py @@ -908,7 +908,7 @@ def main(): if not PYOPENSSL_FOUND: module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), 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) elif backend == 'cryptography': if not CRYPTOGRAPHY_FOUND: diff --git a/test/support/integration/plugins/modules/python_requirements_info.py b/test/support/integration/plugins/modules/python_requirements_info.py index aa9e70ec867..e89f5db26e3 100644 --- a/test/support/integration/plugins/modules/python_requirements_info.py +++ b/test/support/integration/plugins/modules/python_requirements_info.py @@ -118,7 +118,7 @@ def main(): supports_check_mode=True, ) 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: module.fail_json( msg='Could not import "distutils" and "pkg_resources" libraries to introspect python environment.', diff --git a/test/support/integration/plugins/modules/xml.py b/test/support/integration/plugins/modules/xml.py index c71b3c1778e..281e9f5558c 100644 --- a/test/support/integration/plugins/modules/xml.py +++ b/test/support/integration/plugins/modules/xml.py @@ -885,7 +885,8 @@ def main(): # 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 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 if xml_string: diff --git a/test/units/module_utils/basic/test_deprecate_warn.py b/test/units/module_utils/basic/test_deprecate_warn.py index eb6f0d8e96f..2e686f966a4 100644 --- a/test/units/module_utils/basic/test_deprecate_warn.py +++ b/test/units/module_utils/basic/test_deprecate_warn.py @@ -22,23 +22,23 @@ def test_warn(am, capfd): @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) def test_deprecate(am, capfd): am.deprecate('deprecation1') - am.deprecate('deprecation2', '2.3') - am.deprecate('deprecation3', version='2.4') - am.deprecate('deprecation4', date='2020-03-10') + am.deprecate('deprecation2', 'ansible.builtin:2.3') + am.deprecate('deprecation3', version='ansible.builtin:2.4') + am.deprecate('deprecation4', date='ansible.builtin:2020-03-10') 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() output = json.loads(out) assert ('warnings' not in output or output['warnings'] == []) assert output['deprecations'] == [ {u'msg': u'deprecation1', u'version': None}, - {u'msg': u'deprecation2', u'version': '2.3'}, - {u'msg': u'deprecation3', u'version': '2.4'}, - {u'msg': u'deprecation4', u'date': '2020-03-10'}, + {u'msg': u'deprecation2', u'version': 'ansible.builtin:2.3'}, + {u'msg': u'deprecation3', u'version': 'ansible.builtin:2.4'}, + {u'msg': u'deprecation4', u'date': 'ansible.builtin:2020-03-10'}, {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'}, ]