diff --git a/lib/ansible/errors/__init__.py b/lib/ansible/errors/__init__.py index 8e33bef120b..f89625cbc1c 100644 --- a/lib/ansible/errors/__init__.py +++ b/lib/ansible/errors/__init__.py @@ -21,6 +21,7 @@ import re import traceback from collections.abc import Sequence +from warnings import warn as _emit_warning from ansible.errors.yaml_strings import ( YAML_COMMON_DICT_ERROR, @@ -35,6 +36,10 @@ from ansible.errors.yaml_strings import ( from ansible.module_utils.common.text.converters import to_native, to_text +_UNDERLYING_EXC_SENTINEL = Exception() +"""A sentinel object for use as a default for arguments in callables.""" + + class AnsibleError(Exception): ''' This is the base class for all errors raised from Ansible code, @@ -50,14 +55,42 @@ class AnsibleError(Exception): which should be returned by the DataLoader() class. ''' - def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None): + def __init__( + self, + message="", + obj=None, + show_content=True, + suppress_extended_error=False, + orig_exc=_UNDERLYING_EXC_SENTINEL, + ): super(AnsibleError, self).__init__(message) self._show_content = show_content self._suppress_extended_error = suppress_extended_error self._message = to_native(message) self.obj = obj - self.orig_exc = orig_exc + if orig_exc is not _UNDERLYING_EXC_SENTINEL: + _emit_warning( + 'Passing the underlying error through the `orig_exc` argument ' + 'is deprecated. Instead, use native Python 3 exception cause ' + 'chaining syntax: `raise ... from ...`', + DeprecationWarning, + stacklevel=2, + ) + self.__cause__ = orig_exc + + @property + def orig_exc(self): + """The underlying exception object. + + If the exception cause is undefined, it's set to :py:data:`None`. + """ + return self.__cause__ + + @orig_exc.setter + def orig_exc(self, underlying_exception): + """Set the underlying exception as cause of the current one.""" + self.__cause__ = underlying_exception @property def message(self): @@ -72,8 +105,8 @@ class AnsibleError(Exception): message.append( '\n\n%s' % to_native(extended_error) ) - elif self.orig_exc: - message.append('. %s' % to_native(self.orig_exc)) + elif self.__cause__: + message.append('. %s' % to_native(self.__cause__)) return ''.join(message) @@ -285,7 +318,16 @@ class AnsibleUndefinedVariable(AnsibleTemplateError): class AnsibleFileNotFound(AnsibleRuntimeError): ''' a file missing failure ''' - def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, paths=None, file_name=None): + def __init__( + self, + message="", + obj=None, + show_content=True, + suppress_extended_error=False, + orig_exc=_UNDERLYING_EXC_SENTINEL, + paths=None, + file_name=None, + ): self.file_name = file_name self.paths = paths @@ -315,7 +357,15 @@ class AnsibleFileNotFound(AnsibleRuntimeError): class AnsibleAction(AnsibleRuntimeError): ''' Base Exception for Action plugin flow control ''' - def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): + def __init__( + self, + message="", + obj=None, + show_content=True, + suppress_extended_error=False, + orig_exc=_UNDERLYING_EXC_SENTINEL, + result=None, + ): super(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content, suppress_extended_error=suppress_extended_error, orig_exc=orig_exc) @@ -328,7 +378,15 @@ class AnsibleAction(AnsibleRuntimeError): class AnsibleActionSkip(AnsibleAction): ''' an action runtime skip''' - def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): + def __init__( + self, + message="", + obj=None, + show_content=True, + suppress_extended_error=False, + orig_exc=_UNDERLYING_EXC_SENTINEL, + result=None, + ): super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content, suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) self.result.update({'skipped': True, 'msg': message}) @@ -336,7 +394,15 @@ class AnsibleActionSkip(AnsibleAction): class AnsibleActionFail(AnsibleAction): ''' an action runtime failure''' - def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): + def __init__( + self, + message="", + obj=None, + show_content=True, + suppress_extended_error=False, + orig_exc=_UNDERLYING_EXC_SENTINEL, + result=None, + ): super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content, suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) self.result.update({'failed': True, 'msg': message, 'exception': traceback.format_exc()}) diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index 5b3e5326b6e..fad65a05f28 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -479,7 +479,7 @@ class TaskExecutor: # Display the error from the conditional as well to prevent # losing information useful for debugging. display.v(to_text(e)) - raise self._loop_eval_error # pylint: disable=raising-bad-type + raise self._loop_eval_error from e # pylint: disable=raising-bad-type raise # Not skipping, if we had loop error raised earlier we need to raise it now to halt the execution of this task @@ -495,7 +495,7 @@ class TaskExecutor: raiseit = False elif isinstance(context_validation_error, AnsibleParserError): # parser error, might be cause by undef too - orig_exc = getattr(context_validation_error, 'orig_exc', None) + orig_exc = context_validation_error.__cause__ if isinstance(orig_exc, AnsibleUndefinedVariable): raiseit = False if raiseit: diff --git a/lib/ansible/galaxy/api.py b/lib/ansible/galaxy/api.py index 156dd4cf700..954cb1b6eac 100644 --- a/lib/ansible/galaxy/api.py +++ b/lib/ansible/galaxy/api.py @@ -104,7 +104,7 @@ def g_connect(versions): data = self._call_galaxy(n_url, method='GET', error_context_msg=error_context_msg, cache=True) except GalaxyError as new_err: if new_err.http_code == 404: - raise err + raise err from new_err raise if 'available_versions' not in data: @@ -407,20 +407,23 @@ class GalaxyAPI: display.vvvv("Calling Galaxy at %s" % url) resp = open_url(to_native(url), data=args, validate_certs=self.validate_certs, headers=headers, method=method, timeout=self._server_timeout, http_agent=user_agent(), follow_redirects='safe') - except HTTPError as e: - raise GalaxyError(e, error_context_msg) - except Exception as e: + except HTTPError as underlying_http_error: + raise GalaxyError( + underlying_http_error, + error_context_msg, + ) from underlying_http_error + except Exception as underlying_error: raise AnsibleError( - "Unknown error when attempting to call Galaxy at '%s': %s" % (url, to_native(e)), - orig_exc=e - ) + "Unknown error when attempting to call Galaxy at '%s': %s" + % (url, to_native(underlying_error)), + ) from underlying_error resp_data = to_text(resp.read(), errors='surrogate_or_strict') try: data = json.loads(resp_data) - except ValueError: + except ValueError as json_value_error: raise AnsibleError("Failed to parse Galaxy response from '%s' as JSON:\n%s" - % (resp.url, to_native(resp_data))) + % (resp.url, to_native(resp_data))) from json_value_error if cache and self._cache: path_cache = self._cache[cache_id][cache_key] @@ -470,10 +473,16 @@ class GalaxyAPI: try: resp = open_url(url, data=args, validate_certs=self.validate_certs, method="POST", http_agent=user_agent(), timeout=self._server_timeout) - except HTTPError as e: - raise GalaxyError(e, 'Attempting to authenticate to galaxy') - except Exception as e: - raise AnsibleError('Unable to authenticate to galaxy: %s' % to_native(e), orig_exc=e) + except HTTPError as underlying_http_error: + raise GalaxyError( + underlying_http_error, + 'Attempting to authenticate to galaxy', + ) from underlying_http_error + except Exception as underlying_error: + raise AnsibleError( + 'Unable to authenticate to galaxy: %s' + % to_native(underlying_error), + ) from underlying_error data = json.loads(to_text(resp.read(), errors='surrogate_or_strict')) return data @@ -525,8 +534,11 @@ class GalaxyAPI: role_name = parts[-1] if notify: display.display("- downloading role '%s', owned by %s" % (role_name, user_name)) - except Exception: - raise AnsibleError("Invalid role name (%s). Specify role as format: username.rolename" % role_name) + except Exception as underlying_error: + raise AnsibleError( + "Invalid role name (%s). Specify role as format: username.rolename" + % role_name, + ) from underlying_error url = _urljoin(self.api_server, self.available_api_versions['v1'], "roles", "?owner__username=%s&name=%s" % (user_name, role_name)) @@ -586,8 +598,11 @@ class GalaxyAPI: results += data['results'] done = (data.get('next_link', None) is None) return results - except Exception as error: - raise AnsibleError("Failed to download the %s list: %s" % (what, to_native(error))) + except Exception as underlying_error: + raise AnsibleError( + "Failed to download the %s list: %s" + % (what, to_native(underlying_error)), + ) from underlying_error @g_connect(['v1']) def search_roles(self, search, **kwargs): diff --git a/test/integration/targets/filter_core/tasks/main.yml b/test/integration/targets/filter_core/tasks/main.yml index 8b325a93279..d2a4268636a 100644 --- a/test/integration/targets/filter_core/tasks/main.yml +++ b/test/integration/targets/filter_core/tasks/main.yml @@ -485,7 +485,7 @@ that: - unsupported_hash_type.msg == msg vars: - msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512" + msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512. user must be unicode or bytes, not None" - name: Verify to_uuid throws on weird namespace set_fact: