Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@

from docutils import nodes
from sphinx import addnodes
from sphinx.errors import NoUri
from sphinx.ext.autodoc.preserve_defaults import DefaultValue, update_default_value
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import logging as sphinx_logging
from sphinx.util.inspect import safe_getattr, stringify_signature

# NB: private modules of ``sphinx-autodoc-typehints``, used deliberately -- see
Expand Down Expand Up @@ -178,6 +180,11 @@
# Those packages are therefore linked to their project pages instead, through
# :data:`rst_epilog` below (:issue:`1936`).

#: The label on the note in :file:`pcapkit/foundation/engines/3rdparty.rst` that
#: says which distribution installs the ``pcap`` module, which ``|pcap|_`` links
#: to (:issue:`1948`).
PCAP_MODULE_LABEL = 'pcap-module'

#: Every third-party project page the docs link to, defined once for all pages.
#: Sphinx appends this to every source file, and so to every page autodoc
#: assembles from docstrings, so a page or a docstring must not define these
Expand All @@ -189,9 +196,14 @@
#: and `Name`_ links the project by its own name. Target names are
#: case-insensitive, so `PyShark`_ and ``|pyshark|_`` share one target. A name
#: below a package, such as ``dpkt.dpkt.Packet.data``, stays a plain literal,
#: since it would link to the same project page. ``pcap`` has no substitution:
#: `PyPCAP`_ and `pcap-ct`_ both install that module, so no one project page is
#: the right target for it.
#: since it would link to the same project page.
#:
#: ``|pcap|_`` links inside these docs instead. `PyPCAP`_ and `pcap-ct`_ both
#: install that module, so no one project page is the right target for it, and
#: it links to the note labelled :data:`PCAP_MODULE_LABEL`, which says which
#: distribution is which. Its target is that label's anchor, right on the note's
#: own page; :func:`link_pcap_module` points it at the note's page from every
#: other one.
rst_epilog = """
.. |aenum| replace:: ``aenum``
.. _aenum: https://github.com/ethanfurman/aenum
Expand All @@ -206,6 +218,8 @@
.. _emoji: https://carpedm20.github.io/emoji/docs/
.. |jspcapy| replace:: ``jspcapy``
.. _jspcapy: https://github.com/JarryShaw/jspcapy
.. |pcap| replace:: ``pcap``
.. _pcap: #pcap-module
.. |pcap-ct| replace:: ``pcap-ct``
.. _pcap-ct: https://pypi.org/project/pcap-ct/
.. |pcapfile| replace:: ``pcapfile``
Expand Down Expand Up @@ -1059,6 +1073,54 @@ def run(self, **kwargs: 'Any') -> None:
container['ids'] = [id_ for id_ in container['ids'] if id_ not in inner]


def link_pcap_module(app: 'Sphinx', doctree: 'document', docname: str) -> None:
"""Point each ``|pcap|_`` at the note labelled :data:`PCAP_MODULE_LABEL`.

The target :data:`rst_epilog` gives ``|pcap|_`` is the label's bare anchor,
since a hyperlink target can only name a URI or a target on the same page,
and the note is on one page while the epilog is on all of them. The anchor
is right on the note's own page; on every other page this prefixes it with
the URI of the note's page, as the builder spells it from ``docname``
(:issue:`1948`). A :rst:role:`ref` would resolve across pages without this,
but renders its text as plain text rather than as the literal the other
``|pkg|_`` links are.

Two kinds of builder are left to themselves. One whose URI for the note's
page is itself a fragment, as singlehtml's ``#document-<docname>`` is, writes
every page into one output, where the bare anchor already resolves and a
second ``#`` would break it. One that raises :exc:`~sphinx.errors.NoUri` has
no page for the note, as latex has not when its document set leaves the note
out, so each link is replaced by its literal, as Sphinx replaces a
cross-reference with no URI.

Args:
app: Sphinx application.
doctree: The resolved doctree of ``docname``.
docname: The document being written.

"""
anchor = f'#{PCAP_MODULE_LABEL}'
refs = [ref for ref in doctree.findall(nodes.reference) if ref.get('refuri') == anchor]
if not refs:
return
target, labelid = app.env.domains.standard_domain.anonlabels.get(PCAP_MODULE_LABEL, ('', ''))
if not target:
sphinx_logging.getLogger(__name__).warning(
'undefined label: %r, which |pcap|_ links to', PCAP_MODULE_LABEL, location=refs[0])
return
try:
page = app.builder.get_relative_uri(docname, target)
except NoUri:
for ref in refs:
ref.replace_self(ref.children)
return
if '#' in page:
return
for ref in refs:
ref['refuri'] = f'{page}#{labelid}'
ref['internal'] = True


def follow_builtins_alias(app: 'Sphinx', modname: 'Optional[str]', # pylint: disable=unused-argument
attribute: str) -> 'Optional[str]':
"""Keep viewcode on the module that binds a name whose object reports :mod:`builtins`.
Expand Down Expand Up @@ -1159,6 +1221,7 @@ def setup(app: 'Sphinx') -> None:
app.connect('autodoc-before-process-signature', sign_wrapped_function, priority=700)
app.connect('viewcode-follow-imported', follow_builtins_alias)
app.add_post_transform(DropTableWrapperIds)
app.connect('doctree-resolved', link_pcap_module)
app.connect('doctree-read', resolve_builtins_exactly)
# NB: either side of napoleon's ``autodoc-process-docstring`` handler, which
# connects at the default 500.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/contributing/conventions/process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ back to the default engine rather than raising when a requested engine is absent
.. note::

mypy types the engine packages that ship no types -- |dpkt|_, |pyshark|_,
``pcap`` and |pcapfile|_ -- from local stubs under :file:`typings/`, which
|pcap|_ and |pcapfile|_ -- from local stubs under :file:`typings/`, which
:file:`mypy.ini` registers through ``mypy_path`` and which ship in neither the
wheel nor the sdist (:issue:`1878`). ``scapy`` ships its own. A stub declares only
what :mod:`pcapkit` uses and cites the version it was written against, so a name
Expand Down
2 changes: 1 addition & 1 deletion docs/source/contributing/pep.rst
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ both of the following are worth knowing before reaching for them:
works on every supported interpreter, verified on 3.10 and 3.14.

A separate engine rather than a second backend because the two are **mutually
exclusive**: both distributions own the import name ``pcap``, and with both
exclusive**: both distributions own the import name |pcap|_, and with both
installed ``pcap-ct`` wins the import while upstream's extension module is
shadowed and unreachable. Each engine detects which backend it actually got and
says so, rather than one ``engine=`` string silently meaning two different
Expand Down
2 changes: 1 addition & 1 deletion docs/source/ext.rst
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ The available engines and the file formats they read:
.. note::

`PyPCAP`_ and `pcap-ct`_ are two distributions of one :manpage:`libpcap(3)`
interface, and both install a top-level ``pcap`` module, so they are two
interface, and both install a top-level |pcap|_ module, so they are two
engines rather than one: ``engine='pypcap'`` and ``engine='pcap_ct'``. Upstream
`PyPCAP`_ cannot be installed on Python 3.12 or newer, and `pcap-ct`_ can --
which is why both exist. See
Expand Down
12 changes: 6 additions & 6 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
PyPCAPKit - Comprehensive Network Packet Analysis Library
=========================================================

PyPCAPKit is an open source Python library for `PCAP`_ file extraction,
construction and analysis.
PyPCAPKit is an open source Python library for
`PCAP <https://en.wikipedia.org/wiki/Pcap>`__ file extraction, construction and
analysis.

.. important::

Expand Down Expand Up @@ -132,7 +133,7 @@ spelled ``'pcapkit'``) is :mod:`pcapkit`'s own parser and the only one with no
third-party requirement.

`PyPCAP`_ and `pcap-ct`_ are two independent distributions of the same
:manpage:`libpcap(3)` interface and both install a top-level ``pcap`` module,
:manpage:`libpcap(3)` interface and both install a top-level |pcap|_ module,
so they are two engines rather than one: upstream `PyPCAP`_ stops at Python 3.11,
`pcap-ct`_ covers 3.10 and newer. **Install exactly one of them** -- with both
present, ``pcap-ct`` wins the import and the other becomes unselectable.
Expand Down Expand Up @@ -164,7 +165,7 @@ Every engine also answers a preflight check --
:meth:`Extractor.run <pcapkit.foundation.extraction.Extractor.run>` consults
before anything is imported -- so asking for one that cannot run in the current
environment gives a single warning naming the actual cause (a Python version, a
missing :program:`tshark`, a missing :manpage:`libpcap(3)`, the wrong ``pcap``
missing :program:`tshark`, a missing :manpage:`libpcap(3)`, the wrong |pcap|_
distribution) and a clean fall back, rather than an error from inside the
third-party package.

Expand Down Expand Up @@ -358,7 +359,7 @@ plug-in functions, you may want to install the optional ones:
.. warning::

**Install only one of** ``pypcap`` **and** ``pcap-ct``. Both own the top-level
``pcap`` module, and ``pip`` will install both without complaint. With both
|pcap|_ module, and ``pip`` will install both without complaint. With both
present the ``pcap-ct`` package wins the import and ``pypcap``'s extension
module is shadowed and unreachable, so ``engine='pypcap'`` stops working --
measured on Python 3.10. :mod:`pcapkit` detects that state and warns with an
Expand Down Expand Up @@ -476,5 +477,4 @@ Indices and Tables
* :ref:`modindex`
* :ref:`search`

.. _PCAP: https://en.wikipedia.org/wiki/Pcap
.. _libpcap: https://www.tcpdump.org
4 changes: 3 additions & 1 deletion docs/source/pcapkit/foundation/engines/3rdparty.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ support, as is used by :class:`pcapkit.foundation.extraction.Extractor`.
expected to lift. Use
:class:`~pcapkit.foundation.engines.pcap_ct.PCAP_CT` on 3.12 and newer.

.. _pcap-module:

.. important::

`pcap-ct`_ installs the same top-level ``pcap`` module as `PyPCAP`_. This
Expand Down Expand Up @@ -435,7 +437,7 @@ Backend Detection

.. module:: pcapkit.foundation.engines._pcap_backend

Unrelated PyPI distributions install a top-level module named ``pcap`` --
Unrelated PyPI distributions install a top-level module named |pcap|_ --
`PyPCAP`_, a Cython binding shipped as a single extension module, and `pcap-ct`_,
a :mod:`ctypes` reimplementation shipped as a package. They therefore collide,
and ``import pcap`` resolves to whichever the import system finds first.
Expand Down
6 changes: 3 additions & 3 deletions docs/source/pcapkit/foundation/engines/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ one warning naming the actual cause and a fall back to the built-in parser.
``tshark``, :class:`~pcapkit.foundation.engines.pypcapfile.PyPCAPFile` its Python ceiling,
:class:`~pcapkit.foundation.engines.pcap_ct.PCAP_CT` for a system ``libpcap``, and
:class:`~pcapkit.foundation.engines.pypcap.PyPCAP` and
:class:`~pcapkit.foundation.engines.pcap_ct.PCAP_CT` which distribution owns ``pcap``.
:class:`~pcapkit.foundation.engines.pcap_ct.PCAP_CT` which distribution owns |pcap|_.
:class:`~pcapkit.foundation.engines.pypcap.PyPCAP` deliberately has no Python ceiling
check, even though upstream cannot be installed on 3.12 or newer: what matters is which
distribution is present, and a version check would refuse a working build.
Expand All @@ -190,7 +190,7 @@ Choosing Between PyPCAP and PCAP_CT
:class:`~pcapkit.foundation.engines.pypcap.PyPCAP` and
:class:`~pcapkit.foundation.engines.pcap_ct.PCAP_CT` read the same
:manpage:`libpcap(3)` interface from two independent distributions, both of which
install a top-level ``pcap`` module. They are separate engines because they
install a top-level |pcap|_ module. They are separate engines because they
are separate projects, with different authors, different install requirements
and different interpreter coverage:

Expand Down Expand Up @@ -223,7 +223,7 @@ installed.** Pick on availability, not on features.
otherwise be.

**The two distributions collide, so install exactly one.** Both own the top-level
``pcap`` module, and pip will happily install both -- on Python 3.10,
|pcap|_ module, and pip will happily install both -- on Python 3.10,
the ``pcap-ct`` package then wins the import and upstream's extension module is
shadowed and unreachable. Each engine therefore detects which distribution it
actually got, via
Expand Down
26 changes: 13 additions & 13 deletions pcapkit/foundation/engines/_pcap_backend.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
"""Which distribution owns the ``pcap`` module?
"""Which distribution owns the |pcap|_ module?
=====================================================

.. module:: pcapkit.foundation.engines._pcap_backend

Unrelated PyPI distributions install a top-level module named ``pcap``:
Unrelated PyPI distributions install a top-level module named |pcap|_:

* `PyPCAP`_ -- a Cython binding, shipped as a single extension module
(:file:`pcap.cpython-*.so`), installable only up to Python 3.11.
Expand Down Expand Up @@ -48,7 +48,7 @@
PYPCAP = 'pypcap'
#: Distribution name of `pcap-ct`_.
PCAP_CT = 'pcap-ct'
#: Every distribution known to provide ``pcap``, in a fixed order so that
#: Every distribution known to provide |pcap|_, in a fixed order so that
#: messages naming several of them read the same way every time.
DISTRIBUTIONS = (PYPCAP, PCAP_CT)
#: The ``engine=`` string that drives each distribution. Kept here rather than in
Expand All @@ -62,7 +62,7 @@

@info_final
class Probe(Info):
"""What one attempt to import ``pcap`` found."""
"""What one attempt to import |pcap|_ found."""

#: Distribution that provided the imported module -- :data:`PYPCAP`,
#: :data:`PCAP_CT`, or :data:`None` when the import did not succeed.
Expand Down Expand Up @@ -101,9 +101,9 @@ def describe(self) -> 'str':


def identify(module: 'ModuleType') -> 'str':
"""Which distribution does an imported ``pcap`` module come from?
"""Which distribution does an imported |pcap|_ module come from?

`pcap-ct`_ ships ``pcap`` as a package whose ``__init__`` does
`pcap-ct`_ ships |pcap|_ as a package whose ``__init__`` does
``from ._pcap import *``, which binds the submodule as an attribute; upstream
`PyPCAP`_ ships a single extension module, which has no such attribute. That
is a structural difference rather than a cosmetic one, which is why it is
Expand All @@ -116,7 +116,7 @@ def identify(module: 'ModuleType') -> 'str':
present on upstream ``pypcap`` 1.3.0 as well.

Args:
module: An already-imported ``pcap`` module.
module: An already-imported |pcap|_ module.

Returns:
:data:`PCAP_CT` or :data:`PYPCAP`.
Expand All @@ -128,7 +128,7 @@ def identify(module: 'ModuleType') -> 'str':
def installed_distributions() -> 'tuple[str, ...]':
"""Which of :data:`DISTRIBUTIONS` are installed, per package metadata.

Asked of the metadata rather than of ``pcap`` itself, because that is the
Asked of the metadata rather than of |pcap|_ itself, because that is the
only way to see the distribution the import did *not* resolve to -- which is
exactly the collision worth reporting.

Expand All @@ -152,7 +152,7 @@ def installed_distributions() -> 'tuple[str, ...]':


def _purge() -> 'None':
"""Drop the ``pcap`` and ``libpcap`` module trees from :data:`sys.modules`.
"""Drop the |pcap|_ and ``libpcap`` module trees from :data:`sys.modules`.

Called after a failed import so that the *next* probe reproduces the same
failure. Without it a second attempt reports something else entirely, which
Expand All @@ -170,8 +170,8 @@ def _purge() -> 'None':

That message names neither the missing library nor the package it came from,
and it is what the user would otherwise be shown. ``libpcap`` is purged as
well as ``pcap`` because the residue is in whichever of the two got part-way:
purging only ``pcap`` moved the ``NameError`` from one to the other rather
well as |pcap|_ because the residue is in whichever of the two got part-way:
purging only |pcap|_ moved the ``NameError`` from one to the other rather
than removing it.

"""
Expand All @@ -184,7 +184,7 @@ def _purge() -> 'None':


def probe() -> 'Probe':
"""Import ``pcap`` and report what was found.
"""Import |pcap|_ and report what was found.

Not cached. The cost after a successful first call is a :data:`sys.modules`
lookup, and a cache would make the answer depend on when it was first asked --
Expand Down Expand Up @@ -228,7 +228,7 @@ def probe() -> 'Probe':


def wrong_backend_reason(wanted: 'str', found: 'Probe') -> 'Optional[str]':
"""Why ``wanted`` cannot run, when some *other* distribution owns ``pcap``.
"""Why ``wanted`` cannot run, when some *other* distribution owns |pcap|_.

Args:
wanted: The distribution the calling engine drives -- :data:`PYPCAP` or
Expand Down
Loading
Loading