From afd14673e8955372e5bff8c94bcac7c19810c2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon-Martin=20Schr=C3=B6der?= Date: Thu, 28 Oct 2021 22:59:08 +0200 Subject: [PATCH 1/6] bpo-43656: Introduce format_locals in traceback This allows customization of the string representation of the locals of stack frames. Also, remind users that __repr__ shouldn't raise. --- Doc/library/traceback.rst | 22 +++++--- Doc/reference/datamodel.rst | 2 + Lib/test/test_traceback.py | 100 ++++++++++++++++++++++++++++-------- Lib/traceback.py | 31 +++++++---- 4 files changed, 115 insertions(+), 40 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index df4a38c955511b..49d6e5eeaee6e5 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -212,10 +212,10 @@ The module also defines the following classes: :class:`TracebackException` objects are created from actual exceptions to capture data for later printing in a lightweight fashion. -.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False) +.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, format_locals=None, compact=False) - Capture an exception for later rendering. *limit*, *lookup_lines* and - *capture_locals* are as for the :class:`StackSummary` class. + Capture an exception for later rendering. *limit*, *lookup_lines*, + *capture_locals* and *format_locals* are as for the :class:`StackSummary` class. If *compact* is true, only data that is required by :class:`TracebackException`'s ``format`` method is saved in the class attributes. In particular, the @@ -264,10 +264,10 @@ capture data for later printing in a lightweight fashion. For syntax errors - the compiler error message. - .. classmethod:: from_exception(exc, *, limit=None, lookup_lines=True, capture_locals=False) + .. classmethod:: from_exception(exc, *, limit=None, lookup_lines=True, capture_locals=False, format_locals=None) - Capture an exception for later rendering. *limit*, *lookup_lines* and - *capture_locals* are as for the :class:`StackSummary` class. + Capture an exception for later rendering. *limit*, *lookup_lines*, + *capture_locals* and *format_locals* are as for the :class:`StackSummary` class. Note that when locals are captured, they are also shown in the traceback. @@ -319,7 +319,7 @@ capture data for later printing in a lightweight fashion. .. class:: StackSummary - .. classmethod:: extract(frame_gen, *, limit=None, lookup_lines=True, capture_locals=False) + .. classmethod:: extract(frame_gen, *, limit=None, lookup_lines=True, capture_locals=False, format_locals=None) Construct a :class:`StackSummary` object from a frame generator (such as is returned by :func:`~traceback.walk_stack` or @@ -331,7 +331,13 @@ capture data for later printing in a lightweight fashion. creating the :class:`StackSummary` cheaper (which may be valuable if it may not actually get formatted). If *capture_locals* is ``True`` the local variables in each :class:`FrameSummary` are captured as object - representations. + representations. If *format_locals* is provided, it is called with four arguments + (filename, lineno, name, locals) to generate string representations + of the local variables in each frame. + + .. + This should be reworded. I'm not sure how callable parameters are usually documented in Python. + .. classmethod:: from_list(a_list) diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index f1334f047d4b73..3be1d9f91861a9 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1380,6 +1380,8 @@ Basic customization This is typically used for debugging, so it is important that the representation is information-rich and unambiguous. + Furthermore, this function should avoid to raise exceptions because that can + lead to problems in some debugging contexts. .. index:: single: string; __str__() (object method) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 1c7db9d3d47376..80e78d893630c7 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1546,7 +1546,7 @@ def test_no_locals(self): s = traceback.StackSummary.extract(iter([(f, 6)])) self.assertEqual(s[0].locals, None) - def test_format_locals(self): + def test_format_locals_default(self): def some_inner(k, v): a = 1 b = 2 @@ -1562,6 +1562,26 @@ def some_inner(k, v): ' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 3) ], s.format()) + def test_format_locals_callback(self): + def _format_locals(filename, lineno, name, locals): + return {k: "<"+repr(v)+">" for k,v in locals.items() if not k.startswith("_")} + + def some_inner(k, v): + a = 1 + b = 2 + return traceback.StackSummary.extract( + traceback.walk_stack(None), capture_locals=True, format_locals=_format_locals, limit=1) + + s = some_inner(3, 4) + self.assertEqual( + [' File "%s", line %d, in some_inner\n' + ' return traceback.StackSummary.extract(\n' + ' a = <1>\n' + ' b = <2>\n' + ' k = <3>\n' + ' v = <4>\n' % (__file__, some_inner.__code__.co_firstlineno + 3) + ], s.format()) + def test_custom_format_frame(self): class CustomStackSummary(traceback.StackSummary): def format_frame_summary(self, frame_summary): @@ -1577,32 +1597,32 @@ def some_inner(): [f'{__file__}:{some_inner.__code__.co_firstlineno + 1}']) def test_dropping_frames(self): - def f(): - 1/0 + def f(): + 1/0 - def g(): - try: - f() - except: - return sys.exc_info() + def g(): + try: + f() + except: + return sys.exc_info() - exc_info = g() + exc_info = g() - class Skip_G(traceback.StackSummary): - def format_frame_summary(self, frame_summary): - if frame_summary.name == 'g': - return None - return super().format_frame_summary(frame_summary) + class Skip_G(traceback.StackSummary): + def format_frame_summary(self, frame_summary): + if frame_summary.name == 'g': + return None + return super().format_frame_summary(frame_summary) - stack = Skip_G.extract( - traceback.walk_tb(exc_info[2])).format() + stack = Skip_G.extract( + traceback.walk_tb(exc_info[2])).format() - self.assertEqual(len(stack), 1) - lno = f.__code__.co_firstlineno + 1 - self.assertEqual( - stack[0], - f' File "{__file__}", line {lno}, in f\n 1/0\n' - ) + self.assertEqual(len(stack), 1) + lno = f.__code__.co_firstlineno + 1 + self.assertEqual( + stack[0], + f' File "{__file__}", line {lno}, in f\n 1/0\n' + ) class TestTracebackException(unittest.TestCase): @@ -1644,6 +1664,42 @@ def foo(): self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(str(exc_info[1]), str(exc)) + def test_from_exception_format_locals(self): + # Check that format_locals works as expected. + + def try_repr(o): + try: + return repr(o) + except: + return object.__repr__(o) + + def format_locals(filename, lineno, name, locals): + return {k: try_repr(v) for k,v in locals.items()} + + class FailingInit: + def __init__(self) -> None: + self.x = 1/0 + def __repr__(self): + return self.x + + try: + FailingInit() + except Exception as e: + exc_info = sys.exc_info() + self.expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2]), limit=2, lookup_lines=False, + capture_locals=True, format_locals=format_locals) + self.exc = traceback.TracebackException.from_exception( + e, limit=2, lookup_lines=False, capture_locals=True, format_locals=format_locals) + expected_stack = self.expected_stack + exc = self.exc + self.assertEqual(None, exc.__cause__) + self.assertEqual(None, exc.__context__) + self.assertEqual(False, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + def test_cause(self): try: try: diff --git a/Lib/traceback.py b/Lib/traceback.py index 568f3ff28c29b2..76de503d72cc1b 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -250,7 +250,7 @@ class FrameSummary: - :attr:`line` The text from the linecache module for the of code that was running when the frame was captured. - :attr:`locals` Either None if locals were not supplied, or a dict - mapping the name to the repr() of the variable. + mapping the name to a string representation of the variable. """ __slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno', @@ -258,7 +258,7 @@ class FrameSummary: def __init__(self, filename, lineno, name, *, lookup_line=True, locals=None, line=None, - end_lineno=None, colno=None, end_colno=None): + end_lineno=None, colno=None, end_colno=None, format_locals=None): """Construct a FrameSummary. :param lookup_line: If True, `linecache` is consulted for the source @@ -267,6 +267,8 @@ def __init__(self, filename, lineno, name, *, lookup_line=True, object representations. :param line: If provided, use this instead of looking up the line in the linecache. + :param format_locals: If provided, use this instead of repr() to generate the + string representations. """ self.filename = filename self.lineno = lineno @@ -274,7 +276,12 @@ def __init__(self, filename, lineno, name, *, lookup_line=True, self._line = line if lookup_line: self.line - self.locals = {k: repr(v) for k, v in locals.items()} if locals else None + + if locals: + self.locals = format_locals(filename, lineno, name, locals) if format_locals is not None else {k: repr(v) for k, v in locals.items()} + else: + self.locals = None + self.end_lineno = end_lineno self.colno = colno self.end_colno = end_colno @@ -369,7 +376,7 @@ class StackSummary(list): @classmethod def extract(klass, frame_gen, *, limit=None, lookup_lines=True, - capture_locals=False): + capture_locals=False, format_locals=None): """Create a StackSummary from a traceback or stack object. :param frame_gen: A generator that yields (frame, lineno) tuples @@ -380,6 +387,8 @@ def extract(klass, frame_gen, *, limit=None, lookup_lines=True, otherwise lookup is deferred until the frame is rendered. :param capture_locals: If True, the local variables from each frame will be captured as object representations into the FrameSummary. + :param format_locals: If provided, the local variables from each frame will + be formatted using this callable. """ def extended_frame_gen(): for f, lineno in frame_gen: @@ -387,11 +396,11 @@ def extended_frame_gen(): return klass._extract_from_extended_frame_gen( extended_frame_gen(), limit=limit, lookup_lines=lookup_lines, - capture_locals=capture_locals) + capture_locals=capture_locals, format_locals=format_locals) @classmethod def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None, - lookup_lines=True, capture_locals=False): + lookup_lines=True, capture_locals=False, format_locals=None): # Same as extract but operates on a frame generator that yields # (frame, (lineno, end_lineno, colno, end_colno)) in the stack. # Only lineno is required, the remaining fields can be None if the @@ -422,7 +431,7 @@ def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None, f_locals = None result.append(FrameSummary( filename, lineno, name, lookup_line=False, locals=f_locals, - end_lineno=end_lineno, colno=colno, end_colno=end_colno)) + end_lineno=end_lineno, colno=colno, end_colno=end_colno, format_locals=format_locals)) for filename in fnames: linecache.checkcache(filename) # If immediate lookup was desired, trigger lookups now. @@ -634,7 +643,7 @@ class TracebackException: """ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, - lookup_lines=True, capture_locals=False, compact=False, + lookup_lines=True, capture_locals=False, format_locals=None, compact=False, _seen=None): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we @@ -645,11 +654,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, _seen = set() _seen.add(id(exc_value)) - # TODO: locals. + # TODO: locals. # <- This could be removed, right? self.stack = StackSummary._extract_from_extended_frame_gen( _walk_tb_with_full_positions(exc_traceback), limit=limit, lookup_lines=lookup_lines, - capture_locals=capture_locals) + capture_locals=capture_locals, format_locals=format_locals) self.exc_type = exc_type # Capture now to permit freeing resources: only complication is in the # unofficial API _format_final_exc_line @@ -685,6 +694,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals, + format_locals=format_locals, _seen=_seen) else: cause = None @@ -704,6 +714,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals, + format_locals=format_locals, _seen=_seen) else: context = None From 39b29ef081986b737c92f6dc19c0a40a559295cb Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 28 Oct 2021 21:02:02 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst diff --git a/Misc/NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst b/Misc/NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst new file mode 100644 index 00000000000000..78d7b87d0e6a44 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst @@ -0,0 +1 @@ +Introduce format_locals in traceback \ No newline at end of file From a37e983e5fd07a1fc55a85163a8b88797c0237d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon-Martin=20Schr=C3=B6der?= Date: Fri, 29 Oct 2021 08:17:20 +0200 Subject: [PATCH 3/6] Add versionchanged and more context for doc --- Doc/library/traceback.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 49d6e5eeaee6e5..f5ee520e5d48b1 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -332,11 +332,16 @@ capture data for later printing in a lightweight fashion. may not actually get formatted). If *capture_locals* is ``True`` the local variables in each :class:`FrameSummary` are captured as object representations. If *format_locals* is provided, it is called with four arguments - (filename, lineno, name, locals) to generate string representations + (filename, lineno, name, locals) to generate a dict of string representations of the local variables in each frame. .. This should be reworded. I'm not sure how callable parameters are usually documented in Python. + The signature is (filename, lineno, name, locals: dict[str, Any]) -> dict[str, str]. + This should be somehow clear from the text above. + + .. versionchanged:: XXX + Added the *format_locals* parameter. .. classmethod:: from_list(a_list) From 30cd72cda4d84d01bee7f3c805a484240a903776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon-Martin=20Schr=C3=B6der?= Date: Mon, 8 Nov 2021 11:36:28 +0100 Subject: [PATCH 4/6] Change wording in documentation and add example --- Doc/library/traceback.rst | 20 +++++++++++++++++++- Lib/traceback.py | 10 ++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index f5ee520e5d48b1..a17114972afbd1 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -332,7 +332,7 @@ capture data for later printing in a lightweight fashion. may not actually get formatted). If *capture_locals* is ``True`` the local variables in each :class:`FrameSummary` are captured as object representations. If *format_locals* is provided, it is called with four arguments - (filename, lineno, name, locals) to generate a dict of string representations + (filename, lineno, name, locals) to generate a :class:`dict` of string representations of the local variables in each frame. .. @@ -531,6 +531,24 @@ The following example shows the different ways to print and format the stack:: ' File "", line 3, in another_function\n lumberstack()\n', ' File "", line 8, in lumberstack\n print(repr(traceback.format_stack()))\n'] +The following example shows how to use *format_locals* to change the +formatting of local variables:: + + >>> import traceback + >>> from unittest.util import safe_repr + >>> def format_locals(filename, lineno, name, locals): + ... return {k: safe_repr(v) for k,v in locals.items()} + ... + ... class A: + ... def __repr__(self): + ... raise ValueError("Unrepresentable") + ... try: + ... a = A() + ... 1/0 + ... except Exception as e: + ... print(traceback.TracebackException.from_exception( + ... e, limit=1, capture_locals=True, format_locals=format_locals)) + This last example demonstrates the final few formatting functions: diff --git a/Lib/traceback.py b/Lib/traceback.py index 76de503d72cc1b..4e73a8536881ac 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -267,8 +267,9 @@ def __init__(self, filename, lineno, name, *, lookup_line=True, object representations. :param line: If provided, use this instead of looking up the line in the linecache. - :param format_locals: If provided, use this instead of repr() to generate the - string representations. + :param format_locals: If provided, use this callable to transform + the supplied locals into a dictionary of string representations. + By default, repr() is applied to every value. """ self.filename = filename self.lineno = lineno @@ -387,8 +388,9 @@ def extract(klass, frame_gen, *, limit=None, lookup_lines=True, otherwise lookup is deferred until the frame is rendered. :param capture_locals: If True, the local variables from each frame will be captured as object representations into the FrameSummary. - :param format_locals: If provided, the local variables from each frame will - be formatted using this callable. + :param format_locals: If provided, this callable will be used to + transform the local variables in each frame into a dictionary + of string representations. """ def extended_frame_gen(): for f, lineno in frame_gen: From dbcb9215a4b8cd0e3129ba176c774fd0a131a860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon-Martin=20Schr=C3=B6der?= Date: Tue, 9 Nov 2021 12:45:12 +0100 Subject: [PATCH 5/6] Improve example --- Doc/library/traceback.rst | 62 +++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index a17114972afbd1..ce47ccda960e9e 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -531,24 +531,54 @@ The following example shows the different ways to print and format the stack:: ' File "", line 3, in another_function\n lumberstack()\n', ' File "", line 8, in lumberstack\n print(repr(traceback.format_stack()))\n'] -The following example shows how to use *format_locals* to change the -formatting of local variables:: +The following example shows how to use *format_locals* to filter and change the +formatting of local variables. - >>> import traceback - >>> from unittest.util import safe_repr - >>> def format_locals(filename, lineno, name, locals): - ... return {k: safe_repr(v) for k,v in locals.items()} - ... - ... class A: - ... def __repr__(self): - ... raise ValueError("Unrepresentable") - ... try: - ... a = A() - ... 1/0 - ... except Exception as e: - ... print(traceback.TracebackException.from_exception( - ... e, limit=1, capture_locals=True, format_locals=format_locals)) +.. testcode:: format_locals + + import traceback + from unittest.util import safe_repr + + def format_locals(filename, lineno, name, locals): + return { + k: safe_repr(v) # Handle exceptions thrown by __repr__ + for k, v in locals.items() + if not k.startswith("_") # Hide private variables + } + class A: + def __repr__(self): + raise ValueError("Unrepresentable") + + try: + a = A() + _pw = "supersecretpassword" + 1 / 0 + except Exception as e: + print( + "".join( + traceback.TracebackException.from_exception( + e, limit=1, capture_locals=True, format_locals=format_locals + ).format() + ) + ) + +The output would look like this: + +.. testoutput:: format_locals + :options: +NORMALIZE_WHITESPACE + + Traceback (most recent call last): + File "...", line 18, in + 1 / 0 + ~~^~~ + A = + a = + e = ZeroDivisionError('division by zero') + format_locals = + safe_repr = + traceback = + ZeroDivisionError: division by zero This last example demonstrates the final few formatting functions: From b1229011af9b1bceff945ebdd456b7d6d8fcfd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon-Martin=20Schr=C3=B6der?= Date: Wed, 10 Nov 2021 17:37:22 +0100 Subject: [PATCH 6/6] Add docstrings for FrameSummary and remove unnecessary parameters for format_locals. --- Doc/library/traceback.rst | 13 ++++--------- Lib/test/test_traceback.py | 13 ++++--------- Lib/traceback.py | 8 +++++++- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index ce47ccda960e9e..869eb89138a282 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -331,14 +331,9 @@ capture data for later printing in a lightweight fashion. creating the :class:`StackSummary` cheaper (which may be valuable if it may not actually get formatted). If *capture_locals* is ``True`` the local variables in each :class:`FrameSummary` are captured as object - representations. If *format_locals* is provided, it is called with four arguments - (filename, lineno, name, locals) to generate a :class:`dict` of string representations - of the local variables in each frame. - - .. - This should be reworded. I'm not sure how callable parameters are usually documented in Python. - The signature is (filename, lineno, name, locals: dict[str, Any]) -> dict[str, str]. - This should be somehow clear from the text above. + representations. If *format_locals* is provided, it is used to + generate a :class:`dict` of string representations for a frame's + local variables. .. versionchanged:: XXX Added the *format_locals* parameter. @@ -539,7 +534,7 @@ formatting of local variables. import traceback from unittest.util import safe_repr - def format_locals(filename, lineno, name, locals): + def format_locals(locals): return { k: safe_repr(v) # Handle exceptions thrown by __repr__ for k, v in locals.items() diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index b6c42711e66386..30c3f3a1760aec 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -12,6 +12,7 @@ requires_debug_ranges, has_no_debug_ranges) from test.support.os_helper import TESTFN, unlink from test.support.script_helper import assert_python_ok, assert_python_failure +from unittest.util import safe_repr import os import textwrap @@ -1885,7 +1886,7 @@ def some_inner(k, v): ], s.format()) def test_format_locals_callback(self): - def _format_locals(filename, lineno, name, locals): + def _format_locals(locals): return {k: "<"+repr(v)+">" for k,v in locals.items() if not k.startswith("_")} def some_inner(k, v): @@ -1989,14 +1990,8 @@ def foo(): def test_from_exception_format_locals(self): # Check that format_locals works as expected. - def try_repr(o): - try: - return repr(o) - except: - return object.__repr__(o) - - def format_locals(filename, lineno, name, locals): - return {k: try_repr(v) for k,v in locals.items()} + def format_locals(locals): + return {k: safe_repr(v) for k,v in locals.items()} class FailingInit: def __init__(self) -> None: diff --git a/Lib/traceback.py b/Lib/traceback.py index ef12a2da7414bb..dec09a5c565301 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -262,12 +262,18 @@ def __init__(self, filename, lineno, name, *, lookup_line=True, end_lineno=None, colno=None, end_colno=None, format_locals=None): """Construct a FrameSummary. + :param filename: The filename for the frame. + :param lineno: The line within filename for the frame that was + active when the frame was captured. + :param name: The name of the function or method that was executing + when the frame was captured. :param lookup_line: If True, `linecache` is consulted for the source code line. Otherwise, the line will be looked up when first needed. :param locals: If supplied the frame locals, which will be captured as object representations. :param line: If provided, use this instead of looking up the line in the linecache. + :param end_lineno: The end linenumber where the error occurred. :param format_locals: If provided, use this callable to transform the supplied locals into a dictionary of string representations. By default, repr() is applied to every value. @@ -280,7 +286,7 @@ def __init__(self, filename, lineno, name, *, lookup_line=True, self.line if locals: - self.locals = format_locals(filename, lineno, name, locals) if format_locals is not None else {k: repr(v) for k, v in locals.items()} + self.locals = format_locals(locals) if format_locals is not None else {k: repr(v) for k, v in locals.items()} else: self.locals = None