Source code for smarttimers.timer

"""Utilities to measure time from system and custom clocks/counters.

Classes:
    * :class:`TimerDict`
    * :class:`Timer`
"""


__all__ = ['TimerDict', 'Timer']


import os
import time
import types
from .exceptions import TimerCompatibilityError


[docs]class TimerDict(dict): """Map between label identifier and callable object. Args: tdict (dict, optional): Mapping between strings and timing functions. Raises: KeyError: If key is not a string or does not exists. ValueError: If value is not a callable object. """ def __init__(self, tdict=None): if tdict: self.update(tdict) def __setitem__(self, key, value): if not isinstance(key, str): raise KeyError("key '{}' is not a {}".format(key, str)) if not key: raise KeyError("key is empty {}".format(str)) if not callable(value): raise ValueError("value '{}' is not callable".format(value)) super().__setitem__(key, value) def update(self, tdict): """Extends map with given dictionary. Raises: TypeError: If *tdict* is not derived from dict. """ if not isinstance(tdict, dict): raise TypeError("dict '{}' is not a {}".format(tdict, dict)) # Add key-values manually to invoke __setitem__ error checks for k, v in tdict.items(): self[k] = v
class MetaTimerProperty(type): """Metaclass for :class:`Timer` class variables. Requires :class:`TimerDict`. Raises: TypeError: If set property uses an invalid type. """ @property def DEFAULT_CLOCK_NAME(cls): return cls._DEFAULT_CLOCK_NAME @DEFAULT_CLOCK_NAME.setter def DEFAULT_CLOCK_NAME(cls, value): if not isinstance(value, str): raise TypeError("'DEFAULT_CLOCK_NAME' is not a {}".format(str)) cls._DEFAULT_CLOCK_NAME = value @property def CLOCKS(cls): return cls._CLOCKS @CLOCKS.setter def CLOCKS(cls, value): if not isinstance(value, dict): raise TypeError("'CLOCKS' is not a {}".format(dict)) cls._CLOCKS = value if isinstance(value, TimerDict) \ else TimerDict(value)
[docs]class Timer(metaclass=MetaTimerProperty): """Read current time from a clock/counter. Args: label (str, optional): Label identifier. Default is empty string. kwargs (dict, optional): Map of options. Valid options are :attr:`seconds`, :attr:`clock_name`, and :attr:`timer`. seconds (float, optional): Time measured in fractional seconds. Default is 0.0. clock_name (str, optional): Clock name used to select a time measurement function. Default is :attr:`DEFAULT_CLOCK_NAME`. timer (Timer, optional): Reference instance to use as initialization. A :class:`Timer` allows recording the current time measured by a registered timing function. Time is recorded in fractional seconds and fractional minutes. :class:`Timer` supports addition, difference, and logical operators. :class:`Timer` uses a simple and extensible API which allows registering new timing functions. A timing function is compliant if it returns a time measured in fractional seconds. The function can contain arbitrary positional and/or keyword arguments or no arguments. .. literalinclude:: ../examples/example_Timer.py :language: python :linenos: :name: Timer_API :caption: Timer API examples. .. _`time.perf_counter()`: https://docs.python.org/3/library/time.html#time.perf_counter .. _`time.process_time()`: https://docs.python.org/3/library/time.html#time.process_time .. _`time.clock()`: https://docs.python.org/3/library/time.html#time.clock .. _`time.monotonic()`: https://docs.python.org/3/library/time.html#time.monotonic .. _`time.time()`: https://docs.python.org/3/library/time.html#time.time Available time measurement functions in :attr:`CLOCKS`: * 'perf_counter' -> `time.perf_counter()`_ * 'process_time' -> `time.process_time()`_ * 'clock' -> `time.clock()`_ (deprecated) * 'monotonic' -> `time.monotonic()`_ * 'time' -> `time.time()`_ (deprecated) .. code-block:: python :emphasize-lines: 9,10 :name: register_time_function :caption: Registering a new time measurement function. def custom_time_function(*args, **kwargs): # Measure time time_in_some_unit = ... # Convert time to fractional seconds time_seconds = time_in_some_unit ... return time_seconds # Register custom_time_function() as 'custom_time' Timer.register_clock('custom_time', custom_time_function) # or Timer.CLOCKS['custom_time'] = custom_time_function .. _`time`: https://docs.python.org/3/library/time.html Note: * New timing functions need to have a compliant interface. If a user wants to register a non-compliant timing function, a compliant wrapper function can be used. The available timing functions are built-ins from the standard `time`_ library. * Only Timers with compatible clocks support arithmetic and logical operators. Otherwise a :class:`TimerCompatibilityError` exception occurs. Attributes: DEFAULT_CLOCK_NAME (str): Default clock name, used when :attr:`clock_name` is empty string. Raises: TypeError: If not a string. CLOCKS (:class:`TimerDict`, str -> callable): Map between clock name and time measurement functions. Raises: TypeError: If not assigned with dictionary. KeyError: If key is not a string. ValueError: If assigned item is not callable. label (str): Label identifier. Raises: TypeError: If not a string. seconds (float, read-only): Time measured in fractional seconds. Set internally either during initialization or when recording time. Raises: TypeError: If not numeric. ValueError: If negative number. minutes (float, read-only): Time measured in minutes. clock_name (str): Clock name used to select a time measurement function. Indexes the :attr:`CLOCKS` map to select a time function. If set to the empty string then :attr:`DEFAULT_CLOCK_NAME` is used. An instance is reset when set to a new and incompatible clock name. Raises: TypeError: If not a string. .. _`time.get_clock_info`: https://docs.python.org/3/library/time.html#time.get_clock_info """ _DEFAULT_CLOCK_NAME = "perf_counter" _CLOCKS = TimerDict({ "perf_counter": time.perf_counter, "process_time": time.process_time, "clock": time.clock, "monotonic": time.monotonic, "time": time.time }) def __init__(self, label="", **kwargs): self.label = label # Check if another Timer was provided for initialization. # Timer has precedence over explicit time. timer = kwargs.get('timer', None) if timer: self._set_time(timer.seconds) self.clock_name = timer.clock_name else: self._set_time(kwargs.get('seconds', 0.)) self.clock_name = kwargs.get('clock_name', type(self).DEFAULT_CLOCK_NAME)
[docs] def __str__(self): """String representation. Returns: str: Delimited string (:attr:`seconds`, :attr:`minutes`, :attr:`label`) """ return "{:>12} {:12.6f} {:12.6f}" \ .format(self.label, self.seconds, self.minutes)
def __add__(self, timer): if not self.is_compatible(timer): raise TimerCompatibilityError return Timer(label='+'.join(filter(None, [self.label, timer.label])), seconds=self.seconds + timer.seconds, clock_name=self.clock_name) def __sub__(self, timer): if not self.is_compatible(timer): raise TimerCompatibilityError return Timer(label='-'.join(filter(None, [self.label, timer.label])), seconds=abs(self.seconds - timer.seconds), clock_name=self.clock_name) def __eq__(self, timer): # Return false if not compatible, do not raise exception. # This method is used during index searches in Timer iterations. if not self.is_compatible(timer): return False return self.seconds == timer.seconds def __lt__(self, timer): if not self.is_compatible(timer): raise TimerCompatibilityError return self.seconds < timer.seconds def __le__(self, timer): return self < timer or self == timer def __gt__(self, timer): return not (self <= timer) def __ge__(self, timer): return not (self < timer) def _set_time(self, seconds): # Do checks here because attribute is read-only if not isinstance(seconds, (int, float)): raise TypeError("value '{}' is not a {}".format(seconds, float)) if seconds < 0.: raise ValueError("value '{}' is negative".format(seconds)) self._seconds = float(seconds) self._minutes = seconds / 60. @property def label(self): return self._label @label.setter def label(self, label): if not isinstance(label, str): raise TypeError("label '{}' is not a {}".format(label, str)) self._label = label @property def seconds(self): return self._seconds @property def minutes(self): return self._minutes @property def clock(self): return self._clock @property def clock_name(self): return self._clock_name @clock_name.setter def clock_name(self, clock_name): if not isinstance(clock_name, str): raise TypeError("clock_name '{}' is not a {}".format(clock_name, str)) # Clear time if new clock is incompatible with previous one. Skip # check if setting for the first time (e.g., __init__) to prevent # clearing time values that had been set previously. # # Note: Creates an infinite loop if not careful. is_compatible() is # called which in turn creates a Timer object which calls this property # during initialization. Attribute _clock_name is created here, so use # it as a sentinel to detect when not called from __init__. if hasattr(self, '_clock_name') and not self.is_compatible(clock_name): self.clear() self._clock_name = clock_name if clock_name \ else type(self).DEFAULT_CLOCK_NAME self._clock = type(self).CLOCKS[self._clock_name]
[docs] def time(self, *args, **kwargs): """Record time using timing function configured via :attr:`clock_name`. Args: args (tuple, optional): Positional arguments for time function. kwargs (dict, optional): Keyword arguments for time function. Returns: float: Time measured in fractional seconds. """ self._set_time(self._clock(*args, **kwargs)) return self.seconds
[docs] def clear(self): """Set time values to zero.""" self._set_time(0.)
[docs] def reset(self): """Clear, empty :attr:`label`, and reset clock to default value.""" self.label = "" self.clock_name = type(self).DEFAULT_CLOCK_NAME self.clear()
[docs] def get_info(self): """Return clock information. .. _`types.SimpleNamespace`: https://docs.python.org/3/library/types.html?highlight=types#types.SimpleNamespace Returns: `types.SimpleNamespace`_: Namespace with clock info. """ # For :attr:`clock_name` that can be queried with # `time.get_clock_info`_, forward the output namespace object. # Otherwise create and populate a namespace with the timing function. try: return time.get_clock_info(self.clock_name) except (TypeError, ValueError) as ex: clock_info = { "adjustable": None, "implementation": type(self).CLOCKS[self.clock_name].__name__, "monotonic": None, "resolution": None} return types.SimpleNamespace(**clock_info)
[docs] def print_info(self): """Pretty print clock information.""" print("{name}:{lsep}" " function : {func}{lsep}" " adjustable : {info.adjustable}{lsep}" " implementation: {info.implementation}{lsep}" " monotonic : {info.monotonic}{lsep}" " resolution : {info.resolution}" .format(name=self.clock_name, func=type(self).CLOCKS[self.clock_name], info=self.get_info(), lsep=os.linesep))
[docs] def is_compatible(self, timer): """Return truth of compatibility between a :class:`Timer` pair. For a :attr:`clock_name` that can be queried with `time.get_clock_info`_, compatibility requires that all attributes are identical. Otherwise the timing functions have to be the same. Args: timer (Timer): Second instance. Returns: bool: True if compatible, else False. """ if not isinstance(timer, Timer): return False # Exception is triggered if time.get_clock_info() does not support the # provided clock_name. try: return time.get_clock_info(self.clock_name) == \ time.get_clock_info(timer.clock_name) except Exception as ex: return type(self).CLOCKS[self.clock_name] is \ type(self).CLOCKS[timer.clock_name]
[docs] def sleep(self, seconds): """Sleep for given seconds.""" time.sleep(seconds)
[docs] @classmethod def register_clock(cls, clock_name, clock_func): """Registers a time function to :attr:`CLOCKS` map. Args: clock_name (str): Clock name. clock_func (callable): Reference to a time measurement function. """ cls.CLOCKS[clock_name] = clock_func
[docs] @classmethod def unregister_clock(cls, clock_name): """Remove a registered clock from :attr:`CLOCKS` map. Args: clock_name (str): Clock name. """ # Query map using __getitem__ property to check for valid key because # 'del' does not triggers __getitem__. # Use dummy variable to prevent warning from linting. _ = cls.CLOCKS[clock_name] del _ del cls.CLOCKS[clock_name]
[docs] @classmethod def print_clocks(cls): """Pretty print information of registered clocks.""" print("Default clock: {}".format(cls.DEFAULT_CLOCK_NAME)) for n in cls.CLOCKS.keys(): print() Timer(clock_name=n).print_info()