@@ -166,10 +166,19 @@ def deindent(lines: Iterable[str]) -> list[str]:
166166 return textwrap .dedent ("\n " .join (lines )).splitlines ()
167167
168168
169- def get_statement_startend2 (lineno : int , node : ast .AST ) -> tuple [int , int | None ]:
169+ def _statement_linenos (node : ast .AST ) -> list [int ]:
170+ """Sorted 0-based line numbers of all statements below ``node``.
171+
172+ Walking the tree is proportional to the size of the whole module, while a
173+ traceback asks for many line numbers of the same module -- so the result is
174+ memoized on the node, which shares the lifetime of the caller's ast cache.
175+ """
176+ values : list [int ] | None = getattr (node , "_pytest_statement_linenos" , None )
177+ if values is not None :
178+ return values
170179 # Flatten all statements and except handlers into one lineno-list.
171180 # AST's line numbers start indexing at 1.
172- values : list [ int ] = []
181+ values = []
173182 for x in ast .walk (node ):
174183 if isinstance (x , ast .stmt | ast .ExceptHandler ):
175184 # The lineno points to the class/def, so need to include the decorators.
@@ -183,6 +192,15 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None
183192 # Treat the finally/orelse part as its own statement.
184193 values .append (val [0 ].lineno - 1 - 1 )
185194 values .sort ()
195+ try :
196+ node ._pytest_statement_linenos = values # type: ignore[attr-defined]
197+ except AttributeError : # pragma: no cover - defensive
198+ pass
199+ return values
200+
201+
202+ def get_statement_startend2 (lineno : int , node : ast .AST ) -> tuple [int , int | None ]:
203+ values = _statement_linenos (node )
186204 insert_index = bisect_right (values , lineno )
187205 if insert_index == 0 :
188206 return 0 , None
0 commit comments