Skip to content

Commit 04754ef

Browse files
authored
Add custom Timers class (#23)
* Create a custom class for .timers * Fix type hints in Timers class * Update imports with isort
1 parent 951e24c commit 04754ef

File tree

4 files changed

+159
-22
lines changed

4 files changed

+159
-22
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77

88
## [Unreleased]
99

10-
## [1.1.0] - 2020-01-14
10+
### Changed
11+
12+
- `Timer.timers` changed from regular to `dict` to a custom dictionary supporting basic statistics for named timers.
13+
14+
15+
## [1.1.0] - 2020-01-15
1116

1217
### Added
1318

codetiming/_timer.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import time
1010
from contextlib import ContextDecorator
1111
from dataclasses import dataclass, field
12-
from typing import Any, Callable, ClassVar, Dict, Optional
12+
from typing import Any, Callable, ClassVar, Optional
13+
14+
# Codetiming imports
15+
from codetiming._timers import Timers
1316

1417

1518
class TimerError(Exception):
@@ -20,18 +23,13 @@ class TimerError(Exception):
2023
class Timer(ContextDecorator):
2124
"""Time your code using a class, context manager, or decorator"""
2225

23-
timers: ClassVar[Dict[str, float]] = dict()
26+
timers: ClassVar[Timers] = Timers()
2427
_start_time: Optional[float] = field(default=None, init=False, repr=False)
2528
name: Optional[str] = None
2629
text: str = "Elapsed time: {:0.4f} seconds"
2730
logger: Optional[Callable[[str], None]] = print
2831
last: float = field(default=math.nan, init=False, repr=False)
2932

30-
def __post_init__(self) -> None:
31-
"""Initialization: add timer to dict of timers"""
32-
if self.name:
33-
self.timers.setdefault(self.name, 0)
34-
3533
def start(self) -> None:
3634
"""Start a new timer"""
3735
if self._start_time is not None:
@@ -52,7 +50,7 @@ def stop(self) -> float:
5250
if self.logger:
5351
self.logger(self.text.format(self.last))
5452
if self.name:
55-
self.timers[self.name] += self.last
53+
self.timers.add(self.name, self.last)
5654

5755
return self.last
5856

codetiming/_timers.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Dictionary-like structure with information about timers"""
2+
3+
# Standard library imports
4+
import collections
5+
import math
6+
import statistics
7+
from typing import TYPE_CHECKING, Any, Callable, Dict, List
8+
9+
# Annotate generic UserDict
10+
if TYPE_CHECKING:
11+
UserDict = collections.UserDict[str, float] # pragma: no cover
12+
else:
13+
UserDict = collections.UserDict
14+
15+
16+
class Timers(UserDict):
17+
def __init__(self, *args: Any, **kwargs: Any) -> None:
18+
"""Add a private dictionary keeping track of all timings"""
19+
super().__init__(*args, **kwargs)
20+
self._timings: Dict[str, List[float]] = collections.defaultdict(list)
21+
22+
def add(self, name: str, value: float) -> None:
23+
"""Add a timing value to the given timer"""
24+
self._timings[name].append(value)
25+
self.data.setdefault(name, 0)
26+
self.data[name] += value
27+
28+
def clear(self) -> None:
29+
"""Clear timers"""
30+
self.data.clear()
31+
self._timings.clear()
32+
33+
def __setitem__(self, name: str, value: float) -> None:
34+
"""Disallow setting of timer values"""
35+
raise TypeError(
36+
f"{self.__class__.__name__!r} does not support item assignment. "
37+
"Use '.add()' to update values."
38+
)
39+
40+
def apply(self, func: Callable[[List[float]], float], name: str) -> float:
41+
"""Apply a function to the results of one named timer"""
42+
if name in self._timings:
43+
return func(self._timings[name])
44+
raise KeyError(name)
45+
46+
def count(self, name: str) -> float:
47+
"""Number of timings"""
48+
return self.apply(len, name=name)
49+
50+
def total(self, name: str) -> float:
51+
"""Total time for timers"""
52+
return self.apply(sum, name=name)
53+
54+
def min(self, name: str) -> float:
55+
"""Minimal value of timings"""
56+
return self.apply(lambda values: min(values or [0]), name=name)
57+
58+
def max(self, name: str) -> float:
59+
"""Maximal value of timings"""
60+
return self.apply(lambda values: max(values or [0]), name=name)
61+
62+
def mean(self, name: str) -> float:
63+
"""Mean value of timings"""
64+
return self.apply(lambda values: statistics.mean(values or [0]), name=name)
65+
66+
def median(self, name: str) -> float:
67+
"""Median value of timings"""
68+
return self.apply(lambda values: statistics.median(values or [0]), name=name)
69+
70+
def stdev(self, name: str) -> float:
71+
"""Standard deviation of timings"""
72+
if name in self._timings:
73+
value = self._timings[name]
74+
return statistics.stdev(value) if len(value) >= 2 else math.nan
75+
raise KeyError(name)

tests/test_codetiming.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@
2121
RE_TIME_MESSAGE = re.compile(TIME_PREFIX + r" 0\.\d{4} seconds")
2222

2323

24+
def waste_time(num=1000):
25+
"""Just waste a little bit of time"""
26+
sum(n ** 2 for n in range(num))
27+
28+
2429
@Timer(text=TIME_MESSAGE)
25-
def timewaster(num):
30+
def decorated_timewaste(num=1000):
2631
"""Just waste a little bit of time"""
2732
sum(n ** 2 for n in range(num))
2833

2934

3035
@Timer(name="accumulator", text=TIME_MESSAGE)
31-
def accumulated_timewaste(num):
36+
def accumulated_timewaste(num=1000):
3237
"""Just waste a little bit of time"""
3338
sum(n ** 2 for n in range(num))
3439

@@ -48,7 +53,7 @@ def __call__(self, message):
4853
#
4954
def test_timer_as_decorator(capsys):
5055
"""Test that decorated function prints timing information"""
51-
timewaster(1000)
56+
decorated_timewaste()
5257
stdout, stderr = capsys.readouterr()
5358
assert RE_TIME_MESSAGE.match(stdout)
5459
assert stdout.count("\n") == 1
@@ -58,7 +63,7 @@ def test_timer_as_decorator(capsys):
5863
def test_timer_as_context_manager(capsys):
5964
"""Test that timed context prints timing information"""
6065
with Timer(text=TIME_MESSAGE):
61-
sum(n ** 2 for n in range(1000))
66+
waste_time()
6267
stdout, stderr = capsys.readouterr()
6368
assert RE_TIME_MESSAGE.match(stdout)
6469
assert stdout.count("\n") == 1
@@ -69,7 +74,7 @@ def test_explicit_timer(capsys):
6974
"""Test that timed section prints timing information"""
7075
t = Timer(text=TIME_MESSAGE)
7176
t.start()
72-
sum(n ** 2 for n in range(1000))
77+
waste_time()
7378
t.stop()
7479
stdout, stderr = capsys.readouterr()
7580
assert RE_TIME_MESSAGE.match(stdout)
@@ -96,14 +101,14 @@ def test_custom_logger():
96101
"""Test that we can use a custom logger"""
97102
logger = CustomLogger()
98103
with Timer(text=TIME_MESSAGE, logger=logger):
99-
sum(n ** 2 for n in range(1000))
104+
waste_time()
100105
assert RE_TIME_MESSAGE.match(logger.messages)
101106

102107

103108
def test_timer_without_text(capsys):
104109
"""Test that timer with logger=None does not print anything"""
105110
with Timer(logger=None):
106-
sum(n ** 2 for n in range(1000))
111+
waste_time()
107112

108113
stdout, stderr = capsys.readouterr()
109114
assert stdout == ""
@@ -112,8 +117,8 @@ def test_timer_without_text(capsys):
112117

113118
def test_accumulated_decorator(capsys):
114119
"""Test that decorated timer can accumulate"""
115-
accumulated_timewaste(1000)
116-
accumulated_timewaste(1000)
120+
accumulated_timewaste()
121+
accumulated_timewaste()
117122

118123
stdout, stderr = capsys.readouterr()
119124
lines = stdout.strip().split("\n")
@@ -127,9 +132,9 @@ def test_accumulated_context_manager(capsys):
127132
"""Test that context manager timer can accumulate"""
128133
t = Timer(name="accumulator", text=TIME_MESSAGE)
129134
with t:
130-
sum(n ** 2 for n in range(1000))
135+
waste_time()
131136
with t:
132-
sum(n ** 2 for n in range(1000))
137+
waste_time()
133138

134139
stdout, stderr = capsys.readouterr()
135140
lines = stdout.strip().split("\n")
@@ -144,10 +149,10 @@ def test_accumulated_explicit_timer(capsys):
144149
t = Timer(name="accumulated_explicit_timer", text=TIME_MESSAGE)
145150
total = 0
146151
t.start()
147-
sum(n ** 2 for n in range(1000))
152+
waste_time()
148153
total += t.stop()
149154
t.start()
150-
sum(n ** 2 for n in range(1000))
155+
waste_time()
151156
total += t.stop()
152157

153158
stdout, stderr = capsys.readouterr()
@@ -179,3 +184,57 @@ def test_timer_sets_last():
179184
time.sleep(0.02)
180185

181186
assert t.last >= 0.02
187+
188+
189+
def test_timers_cleared():
190+
"""Test that timers can be cleared"""
191+
with Timer(name="timer_to_be_cleared"):
192+
waste_time()
193+
194+
assert "timer_to_be_cleared" in Timer.timers
195+
Timer.timers.clear()
196+
assert not Timer.timers
197+
198+
199+
def test_running_cleared_timers():
200+
"""Test that timers can still be run after they're cleared"""
201+
t = Timer(name="timer_to_be_cleared")
202+
Timer.timers.clear()
203+
204+
accumulated_timewaste()
205+
with t:
206+
waste_time()
207+
208+
assert "accumulator" in Timer.timers
209+
assert "timer_to_be_cleared" in Timer.timers
210+
211+
212+
def test_timers_stats():
213+
"""Test that we can get basic statistics from timers"""
214+
name = "timer_with_stats"
215+
t = Timer(name=name)
216+
for num in range(5, 10):
217+
with t:
218+
waste_time(num=100 * num)
219+
220+
stats = Timer.timers
221+
assert stats.total(name) == stats[name]
222+
assert stats.count(name) == 5
223+
assert stats.min(name) <= stats.median(name) <= stats.max(name)
224+
assert stats.mean(name) >= stats.min(name)
225+
assert stats.stdev(name) >= 0
226+
227+
228+
def test_stats_missing_timers():
229+
"""Test that getting statistics from non-existent timers raises exception"""
230+
with pytest.raises(KeyError):
231+
Timer.timers.count("non_existent_timer")
232+
233+
with pytest.raises(KeyError):
234+
Timer.timers.stdev("non_existent_timer")
235+
236+
237+
def test_setting_timers_exception():
238+
"""Test that setting .timers items raises exception"""
239+
with pytest.raises(TypeError):
240+
Timer.timers["set_timer"] = 1.23

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy