Skip to content

Commit bfdfebb

Browse files
Preserve SyntaxError source line in message on Python 3.12+
Python 3.12 eagerly normalizes the error indicator, so PyErr_Fetch now hands us the SyntaxError instance (whose str() omits the offending source line) instead of the raw args tuple (whose str() included it). Callers that surface PythonException.Message for compile diagnostics therefore lost the offending source text on 3.12+. GetMessage now re-appends the SyntaxError 'text' attribute when present. This is a no-op on <=3.11 (there the fetched value is a tuple without the SyntaxError attributes) and only affects SyntaxError messages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 73389fc commit bfdfebb

1 file changed

Lines changed: 50 additions & 1 deletion

File tree

‎src/runtime/PythonException.cs‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,61 @@ private static string GetMessage(PyObject? value, PyType type)
252252

253253
if (value != null && !value.IsNone())
254254
{
255-
return value.ToString() ?? "no message";
255+
var message = value.ToString() ?? "no message";
256+
257+
// Python 3.12+ eagerly normalizes the error indicator, so a SyntaxError
258+
// reaches us as the exception instance whose str() omits the offending
259+
// source line. Pre-3.12 we received the raw args tuple, whose str()
260+
// included it. Re-append the source text so the message stays complete
261+
// for callers that surface it (e.g. compile diagnostics). This is a
262+
// no-op on <=3.11 (there 'value' is a tuple without these attributes).
263+
if (TryGetSyntaxErrorText(value, out var sourceText))
264+
{
265+
message = $"{message}: {sourceText}";
266+
}
267+
268+
return message;
256269
}
257270

258271
return type.Name;
259272
}
260273

274+
/// <summary>
275+
/// If <paramref name="value"/> is a SyntaxError instance carrying the offending
276+
/// source line (its <c>text</c> attribute), returns that trimmed text.
277+
/// </summary>
278+
private static bool TryGetSyntaxErrorText(PyObject value, out string text)
279+
{
280+
text = string.Empty;
281+
try
282+
{
283+
// 'msg' + 'text' is the distinctive SyntaxError shape; bail otherwise.
284+
if (!value.HasAttr("msg") || !value.HasAttr("text"))
285+
{
286+
return false;
287+
}
288+
289+
using var textObj = value.GetAttr("text");
290+
if (textObj.IsNone())
291+
{
292+
return false;
293+
}
294+
295+
var sourceLine = textObj.ToString();
296+
if (string.IsNullOrWhiteSpace(sourceLine))
297+
{
298+
return false;
299+
}
300+
301+
text = sourceLine.Trim();
302+
return true;
303+
}
304+
catch (PythonException)
305+
{
306+
return false;
307+
}
308+
}
309+
261310
private static string TracebackToString(PyObject traceback)
262311
{
263312
if (traceback is null)

0 commit comments

Comments
 (0)