#!/usr/bin/env python3
__author__ = "Christian Heider Lindbjerg"
__doc__ = r"""
Created on 14/11/2019
"""
__all__ = ["timeit", "StopWatch"]
import contextlib
import functools
import logging
import time
import typing
from functools import wraps
from typing import Any, MutableMapping, Sequence
_logger = logging.getLogger(__name__)
[docs]
def timeit(f: typing.Callable) -> typing.Callable:
"""
:param f:
:type f:
:return:
:rtype:"""
@wraps(f)
def wrapper(*args, **kwds) -> typing.Tuple[float, Any]:
"""
:param args:
:type args:
:param kwds:
:type kwds:
:return:
:rtype:"""
start_time = time.time()
result = f(*args, **kwds)
elapsed_time = time.time() - start_time
_logger.info(f"{f} took {elapsed_time:.3f} seconds to compute")
return elapsed_time, result
return wrapper
[docs]
class StopWatch(contextlib.AbstractContextManager):
r"""**Measure execution time of function.**
Can be used as context manager or function decorator, perform checkpoints
or display absolute time from measurements beginning.
**Used as context manager**::
with Timer() as timer:
... # your operations
logger.info(timer) # __str__ calls timer.time() internally
timer.checkpoint() # register checkpoint
... # more operations
logger.info(timer.checkpoint()) # time since last timer.checkpoint() call
... # even more operations
logger.info(timer) # time taken for the block, will not be updated outside of it
When execution leaves the block, timer will be blocked. Last checkpoint and time taken
to execute whole block will be returned by `checkpoint()` and `time()` methods respectively.
**Used as function decorator**::
@Timer()
def foo():
return 42
value, time = foo()
Parameters
----------
function : Callable, optional
No argument function used to measure time. Default: time.perf_counter
"""
[docs]
def __init__(
self,
function: typing.Callable = time.perf_counter,
auto_start_on_construction: bool = False,
auto_start_on_enter: bool = True,
auto_stop_on_exit: bool = True,
):
self._stopped: bool = False
self._started = False
self._callable = function
self._auto_start_on_construction = auto_start_on_construction
self._auto_start_on_enter = auto_start_on_enter
self._auto_stop_on_exit = auto_stop_on_exit
self.start_time = 0
self.new_time = 0
self.previous_time = 0
if self._auto_start_on_construction:
self.start_timer()
self.override_arithmetics()
[docs]
def override_arithmetics(self):
"""description"""
def make_func(name):
"""description"""
return lambda self, *args: getattr(self.since_start, name)(*args)
arithmetics = (
"add",
"sub",
"mul",
"div",
"truediv",
"floordiv",
"mod",
"divmod",
"pow",
)
methods = [
"__invert__",
"__neg__",
"__pos__",
"abs",
"__round__",
"__floor__",
"__ceil__",
"__int__",
"__float__",
]
methods.extend([f"__{n}__" for n in arithmetics])
methods.extend([f"__r{n}__" for n in arithmetics])
methods.extend([f"__i{n}__" for n in arithmetics])
for name in methods:
setattr(StopWatch, name, make_func(name))
[docs]
def start_timer(self):
"""description"""
self._started = True
self.start_time = self._callable()
self.new_time = self.start_time
self.previous_time = self.start_time
[docs]
def stop_timer(self):
"""description"""
self.new_time = self._callable()
self._stopped: bool = True
@property
def since_start(self):
"""**Time taken since the start of timer (measurements beginning).**
Returns
-------
time-like
Whatever `self.function() - self.function()` returns,
usually fraction of seconds"""
if not self._stopped and self._started:
return self._callable() - self.start_time
return self.new_time - self.start_time
[docs]
def tick(self):
"""**Time taken since last tick call.**
If wasn't called before, it is the same as as Timer creation time (first call returns
the same thing as `time()`)
Returns
-------
time-like
Whatever `self.function() - self.function()` returns,
usually fraction of seconds"""
if not self._stopped:
if self._started:
self.previous_time = self.new_time
self.new_time = self._callable()
else:
self.start_timer()
return self.new_time - self.previous_time
def __call__(self, function):
"""
decorator functionality
:param function:
:type function:
:return:
:rtype:"""
@functools.wraps(function)
def decorated(*args: Sequence[Any], **kwargs: MutableMapping[str, Any]):
"""description"""
self.start_timer()
values = function(*args, **kwargs)
self.stop_timer()
return values, self.since_start
return decorated
def __enter__(self):
if self._auto_start_on_enter:
self.start_timer()
return self
def __exit__(self, *_, **__) -> bool:
if self._auto_stop_on_exit:
self.stop_timer()
return False
def __str__(self) -> str:
return str(self.__repr__())
def __repr__(self) -> int:
return self.since_start
if __name__ == "__main__":
a = StopWatch()
_logger.info(f"Timer str rep: {a}")
_logger.info(a.tick())
_logger.info(a.tick())
_logger.info(a // 2)
_logger.info()
with StopWatch(auto_start_on_enter=False) as timer1:
_logger.info(timer1) # __str__ calls timer.time() internally
timer1.tick() # register checkpoint
_logger.info(timer1.tick()) # time since last timer.checkpoint() call
_logger.info()
with StopWatch() as timer4:
_logger.info(timer4) # __str__ calls timer.time() internally
timer4.tick() # register checkpoint
_logger.info(timer4.tick()) # time since last timer.checkpoint() call
_logger.info()
_logger.info(timer4) # time since start
_logger.info(timer4.tick()) # time taken for the block, will not be updated outside of it
_logger.info(timer4.tick()) # time taken for the block, will not be updated outside of it
_logger.info(timer4) # ime since start, will not be updated outside of it
_logger.info()
with StopWatch(auto_start_on_construction=True, auto_start_on_enter=False) as timer2:
_logger.info(timer2) # __str__ calls timer.time() internally
_logger.info(timer2.tick()) # time since last timer.checkpoint() call
_logger.info(timer2)
@StopWatch()
def foo() -> int:
"""
:rtype: None
"""
return 42
value, time = foo()
_logger.info(f"foo time: {time}, value: {value}")