Skip to content

Commit 5bcc344

Browse files
author
Peter Kirk
committed
Support Template Strings for log messages (#1397)
1 parent 7db92d5 commit 5bcc344

4 files changed

Lines changed: 80 additions & 4 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Add requirement for ``TERM`` environment variable not to be ``"dumb"`` to enable colorization (`#1287 <https://github.com/Delgan/loguru/pull/1287>`_, thanks `@snosov1 <https://github.com/snosov1>`_).
1212
- Make ``logger.catch()`` usable as an asynchronous context manager (`#1084 <https://github.com/Delgan/loguru/issues/1084>`_).
1313
- Make ``logger.catch()`` compatible with asynchronous generators (`#1302 <https://github.com/Delgan/loguru/issues/1302>`_).
14+
- Support python-3.14 template strings as log messages. The comfort of f-string syntax combined with the performance of lazily evaluated formatting. (`#1397 <https://github.com/Delgan/loguru/issues/1302>`_).
1415

1516

1617
`0.7.3`_ (2024-12-06)

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</a>
2121
</p>
2222

23-
______________________________________________________________________
23+
---
2424

2525
**Loguru** is a library which aims to bring enjoyable logging in Python.
2626

@@ -109,10 +109,24 @@ logger.add("file_Y.log", compression="zip") # Save some loved space
109109

110110
### Modern string formatting using braces style
111111

112-
Loguru favors the much more elegant and powerful `{}` formatting over `%`, logging functions are actually equivalent to `str.format()`.
112+
For python-3.14 and higher, you can pass a template string, which will be evaluated lazily. The behavior is the same as with a f-string, but the performance is better: Messages that are never emitted won't pay the cost of evaluation.
113113

114114
```python
115-
logger.info("If you're using Python {}, prefer {feature} of course!", 3.6, feature="f-strings")
115+
version = 3.14
116+
feature = "t-strings"
117+
logger.info(t"If you're using Python {version}, prefer {feature} of course!")
118+
```
119+
120+
Before python-3.14, you can still use lazily evaluated formatting and the elegant and powerful `{}` formatting: Use a str.format() style string and pass the parameters as additional arguments:
121+
122+
```python
123+
logger.info("If you're using Python {}, prefer {feature} of course!", 3.6, feature="str.format() style strings")
124+
```
125+
126+
Note that using f-strings in log messages is not considered best practice for performance reasons, as they are evaluated eagerly. Expensive conversion to string might occur even when in the end they are not needed:
127+
128+
```python
129+
logger.debug(f"obj = {large_obj}") # Do not do this, prefer the above methods!
116130
```
117131

118132
### Exceptions catching within threads or main

loguru/_logger.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ def isasyncgenfunction(func):
134134
return False
135135

136136

137+
try:
138+
from string.templatelib import Interpolation as _Interpolation
139+
from string.templatelib import Template as _Template
140+
from string.templatelib import convert as _tmpl_convert
141+
except ImportError:
142+
_Template = None # type: ignore[assignment,misc]
143+
_Interpolation = None # type: ignore[assignment,misc]
144+
_tmpl_convert = None # type: ignore[assignment,misc]
145+
146+
137147
Level = namedtuple("Level", ["name", "no", "color", "icon"]) # noqa: PYI024
138148

139149
start_time = aware_now()
@@ -2023,6 +2033,24 @@ def _find_iter(fileobj, regex, chunk):
20232033
buffer = buffer[end:]
20242034
yield from matches[:-1]
20252035

2036+
@staticmethod
2037+
def _message_to_string(message):
2038+
"""Message can be a string, Any or a Template (for python>=3.14).
2039+
2040+
For templates, we convert them into a string analogously to how f-string work.
2041+
Everything else is just converted to string via str(message).
2042+
"""
2043+
if _Template is None or not isinstance(message, _Template):
2044+
return str(message)
2045+
2046+
# Code follows PEP-750 example "implementing f-strings with t-strings"
2047+
def item_to_string(item):
2048+
if isinstance(item, _Interpolation):
2049+
return format(_tmpl_convert(item.value, item.conversion), item.format_spec)
2050+
return item
2051+
2052+
return "".join(item_to_string(item) for item in message)
2053+
20262054
def _log(self, level, from_decorator, options, message, args, kwargs):
20272055
core = self._core
20282056

@@ -2116,7 +2144,7 @@ def _log(self, level, from_decorator, options, message, args, kwargs):
21162144
"function": co_name,
21172145
"level": RecordLevel(level_name, level_no, level_icon),
21182146
"line": f_lineno,
2119-
"message": str(message),
2147+
"message": Logger._message_to_string(message),
21202148
"module": splitext(file_name)[0],
21212149
"name": name,
21222150
"process": RecordProcess(process.ident, process.name),

tests/test_formatting.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from loguru import logger
7+
from loguru._logger import _Interpolation, _Template
78

89

910
@pytest.mark.parametrize(
@@ -240,3 +241,35 @@ def test_invalid_color_markup(writer):
240241
ValueError, match="^Invalid format, color markups could not be parsed correctly$"
241242
):
242243
logger.add(writer, format="<red>Not closed tag", colorize=True)
244+
245+
246+
@pytest.mark.skipif(_Template is None, reason="Template Strings not supported")
247+
def test_template_string(writer):
248+
# We can't just use t"2**8 = {2**8}", because its a syntax error before python-3.14
249+
logger.add(writer)
250+
logger.info(_Template("2**8 = ", _Interpolation(2**8)))
251+
result = writer.read()
252+
assert result.endswith("2**8 = 256\n")
253+
254+
255+
@pytest.mark.skipif(_Template is None, reason="Template Strings not supported")
256+
def test_template_string_is_lazy(writer):
257+
# We can't just use t"debug = {debug_tracker}", because its a syntax error before python-3.14
258+
class StrCalledTracker:
259+
def __init__(self):
260+
self.str_called = False
261+
262+
def __str__(self):
263+
self.str_called = True
264+
return "xxx"
265+
266+
logger.add(writer, level="INFO")
267+
debug_tracker = StrCalledTracker()
268+
info_tracker = StrCalledTracker()
269+
logger.debug(_Template("debug = ", _Interpolation(debug_tracker))) # Should be ignored (debug)
270+
logger.info(_Template("info = ", _Interpolation(info_tracker))) # Should be logged (info)
271+
result = writer.read()
272+
assert len(result.strip().split("\n")) == 1
273+
assert result.endswith("info = xxx\n")
274+
assert not debug_tracker.str_called
275+
assert info_tracker.str_called

0 commit comments

Comments
 (0)