diff --git a/AUTHORS.rst b/AUTHORS.rst index 6d7199a..7646b31 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -17,3 +17,5 @@ Authors * Oldřich Jedlička - https://github.com/oldium * Colin Watson - https://github.com/cjwatson * Jacob Tomlinson - https://github.com/jacobtomlinson + +* Shahadat Islam Koyel <179203101+ShahadatTest@users.noreply.github.com> diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed95b83..fca21bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +Unreleased +~~~~~~~~~~ + +* Visit exception subclasses only once across multiple-inheritance paths when installing pickle support. + 3.2.2 (2025-11-12) ~~~~~~~~~~~~~~~~~~ diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index 2f57449..d48fa35 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -122,8 +122,12 @@ def pickle_exception( def _get_subclasses(cls): # Depth-first traversal of all direct and indirect subclasses of cls to_visit = [cls] + seen = set() while to_visit: this = to_visit.pop() + if this in seen: + continue + seen.add(this) yield this to_visit += list(this.__subclasses__()) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 0990d9d..d74c4de 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -543,3 +543,36 @@ def test_exception_group(): assert isinstance(exc.exceptions[8], OSError) assert exc.exceptions[8].errno == 2 assert str(exc.exceptions[8]) == real_oserror_str + + +def test_exception_subclass_diamonds_are_visited_once(): + class RootError(Exception): + pass + + left = type('LeftError', (RootError,), {}) + right = type('RightError', (RootError,), {}) + leaf = type('LeafError', (left, right), {}) + descendants = list(tblib.pickling_support._get_subclasses(RootError)) + assert len(descendants) == 4 + assert set(descendants) == {RootError, left, right, leaf} + + +def test_global_install_registers_each_exception_type_once(monkeypatch, clear_dispatch_table): + class RootError(Exception): + pass + + left = type('LeftError', (RootError,), {}) + right = type('RightError', (RootError,), {}) + leaf = type('LeafError', (left, right), {}) + registrations = [] + original_pickle = copyreg.pickle + + def record(cls, reducer): + registrations.append(cls) + original_pickle(cls, reducer) + + monkeypatch.setattr(copyreg, 'pickle', record) + tblib.pickling_support.install() + for cls in (RootError, left, right, leaf): + assert registrations.count(cls) == 1 + assert copyreg.dispatch_table[cls] is tblib.pickling_support.pickle_exception