Skip to content

Commit 33adce0

Browse files
Peter KirkDelgan
authored andcommitted
Support Template Strings for log messages (#1397)
1 parent 5ec8d00 commit 33adce0

4 files changed

Lines changed: 81 additions & 4 deletions

File tree

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
- Make ``logger.catch()`` usable as an asynchronous context manager (`#1084 <https://github.com/Delgan/loguru/issues/1084>`_).
1717
- Make ``logger.catch()`` compatible with asynchronous generators (`#1302 <https://github.com/Delgan/loguru/issues/1302>`_).
1818
- Improve feedback for invalid format keys in logger format strings (`#1450 <https://github.com/Delgan/loguru/issues/1450>`_, thanks `@Krishnachaitanyakc <https://github.com/Krishnachaitanyakc>`_).
19+
- 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>`_).
20+
1921

2022
`0.7.3`_ (2024-12-06)
2123
=====================

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
@@ -136,6 +136,16 @@ def isasyncgenfunction(func):
136136
return False
137137

138138

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

141151
start_time = aware_now()
@@ -2025,6 +2035,24 @@ def _find_iter(fileobj, regex, chunk):
20252035
buffer = buffer[end:]
20262036
yield from matches[:-1]
20272037

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

@@ -2118,7 +2146,7 @@ def _log(self, level, from_decorator, options, message, args, kwargs):
21182146
"function": co_name,
21192147
"level": RecordLevel(level_name, level_no, level_icon),
21202148
"line": f_lineno,
2121-
"message": str(message),
2149+
"message": Logger._message_to_string(message),
21222150
"module": splitext(file_name)[0],
21232151
"name": name,
21242152
"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(
@@ -266,3 +267,35 @@ def test_invalid_format_key_raises_enhanced_error_without_catch(format_, coloriz
266267
logger.add(lambda msg: None, format=format_, catch=False, colorize=colorize)
267268
with pytest.raises(ValueError, match=r"Failed to format log record: key 'missing' not found."):
268269
logger.opt(colors=colors).info("Hello")
270+
271+
272+
@pytest.mark.skipif(_Template is None, reason="Template Strings not supported")
273+
def test_template_string(writer):
274+
# We can't just use t"2**8 = {2**8}", because its a syntax error before python-3.14
275+
logger.add(writer)
276+
logger.info(_Template("2**8 = ", _Interpolation(2**8)))
277+
result = writer.read()
278+
assert result.endswith("2**8 = 256\n")
279+
280+
281+
@pytest.mark.skipif(_Template is None, reason="Template Strings not supported")
282+
def test_template_string_is_lazy(writer):
283+
# We can't just use t"debug = {debug_tracker}", because its a syntax error before python-3.14
284+
class StrCalledTracker:
285+
def __init__(self):
286+
self.str_called = False
287+
288+
def __str__(self):
289+
self.str_called = True
290+
return "xxx"
291+
292+
logger.add(writer, level="INFO")
293+
debug_tracker = StrCalledTracker()
294+
info_tracker = StrCalledTracker()
295+
logger.debug(_Template("debug = ", _Interpolation(debug_tracker))) # Should be ignored (debug)
296+
logger.info(_Template("info = ", _Interpolation(info_tracker))) # Should be logged (info)
297+
result = writer.read()
298+
assert len(result.strip().split("\n")) == 1
299+
assert result.endswith("info = xxx\n")
300+
assert not debug_tracker.str_called
301+
assert info_tracker.str_called

0 commit comments

Comments
 (0)