Coverage for mlair/helpers/time_tracking.py: 97%
44 statements
« prev ^ index » next coverage.py v6.4.2, created at 2023-06-01 13:03 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2023-06-01 13:03 +0000
1"""Track time either as decorator or explicit."""
2import datetime as dt
3import logging
4import math
5import time
6import types
7from functools import wraps
8from typing import Optional
11class TimeTrackingWrapper:
12 r"""
13 Wrapper implementation of TimeTracking class.
15 Use this implementation easily as decorator for functions, classes and class methods. Implement a custom function
16 and decorate it for automatic time measure.
18 .. code-block:: python
20 @TimeTrackingWrapper
21 def sleeper():
22 print("start")
23 time.sleep(1)
24 print("end")
26 >>> sleeper()
27 start
28 end
29 INFO: foo finished after 00:00:01 (hh:mm:ss)
31 """
33 def __init__(self, func):
34 """Construct."""
35 wraps(func)(self)
37 def __call__(self, *args, **kwargs):
38 """Start time tracking."""
39 with TimeTracking(name=self.__wrapped__.__name__):
40 return self.__wrapped__(*args, **kwargs)
42 def __get__(self, instance, cls):
43 """Create bound method object and supply self argument to the decorated method."""
44 if instance is None: 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true
45 return self
46 else:
47 return types.MethodType(self, instance)
50class TimeTracking(object):
51 """
52 Track time to measure execution time.
54 Time tracking automatically starts on initialisation and ends by calling stop method. Duration can always be shown
55 by printing the time tracking object or calling get_current_duration. It is possible to start and stop time tracking
56 by hand like
58 .. code-block:: python
60 time = TimeTracking(start=True) # start=True is default and not required to set
61 do_something()
62 time.stop(get_duration=True)
64 A more comfortable way is to use TimeTracking in a with statement like:
66 .. code-block:: python
68 with TimeTracking():
69 do_something()
71 The only disadvantage of the latter implementation is, that the duration is logged but not returned.
72 """
74 def __init__(self, start=True, name="undefined job", logging_level=logging.INFO, log_on_enter=False):
75 """Construct time tracking and start if enabled."""
76 self.start = None
77 self.end = None
78 self._name = name
79 self._logging = {logging.INFO: logging.info, logging.DEBUG: logging.debug}.get(logging_level, logging.info)
80 self._log_on_enter = log_on_enter
81 if start:
82 self._start()
84 def _start(self) -> None:
85 """Start time tracking."""
86 self.start = time.time()
87 self.end = None
89 def _end(self) -> None:
90 """Stop time tracking."""
91 self.end = time.time()
93 def _duration(self) -> float:
94 """Get duration in seconds."""
95 if self.end:
96 return self.end - self.start
97 else:
98 return time.time() - self.start
100 def __repr__(self) -> str:
101 """Display current passed time."""
102 return f"{dt.timedelta(seconds=math.ceil(self._duration()))} (hh:mm:ss)"
104 def run(self) -> None:
105 """Start time tracking."""
106 self._start()
108 def stop(self, get_duration=False) -> Optional[float]:
109 """
110 Stop time tracking.
112 Will raise an error if time tracking was already stopped.
113 :param get_duration: return passed time if enabled.
115 :return: duration if enabled or None
116 """
117 if self.end is None:
118 self._end()
119 else:
120 msg = f"Time was already stopped {time.time() - self.end}s ago."
121 raise AssertionError(msg)
122 if get_duration:
123 return self.duration()
125 def duration(self) -> float:
126 """Return duration in seconds."""
127 return self._duration()
129 def __enter__(self):
130 """Context manager."""
131 self._logging(f"start {self._name}") if self._log_on_enter is True else None
132 return self
134 def __exit__(self, exc_type, exc_val, exc_tb) -> None:
135 """Stop time tracking on exit and log info about passed time."""
136 self.stop()
137 self._logging(f"{self._name} finished after {self}")