Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions src/tblib/pickling_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__())

Expand Down
33 changes: 33 additions & 0 deletions tests/test_pickle_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -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