Skip to content

Commit 14394da

Browse files
committed
More structured error blocks
1 parent 946c801 commit 14394da

1 file changed

Lines changed: 45 additions & 10 deletions

File tree

‎Lib/test/libregrtest/utils.py‎

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import faulthandler
3+
import linecache
34
import locale
45
import math
56
import os.path
@@ -140,6 +141,39 @@ def print_warning(msg: str) -> None:
140141
orig_unraisablehook: Callable[..., None] | None = None
141142

142143

144+
def traceback_location(traceback: str) -> dict[str, str | int] | None:
145+
"""Location of the last frame of a traceback, relative to the repository.
146+
147+
Return a dict with file and line, plus col and endColumn when the
148+
traceback underlines the failing expression (~~~^^^).
149+
"""
150+
lines = traceback.splitlines()
151+
for index in reversed(range(len(lines))):
152+
if match := re.match(r' File "(.+)", line (\d+)', lines[index]):
153+
break
154+
else:
155+
return None
156+
filename, lineno = match.group(1), int(match.group(2))
157+
# Map the stdlib directory (Lib/ in a source checkout or an installed
158+
# lib/python3.X/) to Lib/ in the repository
159+
stdlib_dir = os.path.dirname(os.__file__) + os.sep
160+
if not filename.startswith(stdlib_dir):
161+
return None
162+
relpath = filename.removeprefix(stdlib_dir).replace(os.sep, "/")
163+
location: dict[str, str | int] = {"file": f"Lib/{relpath}", "line": lineno}
164+
165+
# The frame is followed by the dedented source line (" code") and,
166+
# for a single-line expression, by a line of ~ and ^ markers.
167+
markers = lines[index + 2] if index + 2 < len(lines) else ""
168+
source = linecache.getline(filename, lineno)
169+
if markers.strip() and not markers.strip(" ~^") and source:
170+
indent = len(source) - len(source.lstrip())
171+
start = len(markers) - len(markers.lstrip())
172+
location["col"] = indent + start - 4 + 1
173+
location["endColumn"] = indent + len(markers.rstrip()) - 4
174+
return location
175+
176+
143177
def github_annotation(level: str, title: str, message: str) -> None:
144178
"""Emit a GitHub Actions annotation (workflow command).
145179
@@ -152,16 +186,17 @@ def escape(text: str) -> str:
152186
def escape_property(text: str) -> str:
153187
return escape(text).replace(":", "%3A").replace(",", "%2C")
154188

155-
props = f"title={escape_property(title)}"
156-
frames = re.findall(r'^ File "(.+)", line (\d+)', message, re.MULTILINE)
157-
# Map the stdlib directory (Lib/ in a source checkout or an installed
158-
# lib/python3.X/) to Lib/ in the repository
159-
stdlib_dir = os.path.dirname(os.__file__) + os.sep
160-
if frames and frames[-1][0].startswith(stdlib_dir):
161-
filename, line = frames[-1]
162-
filename = filename.removeprefix(stdlib_dir).replace(os.sep, "/")
163-
props = f"file=Lib/{escape_property(filename)},line={line},{props}"
164-
print(f"::{level} {props}::{escape(message)}", flush=True)
189+
props: dict[str, str | int] = {}
190+
if location := traceback_location(message):
191+
props |= location
192+
# The job log only shows the message: start it with the location
193+
position = ":".join(str(location[key])
194+
for key in ("file", "line", "col") if key in location)
195+
message = f"{position}\n{message}"
196+
props["title"] = title
197+
props_text = ",".join(f"{key}={escape_property(str(value))}"
198+
for key, value in props.items())
199+
print(f"::{level} {props_text}::{escape(message)}", flush=True)
165200

166201

167202
def regrtest_unraisable_hook(unraisable) -> None:

0 commit comments

Comments
 (0)