From 74efba3cec96f4d0637057ba1a716def16e2160d Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 16 Oct 2026 19:35:48 -0400 Subject: [PATCH 1/4] docs(foundation): restore Extractor._vfunc's annotation (#1839) - Uncomment the `_vfunc` annotation under its `#:` comment, so Sphinx attaches the description and the type the extraction page asks for. Typed `Callable[[Extractor, Any], Any]` rather than `VerboseHandler`, whose `Packet` union makes mypy reject the default and engine callbacks; mypy output on the extractor and the engines is unchanged. - Add a test that the comment attaches, read the way Sphinx's parser reads it, plus a cross-check against Sphinx itself where it is installed. Closes #1839 --- pcapkit/foundation/extraction.py | 5 +- ...t_extractor_vfunc_doc_comment_1839_unit.py | 124 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/project/test_extractor_vfunc_doc_comment_1839_unit.py diff --git a/pcapkit/foundation/extraction.py b/pcapkit/foundation/extraction.py index dd4acb10b..a75df7af2 100644 --- a/pcapkit/foundation/extraction.py +++ b/pcapkit/foundation/extraction.py @@ -177,8 +177,11 @@ class Extractor(Generic[_P]): #: the file object after extraction. _flag_s: 'bool' + # NOTE: not ``VerboseHandler``, whose frame parameter is the whole + # ``Packet`` union: mypy would then reject the default and the engines' + # callbacks, each of which handles only its own packet type (#1839). #: Verbose callback function. - #_vfunc: 'VerboseHandler' + _vfunc: 'Callable[[Extractor, Any], Any]' #: Frame number. _frnum: 'int' diff --git a/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py b/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py new file mode 100644 index 000000000..ad1c70b30 --- /dev/null +++ b/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +"""Pins the documentation comment on :attr:`Extractor._vfunc`. + +GitHub issue #1839: :file:`docs/source/pcapkit/foundation/extraction.rst` asks +for ``.. autoattribute:: _vfunc``, but the annotation under its ``#:`` comment +had been commented out. Sphinx attaches a ``#:`` comment only to the assignment +or annotation directly below it, so the page rendered a bare name with no type +and no description. + +The first test reads the comment the way :mod:`sphinx.pycode.parser` does and +runs everywhere; it reads source text and never imports the package. The second +asks Sphinx itself, and skips without it. + +""" + +from __future__ import annotations + +import ast +import pathlib +import re +import unittest +from typing import TYPE_CHECKING + +from tests.project.test_docs_ambiguous_xrefs_1775_unit import HAS_SPHINX + +if TYPE_CHECKING: + from typing import Iterable, Iterator, Optional + +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The module that declares the attribute, by name and by path. +MODULE = 'pcapkit.foundation.extraction' +SOURCE = ROOT / 'pcapkit' / 'foundation' / 'extraction.py' + +#: The documented attribute, and the description its page should render. +CLASS = 'Extractor' +ATTR = '_vfunc' +TEXT = 'Verbose callback function.' + +#: A documentation comment line, as :mod:`sphinx.pycode.parser` matches it. +COMMENT = re.compile(r'^\s*#: ?(.*)$') + + +def class_statements(body: 'Iterable[ast.stmt]') -> 'Iterator[ast.stmt]': + """Yield every statement of a class body, including those nested in a block. + + An ``if TYPE_CHECKING:`` block in a class body declares class attributes, + so the walk goes into compound statements; it stops at a function or class, + which opens a new scope. + + """ + for stmt in body: + if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + continue + yield stmt + for field in ('body', 'orelse', 'finalbody'): + yield from class_statements(getattr(stmt, field, [])) + for handler in getattr(stmt, 'handlers', []): + yield from class_statements(handler.body) + + +def doc_comment(source: 'str', cls: 'str', attr: 'str') -> 'Optional[list[str]]': + """Return the ``#:`` lines Sphinx would attach to ``cls.attr``. + + These are the consecutive ``#:`` lines directly above the first assignment + or annotation of ``attr`` in the body of ``cls``. The result is :obj:`None` + when the class body neither assigns nor annotates ``attr``. + + """ + lines = source.splitlines() + classes = [node for node in ast.walk(ast.parse(source)) + if isinstance(node, ast.ClassDef) and node.name == cls] + if not classes: + raise AssertionError(f'no class {cls!r} in {SOURCE}') + + for stmt in class_statements(classes[0].body): + if isinstance(stmt, ast.AnnAssign): + targets = [stmt.target] # type: list[ast.expr] + elif isinstance(stmt, ast.Assign): + targets = stmt.targets + else: + continue + if not any(isinstance(target, ast.Name) and target.id == attr for target in targets): + continue + + found = [] # type: list[str] + for line in reversed(lines[:stmt.lineno - 1]): + match = COMMENT.match(line) + if match is None: + break + found.append(match.group(1)) + return found[::-1] + return None + + +class VerboseCallbackDocCommentTests(unittest.TestCase): + """The ``#:`` comment above :attr:`Extractor._vfunc` attaches to it.""" + + source = '' + + @classmethod + def setUpClass(cls) -> 'None': + cls.source = SOURCE.read_text(encoding='utf-8') + + def test_comment_sits_directly_above_the_annotation(self) -> 'None': + comment = doc_comment(self.source, CLASS, ATTR) + self.assertIsNotNone(comment, f'{CLASS} neither annotates nor assigns {ATTR}') + self.assertIn(TEXT, ' '.join(comment or [])) + + @unittest.skipUnless(HAS_SPHINX, 'Sphinx and sphinx-autodoc-typehints are not installed') + def test_sphinx_attaches_the_comment(self) -> 'None': + from sphinx.pycode import ModuleAnalyzer # pylint: disable=import-outside-toplevel + + docs = ModuleAnalyzer.for_string(self.source, MODULE).find_attr_docs() + lines = docs.get((CLASS, ATTR)) + self.assertIsNotNone(lines, f'Sphinx attaches no comment to {CLASS}.{ATTR}') + found = '\n'.join(lines or []).strip() + self.assertIn(TEXT, found) + # The first test reads the comment the same way, so it is measuring this. + self.assertEqual('\n'.join(doc_comment(self.source, CLASS, ATTR) or []).strip(), found) + + +if __name__ == '__main__': + unittest.main() From e7d2465dd68e6af1a19707fd9c958510cfb0fd11 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 16 Oct 2026 20:12:29 -0400 Subject: [PATCH 2/4] docs(foundation): give _vfunc and VerboseHandler a precise signature (#1839) - Type `VerboseHandler` as `Callable[['Extractor', Packet], object]` and annotate `_vfunc` with it. The return is `object`, not `None`: every caller discards it, and `None` would make mypy reject a user callback that returns a value. - Narrow the frame to the engine's own packet type with `cast` inside the default callback and each engine's callback, so they accept the alias's `Packet` union. Runtime output is unchanged. - Pin the annotation and the alias's signature in the #1839 test. Refs #1839 --- pcapkit/foundation/engines/dpkt.py | 2 +- pcapkit/foundation/engines/pcap_ct.py | 2 +- pcapkit/foundation/engines/pypcap.py | 2 +- pcapkit/foundation/engines/pypcapfile.py | 2 +- pcapkit/foundation/engines/pyshark.py | 2 +- pcapkit/foundation/engines/scapy.py | 2 +- pcapkit/foundation/extraction.py | 15 +-- ...t_extractor_vfunc_doc_comment_1839_unit.py | 119 ++++++++++++------ 8 files changed, 98 insertions(+), 48 deletions(-) diff --git a/pcapkit/foundation/engines/dpkt.py b/pcapkit/foundation/engines/dpkt.py index 508f5bdae..37edce47e 100644 --- a/pcapkit/foundation/engines/dpkt.py +++ b/pcapkit/foundation/engines/dpkt.py @@ -458,7 +458,7 @@ def run(self) -> 'None': if ext._flag_v: from pcapkit.toolkit.dpkt import packet2chain # isort:skip ext._vfunc = lambda e, f: print( - f'Frame {e._frnum:>3d}: {packet2chain(f)}' # pylint: disable=protected-access + f'Frame {e._frnum:>3d}: {packet2chain(cast("DPKTPacket", f))}' # pylint: disable=protected-access ) # extract global header diff --git a/pcapkit/foundation/engines/pcap_ct.py b/pcapkit/foundation/engines/pcap_ct.py index 51f42ef19..5e1f81df3 100644 --- a/pcapkit/foundation/engines/pcap_ct.py +++ b/pcapkit/foundation/engines/pcap_ct.py @@ -403,7 +403,7 @@ def run(self) -> 'None': if ext._flag_v: from pcapkit.toolkit.pcap_ct import packet2chain # isort:skip ext._vfunc = lambda e, f: print( - f'Frame {e._frnum:>3d}: {packet2chain(f[1], data_link=self._dlink)}' # pylint: disable=protected-access + f'Frame {e._frnum:>3d}: {packet2chain(cast("RawFrame", f)[1], data_link=self._dlink)}' # pylint: disable=protected-access ) # pylint: disable=logging-fstring-interpolation # extract & analyse file diff --git a/pcapkit/foundation/engines/pypcap.py b/pcapkit/foundation/engines/pypcap.py index 2121d9782..6eebc29fa 100644 --- a/pcapkit/foundation/engines/pypcap.py +++ b/pcapkit/foundation/engines/pypcap.py @@ -316,7 +316,7 @@ def run(self) -> 'None': if ext._flag_v: from pcapkit.toolkit.pypcap import packet2chain # isort:skip ext._vfunc = lambda e, f: print( - f'Frame {e._frnum:>3d}: {packet2chain(f[1], data_link=self._dlink)}' # pylint: disable=protected-access + f'Frame {e._frnum:>3d}: {packet2chain(cast("RawFrame", f)[1], data_link=self._dlink)}' # pylint: disable=protected-access ) # pylint: disable=logging-fstring-interpolation # extract & analyse file diff --git a/pcapkit/foundation/engines/pypcapfile.py b/pcapkit/foundation/engines/pypcapfile.py index 42a28a6f7..75c5a118e 100644 --- a/pcapkit/foundation/engines/pypcapfile.py +++ b/pcapkit/foundation/engines/pypcapfile.py @@ -224,7 +224,7 @@ def run(self) -> 'None': if ext._flag_v: from pcapkit.toolkit.pypcapfile import packet2chain # isort:skip ext._vfunc = lambda e, f: print( - f'Frame {e._frnum:>3d}: {packet2chain(f, data_link=self._dlink)}' # pylint: disable=protected-access + f'Frame {e._frnum:>3d}: {packet2chain(cast("PCAPFilePacket", f), data_link=self._dlink)}' # pylint: disable=protected-access ) # pylint: disable=logging-fstring-interpolation # extract & analyse file diff --git a/pcapkit/foundation/engines/pyshark.py b/pcapkit/foundation/engines/pyshark.py index 2113ce1ef..7a3dbbb31 100644 --- a/pcapkit/foundation/engines/pyshark.py +++ b/pcapkit/foundation/engines/pyshark.py @@ -259,7 +259,7 @@ def run(self) -> 'None': if ext._flag_v: ext._vfunc = lambda e, f: print( f'Frame {e._frnum:>3d}: ' # pylint: disable=protected-access - f'{cast("BaseLayer", _frame_layer(f)).protocols}' + f'{cast("BaseLayer", _frame_layer(cast("PySharkPacket", f))).protocols}' ) # extract global header diff --git a/pcapkit/foundation/engines/scapy.py b/pcapkit/foundation/engines/scapy.py index 4509cc30b..0f406757c 100644 --- a/pcapkit/foundation/engines/scapy.py +++ b/pcapkit/foundation/engines/scapy.py @@ -387,7 +387,7 @@ def run(self) -> 'None': if ext._flag_v: from pcapkit.toolkit.scapy import packet2chain # isort:skip ext._vfunc = lambda e, f: print( - f'Frame {e._frnum:>3d}: {packet2chain(f)}' # pylint: disable=protected-access + f'Frame {e._frnum:>3d}: {packet2chain(cast("ScapyPacket", f))}' # pylint: disable=protected-access ) # extract global header diff --git a/pcapkit/foundation/extraction.py b/pcapkit/foundation/extraction.py index a75df7af2..c4d4a820c 100644 --- a/pcapkit/foundation/extraction.py +++ b/pcapkit/foundation/extraction.py @@ -85,7 +85,9 @@ PCAPFilePacket, tuple[float, bytes]] Protocols = Union[str, ProtocolBase, Type[ProtocolBase]] - VerboseHandler = Callable[['Extractor', Packet], Any] + # NOTE: every caller discards the callback's result. ``object`` rather than + # ``None`` still accepts one that returns something, e.g. ``sys.stdout.write``. + VerboseHandler = Callable[['Extractor', Packet], object] __all__ = ['Extractor'] @@ -177,11 +179,8 @@ class Extractor(Generic[_P]): #: the file object after extraction. _flag_s: 'bool' - # NOTE: not ``VerboseHandler``, whose frame parameter is the whole - # ``Packet`` union: mypy would then reject the default and the engines' - # callbacks, each of which handles only its own packet type (#1839). #: Verbose callback function. - _vfunc: 'Callable[[Extractor, Any], Any]' + _vfunc: 'VerboseHandler' #: Frame number. _frnum: 'int' @@ -1287,9 +1286,11 @@ def __init__(self, # user-facing output on stdout, not diagnostics -- the frame # chains are a feature of the tool, so they stay on ``print`` # rather than becoming log records a consumer has to configure - # a handler to see (and on a different stream at that). + # a handler to see (and on a different stream at that). Of the + # built-in engines only PCAP and PCAPNG keep this callback, so + # its frame is theirs; the others install their own. self._vfunc = lambda e, f: print( - f'Frame {e._frnum:>3d}: {f.protochain}' # pylint: disable=protected-access + f'Frame {e._frnum:>3d}: {cast("Frame | PCAPNG", f).protochain}' # pylint: disable=protected-access ) else: self._vfunc = lambda e, f: None diff --git a/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py b/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py index ad1c70b30..4c7e14fdf 100644 --- a/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py +++ b/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Pins the documentation comment on :attr:`Extractor._vfunc`. +"""Pins the documentation comment and the type of :attr:`Extractor._vfunc`. GitHub issue #1839: :file:`docs/source/pcapkit/foundation/extraction.rst` asks for ``.. autoattribute:: _vfunc``, but the annotation under its ``#:`` comment @@ -7,9 +7,12 @@ or annotation directly below it, so the page rendered a bare name with no type and no description. -The first test reads the comment the way :mod:`sphinx.pycode.parser` does and -runs everywhere; it reads source text and never imports the package. The second -asks Sphinx itself, and skips without it. +The annotation is the ``VerboseHandler`` alias, whose signature is pinned too: +the frame parameter is the whole ``Packet`` union, and the return is +:class:`object`, because every caller discards it. + +Every test reads source text and never imports the package. The one that asks +Sphinx itself skips without it; the others run everywhere. """ @@ -24,7 +27,9 @@ from tests.project.test_docs_ambiguous_xrefs_1775_unit import HAS_SPHINX if TYPE_CHECKING: - from typing import Iterable, Iterator, Optional + from typing import Iterable, Iterator, Optional, Union + + Binding = Union[ast.Assign, ast.AnnAssign] ROOT = pathlib.Path(__file__).resolve().parents[2] @@ -37,16 +42,20 @@ ATTR = '_vfunc' TEXT = 'Verbose callback function.' +#: The alias that types the attribute, and the signature it should spell. +ALIAS = 'VerboseHandler' +SIGNATURE = "Callable[['Extractor', Packet], object]" + #: A documentation comment line, as :mod:`sphinx.pycode.parser` matches it. COMMENT = re.compile(r'^\s*#: ?(.*)$') -def class_statements(body: 'Iterable[ast.stmt]') -> 'Iterator[ast.stmt]': - """Yield every statement of a class body, including those nested in a block. +def scope_statements(body: 'Iterable[ast.stmt]') -> 'Iterator[ast.stmt]': + """Yield every statement of a module or class body, including nested blocks. - An ``if TYPE_CHECKING:`` block in a class body declares class attributes, - so the walk goes into compound statements; it stops at a function or class, - which opens a new scope. + An ``if TYPE_CHECKING:`` block declares names of the scope it sits in, so the + walk goes into compound statements; it stops at a function or class, which + opens a new scope. """ for stmt in body: @@ -54,9 +63,33 @@ def class_statements(body: 'Iterable[ast.stmt]') -> 'Iterator[ast.stmt]': continue yield stmt for field in ('body', 'orelse', 'finalbody'): - yield from class_statements(getattr(stmt, field, [])) + yield from scope_statements(getattr(stmt, field, [])) for handler in getattr(stmt, 'handlers', []): - yield from class_statements(handler.body) + yield from scope_statements(handler.body) + + +def bindings(body: 'Iterable[ast.stmt]', name: 'str') -> 'list[Binding]': + """Return every assignment or annotation of ``name`` in a scope, in order.""" + found = [] # type: list[Binding] + for stmt in scope_statements(body): + if isinstance(stmt, ast.AnnAssign): + targets = [stmt.target] # type: list[ast.expr] + elif isinstance(stmt, ast.Assign): + targets = stmt.targets + else: + continue + if any(isinstance(target, ast.Name) and target.id == name for target in targets): + found.append(stmt) + return found + + +def class_body(tree: 'ast.Module', cls: 'str') -> 'list[ast.stmt]': + """Return the body of the class ``cls`` defined anywhere in ``tree``.""" + classes = [node for node in ast.walk(tree) + if isinstance(node, ast.ClassDef) and node.name == cls] + if not classes: + raise AssertionError(f'no class {cls!r} in {SOURCE}') + return classes[0].body def doc_comment(source: 'str', cls: 'str', attr: 'str') -> 'Optional[list[str]]': @@ -67,30 +100,17 @@ def doc_comment(source: 'str', cls: 'str', attr: 'str') -> 'Optional[list[str]]' when the class body neither assigns nor annotates ``attr``. """ - lines = source.splitlines() - classes = [node for node in ast.walk(ast.parse(source)) - if isinstance(node, ast.ClassDef) and node.name == cls] - if not classes: - raise AssertionError(f'no class {cls!r} in {SOURCE}') - - for stmt in class_statements(classes[0].body): - if isinstance(stmt, ast.AnnAssign): - targets = [stmt.target] # type: list[ast.expr] - elif isinstance(stmt, ast.Assign): - targets = stmt.targets - else: - continue - if not any(isinstance(target, ast.Name) and target.id == attr for target in targets): - continue + found = bindings(class_body(ast.parse(source), cls), attr) + if not found: + return None - found = [] # type: list[str] - for line in reversed(lines[:stmt.lineno - 1]): - match = COMMENT.match(line) - if match is None: - break - found.append(match.group(1)) - return found[::-1] - return None + lines = [] # type: list[str] + for line in reversed(source.splitlines()[:found[0].lineno - 1]): + match = COMMENT.match(line) + if match is None: + break + lines.append(match.group(1)) + return lines[::-1] class VerboseCallbackDocCommentTests(unittest.TestCase): @@ -120,5 +140,34 @@ def test_sphinx_attaches_the_comment(self) -> 'None': self.assertEqual('\n'.join(doc_comment(self.source, CLASS, ATTR) or []).strip(), found) +class VerboseHandlerSignatureTests(unittest.TestCase): + """:attr:`Extractor._vfunc` is typed by the precise ``VerboseHandler`` alias.""" + + source = '' + tree = ast.parse('') + + @classmethod + def setUpClass(cls) -> 'None': + cls.source = SOURCE.read_text(encoding='utf-8') + cls.tree = ast.parse(cls.source) + + def test_attribute_is_annotated_with_the_alias(self) -> 'None': + annotations = [ast.literal_eval(stmt.annotation) + for stmt in bindings(class_body(self.tree, CLASS), ATTR) + if isinstance(stmt, ast.AnnAssign)] + self.assertEqual(annotations, [ALIAS]) + + def test_alias_spells_the_precise_signature(self) -> 'None': + found = bindings(self.tree.body, ALIAS) + self.assertEqual(len(found), 1, f'{ALIAS} is not bound exactly once in {SOURCE}') + value = found[0].value + if value is None: + self.fail(f'{ALIAS} is annotated but never assigned') + # Compared as trees, so spacing and quoting do not matter. + expected = ast.parse(SIGNATURE, mode='eval').body + self.assertEqual(ast.dump(value), ast.dump(expected), + f'{ALIAS} = {ast.get_source_segment(self.source, value)}') + + if __name__ == '__main__': unittest.main() From 817208c8a380cfb6c88aa6c23d3ca5a7d52e5abd Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 16 Oct 2026 20:24:57 -0400 Subject: [PATCH 3/4] docs(foundation): correct the VerboseHandler note's example (#1839) - Replace `sys.stdout.write`, which takes one argument and so is no verbose callback at all, with `lambda e, f: sys.stdout.write(str(f))`: it takes the extractor and the frame and returns a value, so `object` accepts it where `None` would not. Refs #1839 --- pcapkit/foundation/extraction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pcapkit/foundation/extraction.py b/pcapkit/foundation/extraction.py index c4d4a820c..32963961f 100644 --- a/pcapkit/foundation/extraction.py +++ b/pcapkit/foundation/extraction.py @@ -86,7 +86,8 @@ Protocols = Union[str, ProtocolBase, Type[ProtocolBase]] # NOTE: every caller discards the callback's result. ``object`` rather than - # ``None`` still accepts one that returns something, e.g. ``sys.stdout.write``. + # ``None`` still accepts one that returns something, e.g. + # ``lambda e, f: sys.stdout.write(str(f))``. VerboseHandler = Callable[['Extractor', Packet], object] __all__ = ['Extractor'] From 5b0e9fdbe0da48b99bbc87e0ff11c433aaca6d0a Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 16 Oct 2026 22:05:13 -0400 Subject: [PATCH 4/4] docs(foundation): type the verbose callback's result as Any (#1839) - Type `VerboseHandler`'s return as `Any` rather than `object`, per the owner's preference on #1867: every caller discards the result, and `Any` accepts any two-argument callback, whatever it returns. - Rewrite the NOTE above the alias to give that reason. - Pin `Any` in the signature test; the exact comparison still fails on a return of `None` or `object`. Refs #1839 --- pcapkit/foundation/extraction.py | 6 +++--- tests/project/test_extractor_vfunc_doc_comment_1839_unit.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pcapkit/foundation/extraction.py b/pcapkit/foundation/extraction.py index 32963961f..b7feaf080 100644 --- a/pcapkit/foundation/extraction.py +++ b/pcapkit/foundation/extraction.py @@ -85,10 +85,10 @@ PCAPFilePacket, tuple[float, bytes]] Protocols = Union[str, ProtocolBase, Type[ProtocolBase]] - # NOTE: every caller discards the callback's result. ``object`` rather than - # ``None`` still accepts one that returns something, e.g. + # NOTE: every caller discards the callback's result, so it is typed ``Any``: + # any two-argument callback is accepted, whatever it returns, e.g. # ``lambda e, f: sys.stdout.write(str(f))``. - VerboseHandler = Callable[['Extractor', Packet], object] + VerboseHandler = Callable[['Extractor', Packet], Any] __all__ = ['Extractor'] diff --git a/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py b/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py index 4c7e14fdf..a3c598d3c 100644 --- a/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py +++ b/tests/project/test_extractor_vfunc_doc_comment_1839_unit.py @@ -9,7 +9,8 @@ The annotation is the ``VerboseHandler`` alias, whose signature is pinned too: the frame parameter is the whole ``Packet`` union, and the return is -:class:`object`, because every caller discards it. +:data:`~typing.Any`, because every caller discards it. The comparison is exact, +so it fails on a return of ``None`` or :class:`object` as well. Every test reads source text and never imports the package. The one that asks Sphinx itself skips without it; the others run everywhere. @@ -44,7 +45,7 @@ #: The alias that types the attribute, and the signature it should spell. ALIAS = 'VerboseHandler' -SIGNATURE = "Callable[['Extractor', Packet], object]" +SIGNATURE = "Callable[['Extractor', Packet], Any]" #: A documentation comment line, as :mod:`sphinx.pycode.parser` matches it. COMMENT = re.compile(r'^\s*#: ?(.*)$')