diff --git a/AUTHORS.rst b/AUTHORS.rst index 6d7199a..7d1ffc5 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -17,3 +17,4 @@ Authors * Oldřich Jedlička - https://github.com/oldium * Colin Watson - https://github.com/cjwatson * Jacob Tomlinson - https://github.com/jacobtomlinson +* Shahadat Islam Koyel - https://github.com/ShahadatTest diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed95b83..b52b867 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +Unreleased +~~~~~~~~~~ + +* Avoid recursion errors when converting deep tracebacks to and from dictionaries. + 3.2.2 (2025-11-12) ~~~~~~~~~~~~~~~~~~ diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 546c327..c29b573 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -158,26 +158,26 @@ def as_dict(self): Converts to a dictionary representation. You can serialize the result to JSON as it only has builtin objects like dicts, lists, ints or strings. """ - if self.tb_next is None: - tb_next = None - else: - tb_next = self.tb_next.as_dict() - - code = { - 'co_filename': self.tb_frame.f_code.co_filename, - 'co_name': self.tb_frame.f_code.co_name, - } - frame = { - 'f_globals': self.tb_frame.f_globals, - 'f_locals': self.tb_frame.f_locals, - 'f_code': code, - 'f_lineno': self.tb_frame.f_lineno, - } - return { - 'tb_frame': frame, - 'tb_lineno': self.tb_lineno, - 'tb_next': tb_next, - } + result = {} + node = result + current = self + while True: + code = { + 'co_filename': current.tb_frame.f_code.co_filename, + 'co_name': current.tb_frame.f_code.co_name, + } + frame = { + 'f_globals': current.tb_frame.f_globals, + 'f_locals': current.tb_frame.f_locals, + 'f_code': code, + 'f_lineno': current.tb_frame.f_lineno, + } + node.update(tb_frame=frame, tb_lineno=current.tb_lineno, tb_next=None) + current = current.tb_next + if current is None: + return result + node['tb_next'] = {} + node = node['tb_next'] to_dict = as_dict @@ -186,26 +186,28 @@ def from_dict(cls, dct): """ Creates an instance from a dictionary with the same structure as ``.as_dict()`` returns. """ - if dct['tb_next']: - tb_next = cls.from_dict(dct['tb_next']) - else: - tb_next = None + nodes = [dct] + while dct['tb_next']: + dct = dct['tb_next'] + nodes.append(dct) - code = _AttrDict( - co_filename=dct['tb_frame']['f_code']['co_filename'], - co_name=dct['tb_frame']['f_code']['co_name'], - ) - frame = _AttrDict( - f_globals=dct['tb_frame']['f_globals'], - f_locals=dct['tb_frame'].get('f_locals', {}), - f_code=code, - f_lineno=dct['tb_frame']['f_lineno'], - ) - tb = _AttrDict( - tb_frame=frame, - tb_lineno=dct['tb_lineno'], - tb_next=tb_next, - ) + tb = None + for dct in reversed(nodes): + code = _AttrDict( + co_filename=dct['tb_frame']['f_code']['co_filename'], + co_name=dct['tb_frame']['f_code']['co_name'], + ) + frame = _AttrDict( + f_globals=dct['tb_frame']['f_globals'], + f_locals=dct['tb_frame'].get('f_locals', {}), + f_code=code, + f_lineno=dct['tb_frame']['f_lineno'], + ) + tb = _AttrDict( + tb_frame=frame, + tb_lineno=dct['tb_lineno'], + tb_next=tb, + ) return cls(tb, get_locals=get_all_locals) @classmethod diff --git a/tests/test_deep_dicts.py b/tests/test_deep_dicts.py new file mode 100644 index 0000000..32fabdd --- /dev/null +++ b/tests/test_deep_dicts.py @@ -0,0 +1,43 @@ +import sys +from types import TracebackType + +from tblib import Traceback + + +def test_deep_traceback_as_dict(): + depth = sys.getrecursionlimit() + 10 + frame = sys._getframe() + native = None + for lineno in range(depth, 0, -1): + native = TracebackType(native, frame, -1, lineno) + converted = Traceback(native).as_dict() + for lineno in range(1, depth + 1): + assert converted['tb_lineno'] == lineno + assert converted['tb_frame']['f_code']['co_name'] == 'test_deep_traceback_as_dict' + converted = converted['tb_next'] + assert converted is None + + +def test_deep_traceback_from_dict(): + depth = sys.getrecursionlimit() + 10 + serialized = None + for lineno in range(depth, 0, -1): + serialized = { + 'tb_frame': { + 'f_globals': {'__name__': 'deep_example', '__file__': 'deep.py'}, + 'f_locals': {'value': lineno}, + 'f_code': {'co_filename': 'deep.py', 'co_name': 'recursive'}, + 'f_lineno': lineno, + }, + 'tb_lineno': lineno, + 'tb_next': serialized, + } + converted = Traceback.from_dict(serialized) + for lineno in range(1, depth + 1): + assert converted.tb_lineno == lineno + assert converted.tb_frame.f_lineno == lineno + assert converted.tb_frame.f_locals == {'value': lineno} + assert converted.tb_frame.f_globals == {'__name__': 'deep_example', '__file__': 'deep.py'} + assert converted.tb_frame.f_code.co_name == 'recursive' + converted = converted.tb_next + assert converted is None