Skip to content

Commit 02161e2

Browse files
authored
fix(dumpkit): call super() without type(self) in make_dumper's object_hook (#2144) (#2146)
1 parent da7f03a commit 02161e2

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

‎pcapkit/dumpkit/common.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ def object_hook(self, o: 'Any') -> 'Any':
597597
else:
598598
result = render_enum(o)
599599
else:
600-
result = super(type(self), self).object_hook(o)
600+
result = super().object_hook(o)
601601

602602
if escape_strings and isinstance(result, str):
603603
# NOTE: XML 1.0 has no spelling at all for some characters, not
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# -*- coding: utf-8 -*-
2+
"""GitHub issue #2144: ``make_dumper``'s ``object_hook`` recursed in a subclass.
3+
4+
The fallback branch of the ``object_hook`` that
5+
:func:`~pcapkit.dumpkit.common.make_dumper` builds called
6+
``super(type(self), self).object_hook(o)``. In a subclass of the class it
7+
returns, ``type(self)`` is the subclass, so the call resolved back to that same
8+
``object_hook`` and recursed until :exc:`RecursionError`. A value no branch
9+
recognises now falls through to the output class's own ``object_hook``.
10+
11+
:mod:`pcapkit` is imported inside each test, after
12+
:func:`~tests._support.reimport_once_per_class`.
13+
14+
"""
15+
16+
import importlib.util
17+
import unittest
18+
19+
from tests._support import reimport_once_per_class
20+
21+
RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper')
22+
HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS)
23+
24+
#: Every output class the extractor and the flow tracers hand ``make_dumper``,
25+
#: as ``(module, name)``.
26+
OUTPUTS = (
27+
('dictdumper', 'JSON'),
28+
('dictdumper', 'PLIST'),
29+
('dictdumper', 'Tree'),
30+
('pcapkit.dumpkit', 'PCAPIO'),
31+
('pcapkit.dumpkit', 'NotImplementedIO'),
32+
)
33+
34+
35+
class Unhandled:
36+
"""A value none of the hook's branches recognises."""
37+
38+
39+
@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed')
40+
class TestMakeDumperSubclass(unittest.TestCase):
41+
"""A subclass of ``make_dumper``'s class falls through to the output class."""
42+
43+
def setUp(self) -> None:
44+
reimport_once_per_class(self)
45+
46+
def outputs(self) -> 'list[tuple[str, type]]':
47+
"""The output classes of :data:`OUTPUTS`, imported."""
48+
return [(name, getattr(importlib.import_module(module), name)) for module, name in OUTPUTS]
49+
50+
@staticmethod
51+
def hook(cls: 'type', value: 'object') -> 'object':
52+
"""Call ``cls``'s ``object_hook`` on ``value``.
53+
54+
The instance is made without ``__init__``, which opens the output file,
55+
since the hook reads no state from it. :class:`dictdumper.dumper.Dumper`'s
56+
``__new__`` takes the file name, and ignores it.
57+
58+
"""
59+
return cls.__new__(cls, 'unused').object_hook(value)
60+
61+
def test_subclass_falls_through(self) -> None:
62+
"""A direct subclass hands an unhandled value to the output class."""
63+
from pcapkit.dumpkit.common import make_dumper
64+
65+
for name, output in self.outputs():
66+
with self.subTest(output=name):
67+
class Sub(make_dumper(output)): # type: ignore[misc,valid-type]
68+
pass
69+
70+
value = Unhandled()
71+
self.assertIs(self.hook(Sub, value), value)
72+
73+
def test_nested_subclass_falls_through(self) -> None:
74+
"""So does a subclass of that subclass."""
75+
from pcapkit.dumpkit.common import make_dumper
76+
77+
for name, output in self.outputs():
78+
with self.subTest(output=name):
79+
class Sub(make_dumper(output)): # type: ignore[misc,valid-type]
80+
pass
81+
82+
class SubSub(Sub):
83+
pass
84+
85+
value = Unhandled()
86+
self.assertIs(self.hook(SubSub, value), value)
87+
88+
def test_subclass_override_reaches_the_hook(self) -> None:
89+
"""A subclass's own ``object_hook`` can defer to the built one."""
90+
from pcapkit.dumpkit.common import make_dumper
91+
92+
for name, output in self.outputs():
93+
with self.subTest(output=name):
94+
class Sub(make_dumper(output)): # type: ignore[misc,valid-type]
95+
def object_hook(self, o: 'object') -> 'object':
96+
return ('sub', super().object_hook(o))
97+
98+
value = Unhandled()
99+
self.assertEqual(self.hook(Sub, value), ('sub', value))
100+
101+
def test_unsubclassed_class_unchanged(self) -> None:
102+
"""The class ``make_dumper`` returns still falls through as before."""
103+
from pcapkit.dumpkit.common import make_dumper
104+
105+
for name, output in self.outputs():
106+
with self.subTest(output=name):
107+
value = Unhandled()
108+
self.assertIs(self.hook(make_dumper(output), value), value)
109+
110+
111+
if __name__ == '__main__':
112+
unittest.main()

0 commit comments

Comments
 (0)