11import contextlib
22import faulthandler
3+ import linecache
34import locale
45import math
56import os .path
@@ -140,6 +141,39 @@ def print_warning(msg: str) -> None:
140141orig_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+
143177def 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
167202def regrtest_unraisable_hook (unraisable ) -> None :
0 commit comments