Skip to content

Commit ae82e6d

Browse files
authored
docs(conf): resolve the 36 ambiguous cross-references at their source (#1790)
- conf.py: resolve_builtins_exactly drops the fuzzy-search flag from an implicit reference to a builtin name, so `dict`, `type` and `set` in a type reach the Python docs instead of the registry members TCP.dict, Command.type and TCP.set (25 warnings). - conf.py: save/restore_enum_member_docstring undo napoleon's inline `type: description` split on enum members, which turned a URL's colon into a bogus Type field on 51 registry members (the 4 `http`/`Echo` warnings). - docs: 44 `.. data::`/`.. function::` directives repeated their module in the name and registered as `<module>.<module>.<name>`; they take the bare name now, with `:module:` where it differs. Two `:type:` options qualify `_AT` (the 7 TypeVar warnings). - tests: pin the hooks against real Sphinx nodes and napoleon, and the events and priorities setup() connects them at, behind a declared HAS_SPHINX gate, and the directive names textually. Docs build: 0 ambiguity warnings (was 36), no new warnings. Closes #1775
1 parent 376bca2 commit ae82e6d

27 files changed

Lines changed: 487 additions & 47 deletions

‎docs/source/conf.py‎

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
# list see the documentation:
55
# https://www.sphinx-doc.org/en/master/usage/configuration.html
66

7+
import builtins
78
import datetime
9+
import enum
810
import importlib
911
import logging
1012
import os
1113
import pkgutil
1214
from typing import TYPE_CHECKING
1315

16+
from sphinx import addnodes
17+
1418
# NB: a private module of ``sphinx-autodoc-typehints``, used deliberately -- see
1519
# ``bind_type_checking_names`` below for what it buys and why reimplementing it
1620
# here would be worse. If a future release moves it the import fails loudly at
@@ -28,7 +32,8 @@
2832
import pcapkit
2933

3034
if TYPE_CHECKING:
31-
from typing import Any, Dict, Optional
35+
from typing import Any, Dict, List, Optional
36+
from docutils.nodes import document
3237
from sphinx.application import Sphinx
3338

3439
logger = logging.getLogger('pcapkit-sphinx')
@@ -393,6 +398,124 @@ def claim_attribute_signature(app: 'Sphinx', what: str, name: str, # pylint: di
393398
return None
394399

395400

401+
#: Every name :mod:`builtins` binds -- ``dict``, ``set``, ``type`` and the rest.
402+
BUILTIN_NAMES = frozenset(dir(builtins))
403+
404+
405+
def resolve_builtins_exactly(app: 'Sphinx', doctree: 'document') -> None: # pylint: disable=unused-argument
406+
"""Stop a bare builtin name in a type being suffix-matched to a pcapkit member.
407+
408+
The Python domain resolves a reference carrying a ``refspecific`` attribute
409+
by *fuzzy* search when no exact match exists: any documented object whose
410+
name ends in ``.dict`` will do. Every type it builds implicitly carries that
411+
attribute -- the doc-field types (``:type:``, ``:vartype:``) set it to
412+
:data:`True`, and an annotation sets it to :data:`False`, which counts all
413+
the same, because the domain only asks whether the attribute is *present*.
414+
So ``dict`` in an annotation found the registry members
415+
:attr:`TCP.dict <pcapkit.const.reg.apptype.tcp.TCP.dict>` and
416+
``UDP.dict`` and linked to the first, with a ``more than one target found``
417+
warning; ``type`` and ``set`` went the same way. A registry growing one
418+
member named like a builtin would mislink it *silently*.
419+
420+
Dropping the attribute for a builtin name leaves only the exact lookup, by
421+
the bare name and within the current module and class. That finds nothing
422+
for ``dict``, so :mod:`~sphinx.ext.intersphinx` links it to the Python docs.
423+
424+
An explicit role is left alone: ``:class:`.dict``` asks for the fuzzy
425+
search by its leading dot, and only a role's node carries ``refwarn``.
426+
427+
Args:
428+
app: Sphinx application.
429+
doctree: The document just read.
430+
431+
"""
432+
for node in doctree.findall(addnodes.pending_xref):
433+
attributes = node.attributes
434+
if (attributes.get('refdomain') == 'py' and 'refspecific' in attributes
435+
and 'refwarn' not in attributes
436+
and attributes.get('reftarget') in BUILTIN_NAMES):
437+
del attributes['refspecific']
438+
439+
440+
#: Enum member docstrings as autodoc read them, held from
441+
#: :func:`save_enum_member_docstring` until :func:`restore_enum_member_docstring`.
442+
_ENUM_MEMBER_DOCSTRINGS = {} # type: Dict[str, List[str]]
443+
444+
445+
def is_enum_member(name: str) -> bool:
446+
"""Whether ``name`` is the fully qualified name of an enum member.
447+
448+
Decided from the name, because autodoc does not hand over the member: for
449+
``PayloadProtocolIdentifier.H_323`` Sphinx 9.1 passes the plain ``int`` 13.
450+
451+
Args:
452+
name: Fully qualified name of the object being documented.
453+
454+
Returns:
455+
:data:`True` if the name before the last dot is an enumeration and the
456+
last part is one of its members.
457+
458+
"""
459+
parent_name, _, member = name.rpartition('.')
460+
try:
461+
parent = pkgutil.resolve_name(parent_name)
462+
except (ImportError, AttributeError, ValueError):
463+
return False
464+
return isinstance(parent, enum.EnumMeta) and member in parent.__members__
465+
466+
467+
def save_enum_member_docstring(app: 'Sphinx', what: str, name: str, # pylint: disable=unused-argument
468+
obj: 'Any', options: 'Any', lines: 'List[str]') -> None: # pylint: disable=unused-argument
469+
"""Keep an enum member's docstring before :mod:`~sphinx.ext.napoleon` reads it.
470+
471+
Napoleon takes the first line of every attribute docstring as Google's
472+
inline ``type: description`` form and splits it at the first colon. A
473+
registry member's docstring is the registry's own text, so the colon is
474+
usually a URL's: ``H.323 [http://standard.pictel.com/...]`` rendered as
475+
*Type:* ``H.323 [http`` above the description ``//standard.pictel.com/...``,
476+
on 51 members. The bogus type was also parsed as a cross-reference, which is
477+
where the ``http`` and ``Echo`` ``more than one target found`` warnings came
478+
from.
479+
480+
Args:
481+
app: Sphinx application.
482+
what: Type of the object being documented.
483+
name: Fully qualified name of the object.
484+
obj: The object itself.
485+
options: Directive options.
486+
lines: The docstring, which is not changed here.
487+
488+
"""
489+
if what == 'attribute' and is_enum_member(name):
490+
_ENUM_MEMBER_DOCSTRINGS[name] = list(lines)
491+
492+
493+
def restore_enum_member_docstring(app: 'Sphinx', what: str, name: str, # pylint: disable=unused-argument
494+
obj: 'Any', options: 'Any', lines: 'List[str]') -> None: # pylint: disable=unused-argument
495+
"""Undo napoleon's inline type on an enum member, from the saved docstring.
496+
497+
An enum member's type is its enumeration, which autodoc already shows, so a
498+
``:type:`` field napoleon added there is always a misread line. Only that
499+
case is undone: a docstring napoleon rewrote in any other way keeps the
500+
rewrite.
501+
502+
Args:
503+
app: Sphinx application.
504+
what: Type of the object being documented.
505+
name: Fully qualified name of the object.
506+
obj: The object itself.
507+
options: Directive options.
508+
lines: The docstring after napoleon, restored in place.
509+
510+
"""
511+
original = _ENUM_MEMBER_DOCSTRINGS.pop(name, None)
512+
if original is None:
513+
return
514+
if (any(line.startswith(':type:') for line in lines)
515+
and not any(line.startswith(':type:') for line in original)):
516+
lines[:] = original
517+
518+
396519
# NB: four handlers used to sit here -- ``remove_module_docstring``,
397520
# ``process_docstring``, ``process_fields`` and ``source_read`` -- each with its
398521
# ``app.connect`` call commented out in ``setup`` below, so none of them had run
@@ -422,3 +545,8 @@ def setup(app: 'Sphinx') -> None:
422545
# would otherwise win the tie on registration order -- conf.py's ``setup`` runs
423546
# after the extensions in ``extensions`` have been set up.
424547
app.connect('autodoc-process-signature', claim_attribute_signature, priority=400)
548+
app.connect('doctree-read', resolve_builtins_exactly)
549+
# NB: either side of napoleon's ``autodoc-process-docstring`` handler, which
550+
# connects at the default 500.
551+
app.connect('autodoc-process-docstring', save_enum_member_docstring, priority=400)
552+
app.connect('autodoc-process-docstring', restore_enum_member_docstring, priority=600)

‎docs/source/pcapkit/corekit/context.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,5 @@ Decoding an ESP tunnel end to end:
7575
Type Variables
7676
--------------
7777

78-
.. data:: pcapkit.corekit.context._CT
78+
.. data:: _CT
7979
:type: pcapkit.corekit.context.ProtocolContext

‎docs/source/pcapkit/corekit/decorators.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ decorators, including :func:`~pcapkit.corekit.decorators.seekset`,
3131
Type Variables
3232
--------------
3333

34-
.. data:: pcapkit.corekit.decorators.R_seekset
34+
.. data:: R_seekset
3535
:type: typing.Any
3636

37-
.. data:: pcapkit.corekit.decorators.R_beholder
37+
.. data:: R_beholder
3838
:type: pcapkit.protocols.protocol.ProtocolBase
3939

40-
.. data:: pcapkit.corekit.decorators.R_prepare
40+
.. data:: R_prepare
4141
:type: pcapkit.protocols.schema.schema.Schema

‎docs/source/pcapkit/corekit/fields/collections.rst‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ Option List Fields
2020
Type Variables
2121
~~~~~~~~~~~~~~
2222

23-
.. data:: pcapkit.corekit.fields.collections._TL
23+
.. data:: _TL
2424
:type: pcapkit.protocols.schema.schema.Schema | pcapkit.corekit.fields.field.FieldBase | bytes
2525

26-
.. data:: pcapkit.corekit.fields.collections._TS
26+
.. data:: _TS
2727
:type: pcapkit.protocols.schema.schema.Schema

‎docs/source/pcapkit/corekit/fields/field.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ Internal Definitions
3636
Type Variables
3737
--------------
3838

39-
.. data:: pcapkit.corekit.fields.field._T
39+
.. data:: _T
4040
:type: typing.Any

‎docs/source/pcapkit/corekit/fields/ipaddress.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ Internal Definitions
5555
Type Variables
5656
~~~~~~~~~~~~~~
5757

58-
.. data:: pcapkit.corekit.fields.ipaddress._T
58+
.. data:: _T
5959
:type: ipaddress.IPv4Address | ipaddress.IPv6Address | ipaddress.IPv4Interface | ipaddress.IPv6Interface
6060

61-
.. data:: pcapkit.corekit.fields.ipaddress._AT
61+
.. data:: _AT
6262
:type: ipaddress.IPv4Address | ipaddress.IPv6Address
6363

64-
.. data:: pcapkit.corekit.fields.ipaddress._IT
64+
.. data:: _IT
6565
:type: ipaddress.IPv4Interface | ipaddress.IPv6Interface

‎docs/source/pcapkit/corekit/fields/misc.rst‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ Generic Data Fields
3636
Type Variables
3737
~~~~~~~~~~~~~~
3838

39-
.. data:: pcapkit.corekit.fields.misc._TC
39+
.. data:: _TC
4040
:type: typing.Any
4141

42-
.. data:: pcapkit.corekit.fields.misc._TS
42+
.. data:: _TS
4343
:type: pcapkit.protocols.schema.schema.Schema
4444

45-
.. data:: pcapkit.corekit.fields.misc._TP
45+
.. data:: _TP
4646
:type: pcapkit.protocols.protocol.Protocol
4747

48-
.. data:: pcapkit.corekit.fields.misc._TN
48+
.. data:: _TN
4949
:type: pcapkit.corekit.sentinels.NoValueType

‎docs/source/pcapkit/corekit/fields/numbers.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@ Internal Definitions
6363
Type Variables
6464
--------------
6565

66-
.. data:: pcapkit.corekit.fields.numbers._T
66+
.. data:: _T
6767
:type: int

‎docs/source/pcapkit/corekit/fields/strings.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ Internal Definitions
3535
Type Variables
3636
--------------
3737

38-
.. data:: pcapkit.corekit.fields.strings._T
38+
.. data:: _T
3939
:type: str | bytes | python:dict[str, typing.Any]

‎docs/source/pcapkit/corekit/infoclass.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ Internal Definitions
5050
Type Variables
5151
--------------
5252

53-
.. data:: pcapkit.corekit.infoclass.KT
53+
.. data:: KT
5454
:type: typing.Any
5555

56-
.. data:: pcapkit.corekit.infoclass.VT
56+
.. data:: VT
5757
:type: typing.Any
5858

59-
.. data:: pcapkit.corekit.infoclass.ST
59+
.. data:: ST
6060
:type: typing.Type[pcapkit.corekit.infoclass.Info]

0 commit comments

Comments
 (0)