Skip to content

Commit

Permalink
text can be a callable returning a formatted string
Browse files Browse the repository at this point in the history
  • Loading branch information
gahjelle-nb authored and gahjelle committed Feb 9, 2021
1 parent c82b87c commit 95e6bda
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 9 deletions.
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

0 comments on commit 95e6bda

Please sign in to comment.