@@ -208,6 +208,26 @@ def with_repr_style(
208208 def lineno (self ) -> int :
209209 return self ._rawentry .tb_lineno - 1
210210
211+ def get_python_framesummary (self ) -> traceback .FrameSummary :
212+ # Python's built-in traceback module implements all the nitty gritty
213+ # details to get column numbers of out frames.
214+ stack_summary = traceback .extract_tb (self ._rawentry , limit = 1 )
215+ return stack_summary [0 ]
216+
217+ @property
218+ def end_lineno (self ) -> int :
219+ return self .get_python_framesummary ().end_lineno - 1
220+
221+ @property
222+ def colno (self ) -> int | None :
223+ """Starting byte offset of the expression in the traceback entry."""
224+ return self .get_python_framesummary ().colno
225+
226+ @property
227+ def end_colno (self ) -> int | None :
228+ """Ending byte offset of the expression in the traceback entry."""
229+ return self .get_python_framesummary ().end_colno
230+
211231 @property
212232 def frame (self ) -> Frame :
213233 return Frame (self ._rawentry .tb_frame )
@@ -856,6 +876,9 @@ def get_source(
856876 line_index : int = - 1 ,
857877 excinfo : ExceptionInfo [BaseException ] | None = None ,
858878 short : bool = False ,
879+ end_line_index : int | None = None ,
880+ colno : int | None = None ,
881+ end_colno : int | None = None ,
859882 ) -> list [str ]:
860883 """Return formatted and marked up source lines."""
861884 lines = []
@@ -869,17 +892,76 @@ def get_source(
869892 space_prefix = " "
870893 if short :
871894 lines .append (space_prefix + source .lines [line_index ].strip ())
895+ lines .extend (
896+ self .get_highlight_arrows_for_line (
897+ raw_line = source .raw_lines [line_index ],
898+ line = source .lines [line_index ].strip (),
899+ lineno = line_index ,
900+ end_lineno = end_line_index ,
901+ colno = colno ,
902+ end_colno = end_colno ,
903+ )
904+ )
872905 else :
873906 for line in source .lines [:line_index ]:
874907 lines .append (space_prefix + line )
875908 lines .append (self .flow_marker + " " + source .lines [line_index ])
909+ lines .extend (
910+ self .get_highlight_arrows_for_line (
911+ raw_line = source .raw_lines [line_index ],
912+ line = source .lines [line_index ],
913+ lineno = line_index ,
914+ end_lineno = end_line_index ,
915+ colno = colno ,
916+ end_colno = end_colno ,
917+ )
918+ )
876919 for line in source .lines [line_index + 1 :]:
877920 lines .append (space_prefix + line )
878921 if excinfo is not None :
879922 indent = 4 if short else self ._getindent (source )
880923 lines .extend (self .get_exconly (excinfo , indent = indent , markall = True ))
881924 return lines
882925
926+ def get_highlight_arrows_for_line (
927+ self ,
928+ line : str ,
929+ raw_line : str ,
930+ lineno : int | None ,
931+ end_lineno : int | None ,
932+ colno : int | None ,
933+ end_colno : int | None ,
934+ ) -> list [str ]:
935+ """Return characters highlighting a source line.
936+
937+ Example with colno and end_colno pointing to the bar expression:
938+ "foo() + bar()"
939+ returns " ^^^^^"
940+ """
941+ if lineno != end_lineno :
942+ # Don't handle expressions that span multiple lines.
943+ return []
944+ if colno is None or end_colno is None :
945+ # Can't do anything without column information.
946+ return []
947+
948+ num_stripped_chars = len (raw_line ) - len (line )
949+
950+ start_char_offset = traceback ._byte_offset_to_character_offset (raw_line , colno )
951+ end_char_offset = traceback ._byte_offset_to_character_offset (
952+ raw_line , end_colno
953+ )
954+ num_carets = end_char_offset - start_char_offset
955+ # If the highlight would span the whole line, it is redundant, don't
956+ # show it.
957+ if num_carets >= len (line .strip ()):
958+ return []
959+
960+ highlights = " "
961+ highlights += " " * (start_char_offset - num_stripped_chars + 1 )
962+ highlights += "^" * num_carets
963+ return [highlights ]
964+
883965 def get_exconly (
884966 self ,
885967 excinfo : ExceptionInfo [BaseException ],
@@ -939,11 +1021,23 @@ def repr_traceback_entry(
9391021 if source is None :
9401022 source = Source ("???" )
9411023 line_index = 0
1024+ end_line_index , colno , end_colno = None , None , None
9421025 else :
9431026 line_index = entry .lineno - entry .getfirstlinesource ()
1027+ end_line_index = entry .end_lineno - entry .getfirstlinesource ()
1028+ colno = entry .colno
1029+ end_colno = entry .end_colno
9441030 short = style == "short"
9451031 reprargs = self .repr_args (entry ) if not short else None
946- s = self .get_source (source , line_index , excinfo , short = short )
1032+ s = self .get_source (
1033+ source = source ,
1034+ line_index = line_index ,
1035+ excinfo = excinfo ,
1036+ short = short ,
1037+ end_line_index = end_line_index ,
1038+ colno = colno ,
1039+ end_colno = end_colno ,
1040+ )
9471041 lines .extend (s )
9481042 if short :
9491043 message = f"in { entry .name } "
0 commit comments