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

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. 

6 

7Basic usage: 

8 

9.. code-block:: python 

10 

11 from mafw import timer 

12 

13 with Timer() as timer: 

14 do_long_lasting_operation() 

15 

16When exiting from the context manager, a message with the duration of the process is printed. 

17 

18""" 

19 

20import logging 

21from datetime import timedelta 

22from time import perf_counter 

23from types import TracebackType 

24 

25 

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. 

29 

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}, ' 

60 

61 return rreplace(msg.rstrip(', '), ', ', ' and ', 1) 

62 

63 

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. 

67 

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. 

70 

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) 

87 

88 

89class Timer: 

90 """ 

91 The timer class. 

92 """ 

93 

94 def __init__(self, suppress_message: bool = False) -> None: 

95 """ 

96 Constructor parameter: 

97 

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 

104 

105 def __enter__(self) -> 'Timer': 

106 """ 

107 Context manager enter dunder method. 

108 

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. 

111 

112 :return: The class instance. 

113 """ 

114 self.start = perf_counter() 

115 self.end = 0.0 

116 return self 

117 

118 def __exit__( 

119 self, type_: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 

120 ) -> None: 

121 """ 

122 Context manager exit dunder method. 

123 

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. 

126 

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()) 

137 

138 @property 

139 def duration(self) -> float: 

140 """ 

141 The elapsed time of the timer. 

142 

143 :return: Elapsed time in seconds. 

144 """ 

145 return self.end - self.start 

146 

147 def format_duration(self) -> str: 

148 """ 

149 Nicely format the timer duration. 

150 

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)