"""Utilities for providing backward compatibility.""" import inspect from fractions import Fraction from warnings import warn import six from tenacity import _utils def warn_about_non_retry_state_deprecation(cbname, func, stacklevel): msg = ( '"%s" function must accept single "retry_state" parameter,' ' please update %s' % (cbname, _utils.get_callback_name(func))) warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) def warn_about_dunder_non_retry_state_deprecation(fn, stacklevel): msg = ( '"%s" method must be called with' ' single "retry_state" parameter' % (_utils.get_callback_name(fn))) warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) def func_takes_retry_state(func): if not six.callable(func): raise Exception(func) return False if not inspect.isfunction(func) and not inspect.ismethod(func): # func is a callable object rather than a function/method func = func.__call__ func_spec = _utils.getargspec(func) return 'retry_state' in func_spec.args _unset = object() def _make_unset_exception(func_name, **kwargs): missing = [] for k, v in six.iteritems(kwargs): if v is _unset: missing.append(k) missing_str = ', '.join(repr(s) for s in missing) return TypeError(func_name + ' func missing parameters: ' + missing_str) def _set_delay_since_start(retry_state, delay): # Ensure outcome_timestamp - start_time is *exactly* equal to the delay to # avoid complexity in test code. retry_state.start_time = Fraction(retry_state.start_time) retry_state.outcome_timestamp = (retry_state.start_time + Fraction(delay)) assert retry_state.seconds_since_start == delay def make_retry_state(previous_attempt_number, delay_since_first_attempt, last_result=None): """Construct RetryCallState for given attempt number & delay. Only used in testing and thus is extra careful about timestamp arithmetics. """ required_parameter_unset = (previous_attempt_number is _unset or delay_since_first_attempt is _unset) if required_parameter_unset: raise _make_unset_exception( 'wait/stop', previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt) from tenacity import RetryCallState retry_state = RetryCallState(None, None, (), {}) retry_state.attempt_number = previous_attempt_number if last_result is not None: retry_state.outcome = last_result else: retry_state.set_result(None) _set_delay_since_start(retry_state, delay_since_first_attempt) return retry_state def func_takes_last_result(waiter): """Check if function has a "last_result" parameter. Needed to provide backward compatibility for wait functions that didn't take "last_result" in the beginning. """ if not six.callable(waiter): return False if not inspect.isfunction(waiter) and not inspect.ismethod(waiter): # waiter is a class, check dunder-call rather than dunder-init. waiter = waiter.__call__ waiter_spec = _utils.getargspec(waiter) return 'last_result' in waiter_spec.args def stop_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "stop" signature.""" @_utils.wraps(fn) def new_fn(self, previous_attempt_number=_unset, delay_since_first_attempt=_unset, retry_state=None): if retry_state is None: from tenacity import RetryCallState retry_state_passed_as_non_kwarg = ( previous_attempt_number is not _unset and isinstance(previous_attempt_number, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = previous_attempt_number else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = make_retry_state( previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt) return fn(self, retry_state=retry_state) return new_fn def stop_func_accept_retry_state(stop_func): """Wrap "stop" function to accept "retry_state" parameter.""" if not six.callable(stop_func): return stop_func if func_takes_retry_state(stop_func): return stop_func @_utils.wraps(stop_func) def wrapped_stop_func(retry_state): warn_about_non_retry_state_deprecation( 'stop', stop_func, stacklevel=4) return stop_func( retry_state.attempt_number, retry_state.seconds_since_start, ) return wrapped_stop_func def wait_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "wait" signature.""" @_utils.wraps(fn) def new_fn(self, previous_attempt_number=_unset, delay_since_first_attempt=_unset, last_result=None, retry_state=None): if retry_state is None: from tenacity import RetryCallState retry_state_passed_as_non_kwarg = ( previous_attempt_number is not _unset and isinstance(previous_attempt_number, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = previous_attempt_number else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = make_retry_state( previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt, last_result=last_result) return fn(self, retry_state=retry_state) return new_fn def wait_func_accept_retry_state(wait_func): """Wrap wait function to accept "retry_state" parameter.""" if not six.callable(wait_func): return wait_func if func_takes_retry_state(wait_func): return wait_func if func_takes_last_result(wait_func): @_utils.wraps(wait_func) def wrapped_wait_func(retry_state): warn_about_non_retry_state_deprecation( 'wait', wait_func, stacklevel=4) return wait_func( retry_state.attempt_number, retry_state.seconds_since_start, last_result=retry_state.outcome, ) else: @_utils.wraps(wait_func) def wrapped_wait_func(retry_state): warn_about_non_retry_state_deprecation( 'wait', wait_func, stacklevel=4) return wait_func( retry_state.attempt_number, retry_state.seconds_since_start, ) return wrapped_wait_func def retry_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "retry" signature.""" @_utils.wraps(fn) def new_fn(self, attempt=_unset, retry_state=None): if retry_state is None: from tenacity import RetryCallState if attempt is _unset: raise _make_unset_exception('retry', attempt=attempt) retry_state_passed_as_non_kwarg = ( attempt is not _unset and isinstance(attempt, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = attempt else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = RetryCallState(None, None, (), {}) retry_state.outcome = attempt return fn(self, retry_state=retry_state) return new_fn def retry_func_accept_retry_state(retry_func): """Wrap "retry" function to accept "retry_state" parameter.""" if not six.callable(retry_func): return retry_func if func_takes_retry_state(retry_func): return retry_func @_utils.wraps(retry_func) def wrapped_retry_func(retry_state): warn_about_non_retry_state_deprecation( 'retry', retry_func, stacklevel=4) return retry_func(retry_state.outcome) return wrapped_retry_func def before_func_accept_retry_state(fn): """Wrap "before" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_before_func(retry_state): # func, trial_number, trial_time_taken warn_about_non_retry_state_deprecation('before', fn, stacklevel=4) return fn( retry_state.fn, retry_state.attempt_number, ) return wrapped_before_func def after_func_accept_retry_state(fn): """Wrap "after" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_after_sleep_func(retry_state): # func, trial_number, trial_time_taken warn_about_non_retry_state_deprecation('after', fn, stacklevel=4) return fn( retry_state.fn, retry_state.attempt_number, retry_state.seconds_since_start) return wrapped_after_sleep_func def before_sleep_func_accept_retry_state(fn): """Wrap "before_sleep" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_before_sleep_func(retry_state): # retry_object, sleep, last_result warn_about_non_retry_state_deprecation( 'before_sleep', fn, stacklevel=4) return fn( retry_state.retry_object, sleep=getattr(retry_state.next_action, 'sleep'), last_result=retry_state.outcome) return wrapped_before_sleep_func def retry_error_callback_accept_retry_state(fn): if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_retry_error_callback(retry_state): warn_about_non_retry_state_deprecation( 'retry_error_callback', fn, stacklevel=4) return fn(retry_state.outcome) return wrapped_retry_error_callback def get_exc_info_from_future(future): """ Get an exc_info value from a Future. Given a a Future instance, retrieve an exc_info value suitable for passing in as the exc_info parameter to logging.Logger.log() and related methods. On Python 2, this will be a (type, value, traceback) triple. On Python 3, this will be an exception instance (with embedded traceback). If there was no exception, None is returned on both versions of Python. """ if six.PY3: return future.exception() else: ex, tb = future.exception_info() if ex is None: return None return type(ex), ex, tb