import collections import sys import time from webdriver import error DEFAULT_TIMEOUT = 5 DEFAULT_INTERVAL = 0.1 class Poll(object): """ An explicit conditional utility primitive for polling until a condition evaluates to something truthy. A `Poll` instance defines the maximum amount of time to wait for a condition, as well as the frequency with which to check the condition. Furthermore, the user may configure the wait to ignore specific types of exceptions whilst waiting, such as `error.NoSuchElementException` when searching for an element on the page. """ def __init__(self, session, timeout=DEFAULT_TIMEOUT, interval=DEFAULT_INTERVAL, raises=error.TimeoutException, message=None, ignored_exceptions=None, clock=time): """ Configure the poller to have a custom timeout, interval, and list of ignored exceptions. Optionally a different time implementation than the one provided by the standard library (`time`) can also be provided. Sample usage:: # Wait 30 seconds for window to open, # checking for its presence once every 5 seconds. from support.sync import Poll wait = Poll(session, timeout=30, interval=5, ignored_exceptions=error.NoSuchWindowException) window = wait.until(lambda s: s.switch_to_window(42)) :param session: The input value to be provided to conditions, usually a `webdriver.Session` instance. :param timeout: How long to wait for the evaluated condition to become true. :param interval: How often the condition should be evaluated. In reality the interval may be greater as the cost of evaluating the condition function. If that is not the case the interval for the next condition function call is shortend to keep the original interval sequence as best as possible. :param raises: Optional exception to raise when poll elapses. If not used, an `error.TimeoutException` is raised. If it is `None`, no exception is raised on the poll elapsing. :param message: An optional message to include in `raises`'s message if the `until` condition times out. :param ignored_exceptions: Ignore specific types of exceptions whilst waiting for the condition. Any exceptions not in this list will be allowed to propagate, terminating the wait. :param clock: Allows overriding the use of the runtime's default time library. See `sync.SystemClock` for implementation details. """ self.session = session self.timeout = timeout self.interval = interval self.exc_cls = raises self.exc_msg = message self.clock = clock exceptions = [] if ignored_exceptions is not None: if isinstance(ignored_exceptions, collections.Iterable): exceptions.extend(iter(ignored_exceptions)) else: exceptions.append(ignored_exceptions) self.exceptions = tuple(set(exceptions)) def until(self, condition): """ This will repeatedly evaluate `condition` in anticipation for a truthy return value, or the timeout to expire. A condition that returns `None` or does not evaluate to true will fully elapse its timeout before raising, unless the `raises` keyword argument is `None`, in which case the condition's return value is propagated unconditionally. If an exception is raised in `condition` and it's not ignored, this function will raise immediately. If the exception is ignored it will be swallowed and polling will resume until either the condition meets the return requirements or the timeout duration is reached. :param condition: A callable function whose return value will be returned by this function. """ rv = None tb = None start = self.clock.time() end = start + self.timeout while not self.clock.time() >= end: try: next = self.clock.time() + self.interval rv = condition(self.session) except (KeyboardInterrupt, SystemExit): raise except self.exceptions: _, _, tb = sys.exc_info() # re-adjust the interval depending on how long # the callback took to evaluate the condition interval_new = max(next - self.clock.time(), 0) if not rv: self.clock.sleep(interval_new) continue if rv is not None: return rv self.clock.sleep(interval_new) if self.exc_cls is not None: elapsed = round((self.clock.time() - start), 1) message = "Timed out after {} seconds".format(elapsed) if self.exc_msg is not None: message = "{} with message: {}".format(message, self.exc_msg) raise self.exc_cls(message=message).with_traceback(tb) else: return rv