Skip to content

Commit b2996f1

Browse files
Warn user if f-string expression contains await (#944)
Co-authored-by: Alex Hall <[email protected]>
1 parent 2dd65cb commit b2996f1

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

logfire/_internal/formatter.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ def logfire_format_with_magic(
336336
return ''.join(chunk['v'] for chunk in chunks), extra_attrs, new_template
337337
except KnownFormattingError as e:
338338
warn_formatting(str(e) or str(e.__cause__))
339+
except FStringAwaitError as e:
340+
warn_fstring_await(str(e))
339341
except Exception:
340342
# This is an unexpected error that likely indicates a bug in our logic.
341343
# Handle it here so that the span/log still gets created, just without a nice message.
@@ -354,6 +356,12 @@ def compile_formatted_value(node: ast.FormattedValue, ex_source: executing.Sourc
354356
3. Another code object which formats the value.
355357
"""
356358
source = get_node_source_text(node.value, ex_source)
359+
360+
# Check if the expression contains await before attempting to compile
361+
for sub_node in ast.walk(node.value):
362+
if isinstance(sub_node, ast.Await):
363+
raise FStringAwaitError(source)
364+
357365
value_code = compile(source, '<fvalue1>', 'eval')
358366
expr = ast.Expression(
359367
ast.JoinedStr(
@@ -435,6 +443,14 @@ class KnownFormattingError(Exception):
435443
"""
436444

437445

446+
class FStringAwaitError(Exception):
447+
"""An error raised when an await expression is found in an f-string.
448+
449+
This is a specific case that can't be handled by f-string introspection and requires
450+
pre-evaluating the await expression before logging.
451+
"""
452+
453+
438454
class FormattingFailedWarning(UserWarning):
439455
pass
440456

@@ -449,3 +465,17 @@ def warn_formatting(msg: str):
449465
f' The problem was: {msg}',
450466
category=FormattingFailedWarning,
451467
)
468+
469+
470+
def warn_fstring_await(msg: str):
471+
warn_at_user_stacklevel(
472+
f'\n'
473+
f' Cannot evaluate await expression in f-string. Pre-evaluate the expression before logging.\n'
474+
' For example, change:\n'
475+
' logfire.info(f"{await get_value()}")\n'
476+
' To:\n'
477+
' value = await get_value()\n'
478+
' logfire.info(f"{value}")\n'
479+
f' The problematic f-string value was: {msg}',
480+
category=FormattingFailedWarning,
481+
)

tests/test_formatter.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import contextlib
2+
import sys
23
from collections import ChainMap
34
from types import SimpleNamespace
45
from typing import Any, Mapping
56

67
import pytest
78
from inline_snapshot import snapshot
89

10+
import logfire
911
from logfire._internal.formatter import FormattingFailedWarning, chunks_formatter, logfire_format
1012
from logfire._internal.scrubbing import NOOP_SCRUBBER, JsonPath, Scrubber
13+
from logfire.testing import TestExporter
1114

1215

1316
def chunks(format_string: str, kwargs: Mapping[str, Any]):
@@ -178,3 +181,50 @@ def test_internal_exception_formatting(caplog: pytest.LogCaptureFixture):
178181
assert len(caplog.records) == 1
179182
assert caplog.records[0].message.startswith('Caught an internal error in Logfire.')
180183
assert str(caplog.records[0].exc_info[1]) == 'bad scrubber' # type: ignore
184+
185+
186+
@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason='fstring magic is only for 3.9+')
187+
@pytest.mark.anyio
188+
async def test_await_in_fstring(exporter: TestExporter):
189+
"""Test that logfire.info(f'{foo(await bar())}') evaluates the await expression and logs a warning."""
190+
191+
async def bar() -> str:
192+
return 'content data'
193+
194+
def foo(x: str) -> str:
195+
return x
196+
197+
with pytest.warns(FormattingFailedWarning) as warnings:
198+
logfire.info(f'{foo(await bar())}')
199+
[warning] = warnings
200+
assert str(warning.message) == snapshot(
201+
'\n'
202+
' Cannot evaluate await expression in f-string. Pre-evaluate the expression before logging.\n'
203+
' For example, change:\n'
204+
' logfire.info(f"{await get_value()}")\n'
205+
' To:\n'
206+
' value = await get_value()\n'
207+
' logfire.info(f"{value}")\n'
208+
' The problematic f-string value was: foo(await bar())'
209+
)
210+
211+
assert exporter.exported_spans_as_dict() == snapshot(
212+
[
213+
{
214+
'name': f'{foo(await bar())}',
215+
'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
216+
'parent': None,
217+
'start_time': 1000000000,
218+
'end_time': 1000000000,
219+
'attributes': {
220+
'logfire.span_type': 'log',
221+
'logfire.level_num': 9,
222+
'logfire.msg_template': f'{foo(await bar())}',
223+
'logfire.msg': 'content data',
224+
'code.filepath': 'test_formatter.py',
225+
'code.lineno': 123,
226+
'code.function': 'test_await_in_fstring',
227+
},
228+
}
229+
]
230+
)

0 commit comments

Comments
 (0)