Skip to content

Commit 95e6bda

Browse files
gahjelle-nbgahjelle
authored andcommitted
text can be a callable returning a formatted string
1 parent c82b87c commit 95e6bda

File tree

4 files changed

+80
-9
lines changed

4 files changed

+80
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99

1010
### Added
1111

12+
- `text` can be a callable returning a formatted string, suggested by [@dchess](https://github.com/dchess) in [#29] ([#30]).
1213
- Testing with [Interrogate](https://interrogate.readthedocs.io/) to enforce docstrings ([#27]).
1314

1415

@@ -50,3 +51,5 @@ Initial version of `codetiming`. Version 1.0.0 corresponds to the code in the tu
5051
[#24]: https://github.com/realpython/codetiming/issues/24
5152
[#25]: https://github.com/realpython/codetiming/pull/25
5253
[#27]: https://github.com/realpython/codetiming/pull/27
54+
[#29]: https://github.com/realpython/codetiming/issues/29
55+
[#30]: https://github.com/realpython/codetiming/pull/30

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ Note that the strings used by `text` are **not** f-strings. Instead they are use
7171
t = Timer(text=f"{__file__}: {{:.4f}}")
7272
```
7373

74+
`text` is also allowed to be a callable like a function or a class. If `text` is a callable, it is expected to require one argument: the number of seconds elapsed. It should return a text string that will be logged using logger:
75+
76+
```python
77+
t = Timer(text=lambda secs: f"{secs / 86400:.0f} days")
78+
```
79+
80+
This allows you to use third-party libraries like [`humanfriendly`](https://pypi.org/project/humanfriendly/) to do the text formatting:
81+
82+
```
83+
from humanfriendly import format_timespan
84+
85+
t1 = Timer(text=format_timespan)
86+
t2 = Timer(text=lambda secs: f"Elapsed time: {format_timespan(secs)}")
87+
```
88+
89+
7490

7591
## Capturing the Elapsed Time
7692

codetiming/_timer.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import time
1010
from contextlib import ContextDecorator
1111
from dataclasses import dataclass, field
12-
from typing import Any, Callable, ClassVar, Optional
12+
from typing import Any, Callable, ClassVar, Optional, Union
1313

1414
# Codetiming imports
1515
from codetiming._timers import Timers
@@ -26,7 +26,7 @@ class Timer(ContextDecorator):
2626
timers: ClassVar[Timers] = Timers()
2727
_start_time: Optional[float] = field(default=None, init=False, repr=False)
2828
name: Optional[str] = None
29-
text: str = "Elapsed time: {:0.4f} seconds"
29+
text: Union[str, Callable[[float], str]] = "Elapsed time: {:0.4f} seconds"
3030
logger: Optional[Callable[[str], None]] = print
3131
last: float = field(default=math.nan, init=False, repr=False)
3232

@@ -48,13 +48,17 @@ def stop(self) -> float:
4848

4949
# Report elapsed time
5050
if self.logger:
51-
attributes = {
52-
"name": self.name,
53-
"milliseconds": self.last * 1000,
54-
"seconds": self.last,
55-
"minutes": self.last / 60,
56-
}
57-
self.logger(self.text.format(self.last, **attributes))
51+
if callable(self.text):
52+
text = self.text(self.last)
53+
else:
54+
attributes = {
55+
"name": self.name,
56+
"milliseconds": self.last * 1000,
57+
"seconds": self.last,
58+
"minutes": self.last / 60,
59+
}
60+
text = self.text.format(self.last, **attributes)
61+
self.logger(text)
5862
if self.name:
5963
self.timers.add(self.name, self.last)
6064

tests/test_codetiming.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,54 @@ def test_using_milliseconds_attribute_in_text(capsys):
227227
assert int(milliseconds) == round(float(seconds) * 1000)
228228

229229

230+
def test_text_formatting_function(capsys):
231+
"""Test that text can be formatted by a separate function"""
232+
233+
def format_text(seconds):
234+
"""Function that returns a formatted text"""
235+
return f"Function: {seconds + 1:.0f}"
236+
237+
with Timer(text=format_text):
238+
waste_time()
239+
240+
stdout, stderr = capsys.readouterr()
241+
assert stdout.strip() == "Function: 1"
242+
assert not stderr.strip()
243+
244+
245+
def test_text_formatting_class(capsys):
246+
"""Test that text can be formatted by a separate class"""
247+
248+
class TextFormatter:
249+
"""Class that behaves like a formatted text"""
250+
251+
def __init__(self, seconds):
252+
"""Initialize with number of seconds"""
253+
self.seconds = seconds
254+
255+
def __str__(self):
256+
"""Represent the class as a formatted text"""
257+
return f"Class: {self.seconds + 1:.0f}"
258+
259+
with Timer(text=TextFormatter):
260+
waste_time()
261+
262+
stdout, stderr = capsys.readouterr()
263+
assert stdout.strip() == "Class: 1"
264+
assert not stderr.strip()
265+
266+
def format_text(seconds):
267+
"""Callable that returns a formatted text"""
268+
return f"Callable: {seconds + 1:.0f}"
269+
270+
with Timer(text=format_text):
271+
waste_time()
272+
273+
stdout, stderr = capsys.readouterr()
274+
assert stdout.strip() == "Callable: 1"
275+
assert not stderr.strip()
276+
277+
230278
def test_timers_cleared():
231279
"""Test that timers can be cleared"""
232280
with Timer(name="timer_to_be_cleared"):

0 commit comments

Comments
 (0)