Coverage for src / mafw / timer.py: 100%
50 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-09 09:08 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-09 09:08 +0000
1# Copyright 2025 European Union
2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
3# SPDX-License-Identifier: EUPL-1.2
4"""
5Module implements a simple timer to measure the execution duration.
7Basic usage:
9.. code-block:: python
11 from mafw import timer
13 with Timer() as timer:
14 do_long_lasting_operation()
16When exiting from the context manager, a message with the duration of the process is printed.
18"""
20import logging
21from datetime import timedelta
22from time import perf_counter
23from types import TracebackType
26def pretty_format_duration(duration_s: float, n_digits: int = 1) -> str:
27 """
28 Return a formatted version of the duration with increased human readability.
30 :param duration_s: The duration to be printed in seconds. If negative, a ValueError exception is raised.
31 :type duration_s: float
32 :param n_digits: The number of decimal digits to show. Defaults to 1. If negative, a ValueError exception is raised.
33 :type n_digits: int, Optional
34 :return: The formatted string.
35 :rtype: str
36 :raises ValueError: if a negative duration or a negative number of digits is provided
37 """
38 if duration_s < 0:
39 raise ValueError(f'Duration ({duration_s}) cannot be a negative value')
40 if n_digits < 0:
41 raise ValueError('The number of decimal digits cannot be a negative value')
42 td = timedelta(seconds=duration_s)
43 days = td.days
44 hours = td.seconds // 3600
45 minutes = (td.seconds - hours * 3600) // 60
46 seconds = round((td.seconds - hours * 3600 - minutes * 60) + td.microseconds / 1e6, n_digits)
47 time_array = tuple([days, hours, minutes, seconds])
48 if time_array <= (0, 0, 0, 0):
49 return f'< {time_array[3]:.{n_digits}f} seconds'
50 up = ['days', 'hours', 'minutes', 'seconds']
51 us = ['day', 'hour', 'minute', 'second']
52 msg = ''
53 for ti, usi, upi in zip(time_array, us, up):
54 if ti == 0:
55 pass
56 elif ti == 1:
57 msg += f'{ti} {usi}, '
58 else:
59 msg += f'{ti} {upi}, '
61 return rreplace(msg.rstrip(', '), ', ', ' and ', 1)
64def rreplace(inp_string: str, old_string: str, new_string: str, counts: int) -> str:
65 """
66 Utility function to replace a substring in a given string a certain number of times starting from the right-most one.
68 This function is mimicking the behavior of the string.replace method, but instead of replacing from the left, it
69 is replacing from the right.
71 :param inp_string: The input string
72 :type inp_string: str
73 :param old_string: The old substring to be matched. If empty, a ValueError is raised.
74 :type old_string: str
75 :param new_string: The new substring to be replaced
76 :type new_string: str
77 :param counts: The number of times the old substring has to be replaced.
78 :type counts: int
79 :return: The modified string
80 :rtype: str
81 :raises ValueError: if old_string is empty.
82 """
83 if old_string == '':
84 raise ValueError('old_string cannot be empty')
85 li = inp_string.rsplit(old_string, counts)
86 return new_string.join(li)
89class Timer:
90 """
91 The timer class.
92 """
94 def __init__(self, suppress_message: bool = False) -> None:
95 """
96 Constructor parameter:
98 :param suppress_message: A boolean flag to mute the timer
99 :type suppress_message: bool
100 """
101 self._suppress_message = suppress_message
102 self.start = 0.0
103 self.end = 0.0
105 def __enter__(self) -> 'Timer':
106 """
107 Context manager enter dunder method.
109 When an instance of the Timer class is created via the context manager, the start time attribute is set to the
110 current time, while the end is set to zero.
112 :return: The class instance.
113 """
114 self.start = perf_counter()
115 self.end = 0.0
116 return self
118 def __exit__(
119 self, type_: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
120 ) -> None:
121 """
122 Context manager exit dunder method.
124 When the method is invoked, the end time is set to the current time, and, if the timer is not muted, a log
125 message with the total duration is printed.
127 :param type_: Exception type causing the context manager to exit. Defaults to None.
128 :type type_: type[BaseException], Optional
129 :param value: Exception that caused the context manager to exit. Defaults to None.
130 :type value: BaseException, Optional
131 :param traceback: Traceback. Defaults to None.
132 :type traceback: TracebackType
133 """
134 self.end = perf_counter()
135 if not self._suppress_message:
136 logging.getLogger(__name__).info('Total execution time: %s' % self.format_duration())
138 @property
139 def duration(self) -> float:
140 """
141 The elapsed time of the timer.
143 :return: Elapsed time in seconds.
144 """
145 return self.end - self.start
147 def format_duration(self) -> str:
148 """
149 Nicely format the timer duration.
151 :return: A string with the timer duration in a human-readable formatted string
152 :rtype: str
153 """
154 return pretty_format_duration(self.duration)