From 5ecfb19cadc622a498208c4c7309c84810ec4aef Mon Sep 17 00:00:00 2001 From: Sam Doooran Date: Thu, 17 Dec 2020 17:15:00 -0500 Subject: [PATCH] arg_spec - move validator lookup method to a function (#72667) * arg_spec - move type checking lookup method to a function * Change get_wanted_type name and behavior Change the name to get_validator to bette describe what it is doing. Change the interface to always return a value. This lines up with the behavior of get_* functions always returning something or None and check_* functions raising an Exception if something went wrong during the check. * Add param to check_type_str() Not meant to be a long term fix, but gets tests passing. More work is needed to figure out how to solve this cleanly. * Remove private attribute mapping types to validator Since the function that needs it has moved to parameters.py, there is no need to have it as a attribute of AnsibleModule. Update tests that were referencing the private attribute. * Use private method for 'str' type To avoid having to put the string conversion warning behavior in the check_type_str() method, use the private _check_type_str() method for 'str' type. Import CHECK_ARGUMENT_TYPES_DISPATCHER for backwards compalitibility and store it as a private attribute. Revert changes to support plugins that are referencing serf._CHECK_ARGUMENT_TYPES_DISPATCHER. * Add changelog * Change function name to better reflect its... function * Change dict name to better reflect its contents CHECK_ARGUMENT_TYPES_DISPATCHER --> DEFAULT_TYPE_VALIDATORS * Fix changelog --- .../fragments/arg_spec-get_type_validator.yml | 2 + lib/ansible/module_utils/basic.py | 38 ++++--------- lib/ansible/module_utils/common/parameters.py | 57 ++++++++++++++++++- .../validate-modules/validate_modules/main.py | 7 ++- 4 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 changelogs/fragments/arg_spec-get_type_validator.yml diff --git a/changelogs/fragments/arg_spec-get_type_validator.yml b/changelogs/fragments/arg_spec-get_type_validator.yml new file mode 100644 index 00000000000..133d1d6dbb9 --- /dev/null +++ b/changelogs/fragments/arg_spec-get_type_validator.yml @@ -0,0 +1,2 @@ +minor_changes: + - create ``get_type_validator`` standalone function and move that functionality out of ``AnsibleModule`` (https://github.com/ansible/ansible/pull/72667) diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index f7cd7a1060a..6081574853b 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -157,9 +157,11 @@ from ansible.module_utils.common.sys_info import ( from ansible.module_utils.pycompat24 import get_exception, literal_eval from ansible.module_utils.common.parameters import ( get_unsupported_parameters, + get_type_validator, handle_aliases, list_deprecations, list_no_log_values, + DEFAULT_TYPE_VALIDATORS, PASS_VARS, PASS_BOOLS, ) @@ -739,20 +741,9 @@ class AnsibleModule(object): self._set_defaults(pre=True) - self._CHECK_ARGUMENT_TYPES_DISPATCHER = { - 'str': self._check_type_str, - 'list': self._check_type_list, - 'dict': self._check_type_dict, - 'bool': self._check_type_bool, - 'int': self._check_type_int, - 'float': self._check_type_float, - 'path': self._check_type_path, - 'raw': self._check_type_raw, - 'jsonarg': self._check_type_jsonarg, - 'json': self._check_type_jsonarg, - 'bytes': self._check_type_bytes, - 'bits': self._check_type_bits, - } + # This is for backwards compatibility only. + self._CHECK_ARGUMENT_TYPES_DISPATCHER = DEFAULT_TYPE_VALIDATORS + if not bypass_checks: self._check_required_arguments() self._check_argument_types() @@ -1853,20 +1844,13 @@ class AnsibleModule(object): self._options_context.pop() def _get_wanted_type(self, wanted, k): - if not callable(wanted): - if wanted is None: - # Mostly we want to default to str. - # For values set to None explicitly, return None instead as - # that allows a user to unset a parameter - wanted = 'str' - try: - type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted] - except KeyError: - self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k)) + # Use the private method for 'str' type to handle the string conversion warning. + if wanted == 'str': + type_checker, wanted = self._check_type_str, 'str' else: - # set the type_checker to the callable, and reset wanted to the callable's name (or type if it doesn't have one, ala MagicMock) - type_checker = wanted - wanted = getattr(wanted, '__name__', to_native(type(wanted))) + type_checker, wanted = get_type_validator(wanted) + if type_checker is None: + self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k)) return type_checker, wanted diff --git a/lib/ansible/module_utils/common/parameters.py b/lib/ansible/module_utils/common/parameters.py index 4e263d932f5..24a82acb076 100644 --- a/lib/ansible/module_utils/common/parameters.py +++ b/lib/ansible/module_utils/common/parameters.py @@ -8,7 +8,6 @@ __metaclass__ = type from ansible.module_utils._text import to_native from ansible.module_utils.common._collections_compat import Mapping from ansible.module_utils.common.collections import is_iterable -from ansible.module_utils.common.validation import check_type_dict from ansible.module_utils.six import ( binary_type, @@ -17,6 +16,20 @@ from ansible.module_utils.six import ( text_type, ) +from ansible.module_utils.common.validation import ( + check_type_bits, + check_type_bool, + check_type_bytes, + check_type_dict, + check_type_float, + check_type_int, + check_type_jsonarg, + check_type_list, + check_type_path, + check_type_raw, + check_type_str, +) + # Python2 & 3 way to get NoneType NoneType = type(None) @@ -42,6 +55,21 @@ PASS_VARS = { PASS_BOOLS = ('check_mode', 'debug', 'diff', 'keep_remote_files', 'no_log') +DEFAULT_TYPE_VALIDATORS = { + 'str': check_type_str, + 'list': check_type_list, + 'dict': check_type_dict, + 'bool': check_type_bool, + 'int': check_type_int, + 'float': check_type_float, + 'path': check_type_path, + 'raw': check_type_raw, + 'jsonarg': check_type_jsonarg, + 'json': check_type_jsonarg, + 'bytes': check_type_bytes, + 'bits': check_type_bits, +} + def _return_datastructure_name(obj): """ Return native stringified values from datastructures. @@ -220,3 +248,30 @@ def get_unsupported_parameters(argument_spec, module_parameters, legal_inputs=No unsupported_parameters.add(k) return unsupported_parameters + + +def get_type_validator(wanted): + """Returns the callable used to validate a wanted type and the type name. + + :arg wanted: String or callable. If a string, get the corresponding + validation function from DEFAULT_TYPE_VALIDATORS. If callable, + get the name of the custom callable and return that for the type_checker. + + :returns: Tuple of callable function or None, and a string that is the name + of the wanted type. + """ + + # Use one our our builtin validators. + if not callable(wanted): + if wanted is None: + # Default type for parameters + wanted = 'str' + + type_checker = DEFAULT_TYPE_VALIDATORS.get(wanted) + + # Use the custom callable for validation. + else: + type_checker = wanted + wanted = getattr(wanted, '__name__', to_native(type(wanted))) + + return type_checker, wanted 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 aa336b28f5a..2e0e5d0799e 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,6 +41,7 @@ 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.common.parameters import DEFAULT_TYPE_VALIDATORS from ansible.plugins.loader import fragment_loader from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder from ansible.utils.plugin_docs import REJECTLIST, add_collection_to_versions_and_dates, add_fragments, get_docstring @@ -1362,7 +1363,7 @@ class ModuleValidator(Validator): if callable(_type): _type_checker = _type else: - _type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER.get(_type) + _type_checker = DEFAULT_TYPE_VALIDATORS.get(_type) try: with CaptureStd(): dummy = _type_checker(value) @@ -1691,7 +1692,7 @@ class ModuleValidator(Validator): if callable(_type): _type_checker = _type else: - _type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER.get(_type) + _type_checker = DEFAULT_TYPE_VALIDATORS.get(_type) _elements = data.get('elements') if (_type == 'list') and not _elements: @@ -1706,7 +1707,7 @@ class ModuleValidator(Validator): ) if _elements: if not callable(_elements): - module._CHECK_ARGUMENT_TYPES_DISPATCHER.get(_elements) + DEFAULT_TYPE_VALIDATORS.get(_elements) if _type != 'list': msg = "Argument '%s' in argument_spec" % arg if context: