Skip to content

Commit 5ec8d00

Browse files
committed
Refactor handling of error raised for missing formatting keys
The former error message was a bit misleading for cases where the KeyError originates from a missing nested key (e.g. "extra[missing]"). The message was changed to something more generic. The error type was also changed to `ValueError` since `KeyError` is usually expected to only contain the missing key, not a verbose message. Scope of the `try / catch` was reduced to only wrap the `format_map()` function. Tests were extended to cover all branches.
1 parent 9891ff8 commit 5ec8d00

5 files changed

Lines changed: 53 additions & 61 deletions

File tree

loguru/_handler.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ def emit(self, record, level_id, from_decorator, is_raw, colored_message):
158158
elif self._is_formatter_dynamic:
159159
if not self._colorize:
160160
precomputed_format = self._memoize_dynamic_format(dynamic_format)
161-
formatted = precomputed_format.format_map(formatter_record)
161+
formatted = self._format_record(precomputed_format, formatter_record)
162162
elif colored_message is None:
163163
ansi_level = self._levels_ansi_codes[level_id]
164164
_, precomputed_format = self._memoize_dynamic_format(dynamic_format, ansi_level)
165-
formatted = precomputed_format.format_map(formatter_record)
165+
formatted = self._format_record(precomputed_format, formatter_record)
166166
else:
167167
ansi_level = self._levels_ansi_codes[level_id]
168168
formatter, precomputed_format = self._memoize_dynamic_format(
@@ -172,24 +172,24 @@ def emit(self, record, level_id, from_decorator, is_raw, colored_message):
172172
record["message"], ansi_level=ansi_level, colored_message=colored_message
173173
)
174174
formatter_record["message"] = coloring_message
175-
formatted = precomputed_format.format_map(formatter_record)
175+
formatted = self._format_record(precomputed_format, formatter_record)
176176

177177
else:
178178
if not self._colorize:
179179
precomputed_format = self._decolorized_format
180-
formatted = precomputed_format.format_map(formatter_record)
180+
formatted = self._format_record(precomputed_format, formatter_record)
181181
elif colored_message is None:
182182
ansi_level = self._levels_ansi_codes[level_id]
183183
precomputed_format = self._precolorized_formats[level_id]
184-
formatted = precomputed_format.format_map(formatter_record)
184+
formatted = self._format_record(precomputed_format, formatter_record)
185185
else:
186186
ansi_level = self._levels_ansi_codes[level_id]
187187
precomputed_format = self._precolorized_formats[level_id]
188188
coloring_message = self._formatter.make_coloring_message(
189189
record["message"], ansi_level=ansi_level, colored_message=colored_message
190190
)
191191
formatter_record["message"] = coloring_message
192-
formatted = precomputed_format.format_map(formatter_record)
192+
formatted = self._format_record(precomputed_format, formatter_record)
193193

194194
if self._serialize:
195195
formatted = self._serialize_record(formatted, record)
@@ -204,10 +204,6 @@ def emit(self, record, level_id, from_decorator, is_raw, colored_message):
204204
self._queue.put(str_record)
205205
else:
206206
self._sink.write(str_record)
207-
except KeyError as e:
208-
if not self._error_interceptor.should_catch():
209-
raise self._make_key_error(e, record) from e
210-
self._error_interceptor.print(record, exception=self._make_key_error(e, record))
211207
except Exception:
212208
if not self._error_interceptor.should_catch():
213209
raise
@@ -248,22 +244,25 @@ def update_format(self, level_id):
248244
ansi_code = self._levels_ansi_codes[level_id]
249245
self._precolorized_formats[level_id] = self._formatter.colorize(ansi_code)
250246

251-
@staticmethod
252-
def _make_key_error(original, record):
253-
key = original.args[0] if original.args else "?"
254-
available = ", ".join(sorted(record.keys()))
255-
message = (
256-
"The format string references an unknown key '%s'. "
257-
"Available record keys are: %s. "
258-
"To include custom data, use 'logger.bind(key=value)' and reference it "
259-
"as '{extra[key]}' in the format string." % (key, available)
260-
)
261-
return KeyError(message)
262-
263247
@property
264248
def levelno(self):
265249
return self._levelno
266250

251+
@staticmethod
252+
def _format_record(log_format, record):
253+
try:
254+
return log_format.format_map(record)
255+
except KeyError as e:
256+
available = ", ".join(map(repr, record.keys()))
257+
raise ValueError(
258+
"Failed to format log record: key %s not found.\n"
259+
"Verify that the format string %r only references valid record keys "
260+
"and that all required extra keys are present.\n"
261+
"Available records key are: %s.\n"
262+
"To include custom data, use `logger.bind(key=value)` and reference it "
263+
"as '{extra[key]}' in the format string." % (e, log_format, available)
264+
) from e
265+
267266
@staticmethod
268267
def _serialize_record(text, record):
269268
exception = record["exception"]

tests/test_add_option_format.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -85,40 +85,3 @@ def __format__(self, spec):
8585
def test_invalid_format_builtin(writer):
8686
with pytest.raises(ValueError, match=r".* most likely a mistake"):
8787
logger.add(writer, format=format)
88-
89-
90-
def test_invalid_format_key_emits_helpful_error_with_catch(capsys):
91-
logger.add(lambda msg: None, format="{nonexistent}", catch=True)
92-
logger.info("Hello")
93-
stderr = capsys.readouterr().err
94-
assert "nonexistent" in stderr
95-
assert "logger.bind(key=value)" in stderr
96-
assert "extra[key]" in stderr
97-
# Check that available record keys are listed
98-
for key in ["elapsed", "exception", "extra", "file", "level", "message"]:
99-
assert key in stderr
100-
101-
102-
def test_invalid_format_key_raises_enhanced_error_without_catch():
103-
logger.add(lambda msg: None, format="{nonexistent}", catch=False)
104-
with pytest.raises(KeyError, match=r"nonexistent"):
105-
logger.info("Hello")
106-
107-
108-
def test_invalid_format_key_error_message_content():
109-
logger.add(lambda msg: None, format="{bogus_key}", catch=False)
110-
with pytest.raises(KeyError, match=r"logger\.bind") as exc_info:
111-
logger.info("Hello")
112-
error_message = str(exc_info.value)
113-
assert "bogus_key" in error_message
114-
assert "extra[key]" in error_message
115-
116-
117-
def test_patcher_added_keys_work_in_format(writer):
118-
def patcher(record):
119-
record["my_value"] = 42
120-
121-
logger.add(writer, format="{message} | {my_value}")
122-
logger.configure(patcher=patcher)
123-
logger.info("Hello")
124-
assert "Hello | 42" in writer.read()

tests/test_configure.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def test_reset_previous_extra(writer):
138138

139139
logger.configure(extra={})
140140

141-
with pytest.raises(KeyError):
141+
with pytest.raises(ValueError, match=r"Failed to format log record: key 'a' not found."):
142142
logger.debug("Nope")
143143

144144

@@ -148,7 +148,7 @@ def test_reset_previous_patcher(writer):
148148

149149
logger.configure(patcher=lambda r: None)
150150

151-
with pytest.raises(KeyError):
151+
with pytest.raises(ValueError, match=r"Failed to format log record: key 'a' not found."):
152152
logger.debug("Nope")
153153

154154

tests/test_formatting.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,23 @@ def test_invalid_color_markup(writer):
246246
ValueError, match=r"^Invalid format, color markups could not be parsed correctly$"
247247
):
248248
logger.add(writer, format="<red>Not closed tag", colorize=True)
249+
250+
251+
@pytest.mark.parametrize("format_", ["{missing}", lambda _: "{missing}\n"])
252+
@pytest.mark.parametrize("colorize", [True, False])
253+
@pytest.mark.parametrize("colors", [True, False])
254+
def test_invalid_format_key_emits_helpful_error_with_catch(capsys, format_, colorize, colors):
255+
logger.add(lambda msg: None, format=format_, catch=True, colorize=colorize)
256+
logger.opt(colors=colors).info("Hello")
257+
out, err = capsys.readouterr()
258+
assert out == ""
259+
assert "ValueError: Failed to format log record: key 'missing' not found" in err
260+
261+
262+
@pytest.mark.parametrize("format_", ["{missing}", lambda _: "{missing}\n"])
263+
@pytest.mark.parametrize("colorize", [True, False])
264+
@pytest.mark.parametrize("colors", [True, False])
265+
def test_invalid_format_key_raises_enhanced_error_without_catch(format_, colorize, colors):
266+
logger.add(lambda msg: None, format=format_, catch=False, colorize=colorize)
267+
with pytest.raises(ValueError, match=r"Failed to format log record: key 'missing' not found."):
268+
logger.opt(colors=colors).info("Hello")

tests/test_patch.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,13 @@ def patch_3(record):
8383
logger.patch(patch_1).patch(patch_2).patch(patch_3).info("Test")
8484

8585
assert writer.read() == "12 Test\n"
86+
87+
88+
def test_patcher_added_keys_work_in_format(writer):
89+
def patcher(record):
90+
record["my_value"] = 42
91+
92+
logger.add(writer, format="{message} | {my_value}")
93+
logger.configure(patcher=patcher)
94+
logger.info("Hello")
95+
assert "Hello | 42" in writer.read()

0 commit comments

Comments
 (0)