|
4 | 4 | # list see the documentation: |
5 | 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html |
6 | 6 |
|
| 7 | +import builtins |
7 | 8 | import datetime |
| 9 | +import enum |
8 | 10 | import importlib |
9 | 11 | import logging |
10 | 12 | import os |
11 | 13 | import pkgutil |
12 | 14 | from typing import TYPE_CHECKING |
13 | 15 |
|
| 16 | +from sphinx import addnodes |
| 17 | + |
14 | 18 | # NB: a private module of ``sphinx-autodoc-typehints``, used deliberately -- see |
15 | 19 | # ``bind_type_checking_names`` below for what it buys and why reimplementing it |
16 | 20 | # here would be worse. If a future release moves it the import fails loudly at |
|
28 | 32 | import pcapkit |
29 | 33 |
|
30 | 34 | if TYPE_CHECKING: |
31 | | - from typing import Any, Dict, Optional |
| 35 | + from typing import Any, Dict, List, Optional |
| 36 | + from docutils.nodes import document |
32 | 37 | from sphinx.application import Sphinx |
33 | 38 |
|
34 | 39 | logger = logging.getLogger('pcapkit-sphinx') |
@@ -393,6 +398,124 @@ def claim_attribute_signature(app: 'Sphinx', what: str, name: str, # pylint: di |
393 | 398 | return None |
394 | 399 |
|
395 | 400 |
|
| 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 | + |
396 | 519 | # NB: four handlers used to sit here -- ``remove_module_docstring``, |
397 | 520 | # ``process_docstring``, ``process_fields`` and ``source_read`` -- each with its |
398 | 521 | # ``app.connect`` call commented out in ``setup`` below, so none of them had run |
@@ -422,3 +545,8 @@ def setup(app: 'Sphinx') -> None: |
422 | 545 | # would otherwise win the tie on registration order -- conf.py's ``setup`` runs |
423 | 546 | # after the extensions in ``extensions`` have been set up. |
424 | 547 | 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) |
0 commit comments