Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]).


Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 13 additions & 9 deletions codetiming/_timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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)

Expand Down
48 changes: 48 additions & 0 deletions tests/test_codetiming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down