Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- 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>`_).
- Make ``logger.catch()`` usable as an asynchronous context manager (`#1084 <https://github.com/Delgan/loguru/issues/1084>`_).
- Make ``logger.catch()`` compatible with asynchronous generators (`#1302 <https://github.com/Delgan/loguru/issues/1302>`_).
- 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>`_).


`0.7.3`_ (2024-12-06)
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</a>
</p>

______________________________________________________________________
---

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

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

### Modern string formatting using braces style

Loguru favors the much more elegant and powerful `{}` formatting over `%`, logging functions are actually equivalent to `str.format()`.
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.

```python
logger.info("If you're using Python {}, prefer {feature} of course!", 3.6, feature="f-strings")
version = 3.14
feature = "t-strings"
logger.info(t"If you're using Python {version}, prefer {feature} of course!")
```

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:

```python
logger.info("If you're using Python {}, prefer {feature} of course!", 3.6, feature="str.format() style strings")
```

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:

```python
logger.debug(f"obj = {large_obj}") # Do not do this, prefer the above methods!
```

### Exceptions catching within threads or main
Expand Down
30 changes: 29 additions & 1 deletion loguru/_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ def isasyncgenfunction(func):
return False


try:
from string.templatelib import Interpolation as _Interpolation
from string.templatelib import Template as _Template
from string.templatelib import convert as _tmpl_convert
except ImportError:
_Template = None # type: ignore[assignment,misc]
_Interpolation = None # type: ignore[assignment,misc]
_tmpl_convert = None # type: ignore[assignment,misc]


Level = namedtuple("Level", ["name", "no", "color", "icon"]) # noqa: PYI024

start_time = aware_now()
Expand Down Expand Up @@ -2023,6 +2033,24 @@ def _find_iter(fileobj, regex, chunk):
buffer = buffer[end:]
yield from matches[:-1]

@staticmethod
def _message_to_string(message):
"""Message can be a string, Any or a Template (for python>=3.14).

For templates, we convert them into a string analogously to how f-string work.
Everything else is just converted to string via str(message).
"""
if _Template is None or not isinstance(message, _Template):
return str(message)

# Code follows PEP-750 example "implementing f-strings with t-strings"
def item_to_string(item):
if isinstance(item, _Interpolation):
return format(_tmpl_convert(item.value, item.conversion), item.format_spec)
return item

return "".join(item_to_string(item) for item in message)

def _log(self, level, from_decorator, options, message, args, kwargs):
core = self._core

Expand Down Expand Up @@ -2116,7 +2144,7 @@ def _log(self, level, from_decorator, options, message, args, kwargs):
"function": co_name,
"level": RecordLevel(level_name, level_no, level_icon),
"line": f_lineno,
"message": str(message),
"message": Logger._message_to_string(message),
"module": splitext(file_name)[0],
"name": name,
"process": RecordProcess(process.ident, process.name),
Expand Down
33 changes: 33 additions & 0 deletions tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from loguru import logger
from loguru._logger import _Interpolation, _Template


@pytest.mark.parametrize(
Expand Down Expand Up @@ -240,3 +241,35 @@ def test_invalid_color_markup(writer):
ValueError, match="^Invalid format, color markups could not be parsed correctly$"
):
logger.add(writer, format="<red>Not closed tag", colorize=True)


@pytest.mark.skipif(_Template is None, reason="Template Strings not supported")
def test_template_string(writer):
# We can't just use t"2**8 = {2**8}", because its a syntax error before python-3.14
logger.add(writer)
logger.info(_Template("2**8 = ", _Interpolation(2**8)))
result = writer.read()
assert result.endswith("2**8 = 256\n")


@pytest.mark.skipif(_Template is None, reason="Template Strings not supported")
def test_template_string_is_lazy(writer):
# We can't just use t"debug = {debug_tracker}", because its a syntax error before python-3.14
class StrCalledTracker:
def __init__(self):
self.str_called = False

def __str__(self):
self.str_called = True
return "xxx"

logger.add(writer, level="INFO")
debug_tracker = StrCalledTracker()
info_tracker = StrCalledTracker()
logger.debug(_Template("debug = ", _Interpolation(debug_tracker))) # Should be ignored (debug)
logger.info(_Template("info = ", _Interpolation(info_tracker))) # Should be logged (info)
result = writer.read()
assert len(result.strip().split("\n")) == 1
assert result.endswith("info = xxx\n")
assert not debug_tracker.str_called
assert info_tracker.str_called