|
| 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