diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3f497..cfe2e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- `text` can be a callable returning a formatted string, suggested by [@dchess](https://github.com/dchess) in [#29] ([#30]). - Testing with [Interrogate](https://interrogate.readthedocs.io/) to enforce docstrings ([#27]). @@ -50,3 +51,5 @@ Initial version of `codetiming`. Version 1.0.0 corresponds to the code in the tu [#24]: https://github.com/realpython/codetiming/issues/24 [#25]: https://github.com/realpython/codetiming/pull/25 [#27]: https://github.com/realpython/codetiming/pull/27 +[#29]: https://github.com/realpython/codetiming/issues/29 +[#30]: https://github.com/realpython/codetiming/pull/30 diff --git a/README.md b/README.md index b1e9b53..0b57fb7 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,22 @@ Note that the strings used by `text` are **not** f-strings. Instead they are use t = Timer(text=f"{__file__}: {{:.4f}}") ``` +`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: + +```python +t = Timer(text=lambda secs: f"{secs / 86400:.0f} days") +``` + +This allows you to use third-party libraries like [`humanfriendly`](https://pypi.org/project/humanfriendly/) to do the text formatting: + +``` +from humanfriendly import format_timespan + +t1 = Timer(text=format_timespan) +t2 = Timer(text=lambda secs: f"Elapsed time: {format_timespan(secs)}") +``` + + ## Capturing the Elapsed Time diff --git a/codetiming/_timer.py b/codetiming/_timer.py index 39b483e..08a3787 100644 --- a/codetiming/_timer.py +++ b/codetiming/_timer.py @@ -9,7 +9,7 @@ import time from contextlib import ContextDecorator from dataclasses import dataclass, field -from typing import Any, Callable, ClassVar, Optional +from typing import Any, Callable, ClassVar, Optional, Union # Codetiming imports from codetiming._timers import Timers @@ -26,7 +26,7 @@ class Timer(ContextDecorator): timers: ClassVar[Timers] = Timers() _start_time: Optional[float] = field(default=None, init=False, repr=False) name: Optional[str] = None - text: str = "Elapsed time: {:0.4f} seconds" + text: Union[str, Callable[[float], str]] = "Elapsed time: {:0.4f} seconds" logger: Optional[Callable[[str], None]] = print last: float = field(default=math.nan, init=False, repr=False) @@ -48,13 +48,17 @@ def stop(self) -> float: # Report elapsed time if self.logger: - attributes = { - "name": self.name, - "milliseconds": self.last * 1000, - "seconds": self.last, - "minutes": self.last / 60, - } - self.logger(self.text.format(self.last, **attributes)) + if callable(self.text): + text = self.text(self.last) + else: + attributes = { + "name": self.name, + "milliseconds": self.last * 1000, + "seconds": self.last, + "minutes": self.last / 60, + } + text = self.text.format(self.last, **attributes) + self.logger(text) if self.name: self.timers.add(self.name, self.last) diff --git a/tests/test_codetiming.py b/tests/test_codetiming.py index bd77ef3..c916c2c 100644 --- a/tests/test_codetiming.py +++ b/tests/test_codetiming.py @@ -227,6 +227,54 @@ def test_using_milliseconds_attribute_in_text(capsys): assert int(milliseconds) == round(float(seconds) * 1000) +def test_text_formatting_function(capsys): + """Test that text can be formatted by a separate function""" + + def format_text(seconds): + """Function that returns a formatted text""" + return f"Function: {seconds + 1:.0f}" + + with Timer(text=format_text): + waste_time() + + stdout, stderr = capsys.readouterr() + assert stdout.strip() == "Function: 1" + assert not stderr.strip() + + +def test_text_formatting_class(capsys): + """Test that text can be formatted by a separate class""" + + class TextFormatter: + """Class that behaves like a formatted text""" + + def __init__(self, seconds): + """Initialize with number of seconds""" + self.seconds = seconds + + def __str__(self): + """Represent the class as a formatted text""" + return f"Class: {self.seconds + 1:.0f}" + + with Timer(text=TextFormatter): + waste_time() + + stdout, stderr = capsys.readouterr() + assert stdout.strip() == "Class: 1" + assert not stderr.strip() + + def format_text(seconds): + """Callable that returns a formatted text""" + return f"Callable: {seconds + 1:.0f}" + + with Timer(text=format_text): + waste_time() + + stdout, stderr = capsys.readouterr() + assert stdout.strip() == "Callable: 1" + assert not stderr.strip() + + def test_timers_cleared(): """Test that timers can be cleared""" with Timer(name="timer_to_be_cleared"):