From 1ca8b55c9e91245def3d3070939c0f55df6789d4 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 19 Oct 2026 11:54:39 -0400 Subject: [PATCH] docs: link inventory-less packages through rst_epilog substitutions (#1936) - Define every third-party project substitution and target once, in rst_epilog in docs/source/conf.py, and drop the per-page and per-docstring copies, which duplicated each other on assembled pages. - Write the aenum, cryptography, dpkt, emoji, pcapfile and pyshark package literals as |pkg|_ links in pages, docstrings and doc comments; engine, adapter, toolkit and requirement names, and names below a package, stay literals. - Keep pcap a literal: PyPCAP and pcap-ct both install it, so no one project page is its target. - Leave format_class as is: it names classes, which stay literals, and signatures do not expand substitutions. - Add a test pinning the epilog, the links and the absence of duplicates, and point the #1919 test's message at the |pkg|_ form. Closes #1936 --- docs/source/conf.py | 53 +++- .../conventions/mint-criterion.rst | 6 +- .../contributing/conventions/process.rst | 4 +- .../conventions/registry-protocol.rst | 8 +- docs/source/contributing/pep.rst | 7 +- docs/source/ext.rst | 7 - docs/source/index.rst | 19 +- docs/source/pcapkit/const/index.rst | 2 +- docs/source/pcapkit/const/ngap.rst | 3 - docs/source/pcapkit/const/tcp.rst | 2 +- docs/source/pcapkit/corekit/multidict.rst | 2 - .../pcapkit/foundation/engines/3rdparty.rst | 21 +- .../pcapkit/foundation/engines/index.rst | 11 +- docs/source/pcapkit/foundation/extraction.rst | 7 - docs/source/pcapkit/index.rst | 11 +- docs/source/pcapkit/interface/core.rst | 4 - .../pcapkit/protocols/application/ngap.rst | 3 - .../source/pcapkit/protocols/internet/esp.rst | 5 +- .../pcapkit/protocols/internet/internet.rst | 2 +- docs/source/pcapkit/protocols/protocol.rst | 2 +- docs/source/pcapkit/toolkit/3rdparty.rst | 12 - docs/source/pcapkit/utilities/chardet.rst | 2 - docs/source/pcapkit/utilities/exceptions.rst | 3 - docs/source/pcapkit/vendor/ngap.rst | 3 - pcapkit/__init__.py | 6 - pcapkit/__main__.py | 5 +- pcapkit/const/ngap/__init__.py | 3 - pcapkit/corekit/enum.py | 16 +- pcapkit/corekit/fields/numbers.py | 6 +- pcapkit/corekit/fields/strings.py | 3 - pcapkit/corekit/multidict.py | 2 - pcapkit/dumpkit/common.py | 2 +- pcapkit/foundation/engines/__init__.py | 6 - pcapkit/foundation/engines/_pcap_backend.py | 6 - pcapkit/foundation/engines/dpkt.py | 18 +- pcapkit/foundation/engines/pcap_ct.py | 8 - pcapkit/foundation/engines/pypcap.py | 12 - pcapkit/foundation/engines/pypcapfile.py | 38 ++- pcapkit/foundation/engines/pyshark.py | 26 +- pcapkit/foundation/engines/scapy.py | 10 +- pcapkit/foundation/reassembly/data/data.py | 2 +- pcapkit/protocols/application/ngap.py | 3 - pcapkit/protocols/internet/esp.py | 7 +- pcapkit/protocols/protocol.py | 3 - pcapkit/toolkit/dpkt.py | 54 +--- pcapkit/toolkit/pcap.py | 2 +- pcapkit/toolkit/pcap_ct.py | 16 -- pcapkit/toolkit/pypcap.py | 16 -- pcapkit/toolkit/pypcapfile.py | 30 --- pcapkit/toolkit/pyshark.py | 8 +- pcapkit/toolkit/scapy.py | 2 - pcapkit/utilities/chardet.py | 2 - pcapkit/utilities/exceptions.py | 3 - pcapkit/vendor/default.py | 3 - pcapkit/vendor/ngap/__init__.py | 3 - pcapkit/vendor/ngap/procedure_code.py | 3 - pcapkit/vendor/ngap/protocol_ie.py | 3 - .../test_docs_epilog_links_1936_unit.py | 238 ++++++++++++++++++ .../test_docs_unresolvable_roles_1919_unit.py | 10 +- 59 files changed, 387 insertions(+), 387 deletions(-) create mode 100644 tests/project/test_docs_epilog_links_1936_unit.py diff --git a/docs/source/conf.py b/docs/source/conf.py index 53cc1e117..d85723d98 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -175,9 +175,56 @@ # and *zero* ``py:`` objects (5-8 objects apiece), so no ``:mod:``, # ``:class:`` or ``:func:`` cross-reference into them could ever resolve. # -# Those packages are therefore referenced as plain external links instead, the -# ``.. _PyPCAP: https://github.com/pynetwork/pypcap`` style used throughout -# ``pcapkit/foundation/engines/3rdparty.rst``. +# Those packages are therefore linked to their project pages instead, through +# :data:`rst_epilog` below (:issue:`1936`). + +#: 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 +#: names again: a second substitution definition of one name is an error, and a +#: second target of one name warns as soon as the two disagree on the URL. +#: +#: ``|pkg|_`` renders the import name as a literal linked to the project, for a +#: package or module of a package with no ``py:`` objects to cross-reference, +#: 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. +rst_epilog = """ +.. |aenum| replace:: ``aenum`` +.. _aenum: https://github.com/ethanfurman/aenum +.. |chardet| replace:: ``chardet`` +.. _chardet: https://chardet.readthedocs.io +.. |cryptography| replace:: ``cryptography`` +.. _cryptography: https://cryptography.io +.. _DictDumper: https://dictdumper.jarryshaw.me +.. |dpkt| replace:: ``dpkt`` +.. _DPKT: https://dpkt.readthedocs.io +.. |emoji| replace:: ``emoji`` +.. _emoji: https://carpedm20.github.io/emoji/docs/ +.. |jspcapy| replace:: ``jspcapy`` +.. _jspcapy: https://github.com/JarryShaw/jspcapy +.. |pcap-ct| replace:: ``pcap-ct`` +.. _pcap-ct: https://pypi.org/project/pcap-ct/ +.. |pcapfile| replace:: ``pcapfile`` +.. _pcapfile: https://github.com/kisom/pypcapfile +.. |pycrate| replace:: ``pycrate`` +.. _pycrate: https://github.com/pycrate-org/pycrate +.. |pypcap| replace:: ``pypcap`` +.. _PyPCAP: https://github.com/pynetwork/pypcap +.. |pypcapfile| replace:: ``pypcapfile`` +.. _PyPCAPFile: https://github.com/kisom/pypcapfile +.. |pyshark| replace:: ``pyshark`` +.. _PyShark: https://kiminewt.github.io/pyshark +.. |requests| replace:: ``requests`` +.. _requests: https://requests.readthedocs.io +.. _Scapy: https://scapy.net +.. |tbtrim| replace:: ``tbtrim`` +.. _tbtrim: https://github.com/gousaiyang/tbtrim +.. _Werkzeug: https://werkzeug.palletsprojects.com/ +""" autodoc_default_options = { # 'members': True, diff --git a/docs/source/contributing/conventions/mint-criterion.rst b/docs/source/contributing/conventions/mint-criterion.rst index 9b79f3487..fe5d8a583 100644 --- a/docs/source/contributing/conventions/mint-criterion.rst +++ b/docs/source/contributing/conventions/mint-criterion.rst @@ -24,8 +24,8 @@ a style preference**: does not grow, and a second lookup of the same value builds an equal but distinct object rather than handing back the first. -``super()._missing_(value)`` -- *hand back to* ``aenum`` - Neither of the above. For an ``aenum`` flag base this composes a pseudo-member and +``super()._missing_(value)`` -- *hand back to* |aenum|_ + Neither of the above. For an |aenum|_ flag base this composes a pseudo-member and caches it in ``_value2member_map_`` only, so ``__members__`` and iteration are unchanged but a second lookup returns the same object. For a plain enum it returns ``None`` and the lookup raises :exc:`ValueError`; the measured case is @@ -61,7 +61,7 @@ Measured against the tree, by class: no unassigned value is left for ``super()._missing_`` (the line after the check is unreachable). -Note that these are ``aenum`` classes: ``issubclass(Flags, enum.IntFlag)`` is ``False`` +Note that these are |aenum|_ classes: ``issubclass(Flags, enum.IntFlag)`` is ``False`` against the standard library and ``True`` against ``aenum.IntFlag``, so a probe against ``enum`` answers about the wrong hierarchy. diff --git a/docs/source/contributing/conventions/process.rst b/docs/source/contributing/conventions/process.rst index b8869926b..b68e2909d 100644 --- a/docs/source/contributing/conventions/process.rst +++ b/docs/source/contributing/conventions/process.rst @@ -66,8 +66,8 @@ 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 + mypy types the engine packages that ship no types -- |dpkt|_, |pyshark|_, + ``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 diff --git a/docs/source/contributing/conventions/registry-protocol.rst b/docs/source/contributing/conventions/registry-protocol.rst index a9ce06d87..5e74e5d2a 100644 --- a/docs/source/contributing/conventions/registry-protocol.rst +++ b/docs/source/contributing/conventions/registry-protocol.rst @@ -105,7 +105,7 @@ What is easy to get wrong about it: With **no usable** ``default``, the rejection reaches the caller **unwrapped**. ``get`` re-raises a :exc:`ValueError` that is already a :exc:`~pcapkit.utilities.exceptions.BaseError` exactly as the override raised it, - and converts only ``aenum``'s and :mod:`enum`'s own "no member carries this + and converts only |aenum|_'s and :mod:`enum`'s own "no member carries this value". The consequence is the point of the discrimination rather than a side effect: the override's **own message** survives to the caller instead of being replaced by the base's. So an override should say in its message what it rejected @@ -120,7 +120,7 @@ What is easy to get wrong about it: :class:`~pcapkit.corekit.enum.EnumLookup` was **phase 2** of :issue:`877`, and it is **complete**: the phase landed for every non-registry enumeration. **No enumeration remains outside the hierarchy**, measured by a - runtime walk over both the :mod:`enum` and ``aenum`` flavours. + runtime walk over both the :mod:`enum` and |aenum|_ flavours. Failed-Lookup Exceptions ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -189,7 +189,7 @@ installed, the interpreter's own unless an earlier loud error installed ``cls(key)`` in a block declaring that it catches :exc:`ValueError` itself, so the loud :exc:`~pcapkit.utilities.exceptions.EnumValueError` a generated override raises is constructed quietly without the override passing ``quiet=True``, and - the error ``get`` converts from ``aenum``'s own is raised quiet as well. An error + the error ``get`` converts from |aenum|_'s own is raised quiet as well. An error of any other class is not a lookup miss, and stays loud. * :meth:`~pcapkit.corekit.enum.EnumRegistry.register` **stays loud**, even when reached from inside such a block: refusing a mint the caller explicitly asked for @@ -400,7 +400,7 @@ The population it covers is every :class:`~pcapkit.corekit.enum.EnumRegistry` subclass, every one of them under :mod:`pcapkit.const` and each either :class:`int`-valued (the flag registries among them) or ``aenum.StrEnum``-valued, plus every non-registry enumeration a runtime walk over both the :mod:`enum` and -``aenum`` flavours finds, nested classes included -- such as ``FrameType.Flags`` in +|aenum|_ flavours finds, nested classes included -- such as ``FrameType.Flags`` in :mod:`pcapkit.protocols.schema.application.httpv2` and its concrete per-frame subclasses. diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index c0a038c4b..f381e2982 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -124,7 +124,7 @@ that is not an HMAC needs more than a row -- :class:`~pcapkit.protocols.internet.esp.IntegritySuite` records the digest as the name of a :mod:`hashlib` constructor, which AES-XCBC and AES-CMAC are not. And :func:`~pcapkit.protocols.internet.esp.load_cryptography` imports only the -``ciphers`` submodules of ``cryptography``, so the AEAD and CMAC primitives +``ciphers`` submodules of |cryptography|_, so the AEAD and CMAC primitives have to be added there before a cipher suite can reach them. Unsupported algorithms are refused loudly, when the Security Association is constructed rather than when a packet is read, so nothing decodes wrongly in the meantime. @@ -582,9 +582,6 @@ and :meth:`~pcapkit.foundation.engines.engine.EngineBase.read_frame`; passing th still apply is the unified auxiliary tools in :mod:`pcapkit.toolkit`, where each engine has a matching module. -.. _pypcap: https://github.com/pynetwork/pypcap -.. _pypcapfile: https://github.com/kisom/pypcapfile - Test Cases ---------- @@ -831,7 +828,7 @@ the SCTP packet alone. Note also that SCTP's CRC32c is a hand-rolled lookup tabl in :mod:`pcapkit.protocols.transport.sctp`, not a library call, so it is not precedent for a dependency either. -**The cryptographic integrity checks** are where the ``cryptography`` +**The cryptographic integrity checks** are where the |cryptography|_ dependency — introduced for ESP — genuinely buys something new, and half of that is already done. :class:`~pcapkit.protocols.internet.esp.ESP` **already verifies its ICV** when a Security Association is supplied, and deliberately reports a diff --git a/docs/source/ext.rst b/docs/source/ext.rst index d0a48ffae..305a6a71e 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -444,13 +444,6 @@ The available engines and the file formats they read: which is why both exist. See :doc:`pcapkit/foundation/engines/index` for which to pick. -.. _Scapy: https://scapy.net -.. _DPKT: https://dpkt.readthedocs.io -.. _PyShark: https://kiminewt.github.io/pyshark -.. _PyPCAP: https://github.com/pynetwork/pypcap -.. _pcap-ct: https://pypi.org/project/pcap-ct/ -.. _PyPCAPFile: https://github.com/kisom/pypcapfile - Samples ~~~~~~~ diff --git a/docs/source/index.rst b/docs/source/index.rst index b3194fc08..b26218e45 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -56,9 +56,6 @@ About There is a project called |jspcapy|_ that works on :mod:`pcapkit`, which is a command line tool for PCAP extraction. - .. |jspcapy| replace:: ``jspcapy`` - .. _jspcapy: https://github.com/JarryShaw/jspcapy - .. deprecated:: 0.8.0 The |jspcapy|_ project is deprecated and has been merged into the @@ -190,7 +187,7 @@ Engine 3.10 3.11 3.12 3.13 3.14 3.15 [*]_ ================== ======== ======== ======== =========== ======== =========== .. [*] Inferred rather than measured: no 3.15 interpreter was available. -.. [*] ``pyshark`` also needs Wireshark's :program:`tshark`, which was absent here, +.. [*] |pyshark|_ also needs Wireshark's :program:`tshark`, which was absent here, so only the interpreter half of each verdict in this row was verified. ``pypcap`` and ``pypcapfile`` stop at 3.11, and ``pyshark`` at 3.13, for the @@ -382,17 +379,17 @@ preflight described under `Engine Comparison`_. cleanly and then fails when used. - Drives Wireshark's :program:`tshark` binary. It need not be on ``PATH``: - ``pyshark`` looks at ``tshark_path`` in its :file:`config.ini` first, then + |pyshark|_ looks at ``tshark_path`` in its :file:`config.ini` first, then ``PATH`` on POSIX, both Program Files directories on Windows, and :file:`/Applications/Wireshark.app` on macOS. Install Wireshark (or just :program:`tshark`) from your platform's package manager. - - Requires Python **3.13 or older**. ``pyshark`` 0.6 builds its event loop + - Requires Python **3.13 or older**. |pyshark|_ 0.6 builds its event loop with ``asyncio.get_event_loop_policy().get_event_loop()``, and from Python **3.14** :func:`asyncio.get_event_loop` raises :exc:`RuntimeError` when there is no current event loop instead of quietly creating one. Measured: a loop is returned silently on 3.10 and 3.11, returned with a :exc:`DeprecationWarning` on 3.12, and refused on 3.14. 3.13 sits on the - deprecated-but-functional side of that change, and ``pyshark`` extracted a + deprecated-but-functional side of that change, and |pyshark|_ extracted a capture there. :class:`~pcapkit.foundation.engines.pypcap.PyPCAP` @@ -457,7 +454,7 @@ preflight described under `Engine Comparison`_. Only the engines above carry extra constraints. :mod:`pcapkit` itself and its ``default``, ``dpkt`` and ``scapy`` engines need nothing beyond their - ``pip install`` -- ``dpkt`` 1.9.8 and ``scapy`` 2.7.0 were both measured + ``pip install`` -- |dpkt|_ 1.9.8 and ``scapy`` 2.7.0 were both measured reading a capture on Python 3.14. The ``scapy`` engine needs **scapy 2.6.0 or newer**, which the ``Scapy`` extra requires: 2.5.0 can drop a PCAP-NG interface's timestamp resolution, reading nanoseconds as microseconds @@ -480,10 +477,4 @@ Indices and Tables * :ref:`search` .. _PCAP: https://en.wikipedia.org/wiki/Pcap -.. _Scapy: https://scapy.net -.. _DPKT: https://dpkt.readthedocs.io -.. _PyShark: https://kiminewt.github.io/pyshark -.. _PyPCAP: https://github.com/pynetwork/pypcap -.. _pcap-ct: https://pypi.org/project/pcap-ct/ -.. _PyPCAPFile: https://github.com/kisom/pypcapfile .. _libpcap: https://www.tcpdump.org diff --git a/docs/source/pcapkit/const/index.rst b/docs/source/pcapkit/const/index.rst index 4bb3a2def..ba5bbb02f 100644 --- a/docs/source/pcapkit/const/index.rst +++ b/docs/source/pcapkit/const/index.rst @@ -27,7 +27,7 @@ Some behave otherwise, and :ref:`mint-criterion` gives the full split: * :class:`~pcapkit.const.tcp.flags.Flags` and the Mobility Header flag registries, such as :class:`~pcapkit.const.mh.binding_ack_flag.BindingACKFlag`, hand back to - ``aenum``'s flag base, which caches the composed object in the value lookup + |aenum|_'s flag base, which caches the composed object in the value lookup table only: a second lookup returns the same object, while ``__members__`` and iteration are unchanged. * :class:`~pcapkit.const.hip.transport.Transport`, diff --git a/docs/source/pcapkit/const/ngap.rst b/docs/source/pcapkit/const/ngap.rst index f1361d54e..ef2687c51 100644 --- a/docs/source/pcapkit/const/ngap.rst +++ b/docs/source/pcapkit/const/ngap.rst @@ -49,6 +49,3 @@ which is automatically generated from :class:`pcapkit.vendor.ngap.protocol_ie.Pr .. [*] 3GPP TS 38.413 .. [*] 3GPP TS 38.413 - -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate diff --git a/docs/source/pcapkit/const/tcp.rst b/docs/source/pcapkit/const/tcp.rst index 5a713da71..65074282a 100644 --- a/docs/source/pcapkit/const/tcp.rst +++ b/docs/source/pcapkit/const/tcp.rst @@ -69,7 +69,7 @@ which is automatically generated from :class:`pcapkit.vendor.tcp.flags.Flags`. .. versionchanged:: 1.5.0 A value outside the 16-bit field raises :exc:`ValueError`, where - ``aenum`` composed a pseudo-member for any integer. + |aenum|_ composed a pseudo-member for any integer. .. autoclass:: pcapkit.const.tcp.flags.Flags :members: diff --git a/docs/source/pcapkit/corekit/multidict.rst b/docs/source/pcapkit/corekit/multidict.rst index a1cd6042c..58e0cc904 100644 --- a/docs/source/pcapkit/corekit/multidict.rst +++ b/docs/source/pcapkit/corekit/multidict.rst @@ -6,8 +6,6 @@ Multi-Mapping Dictionary :mod:`pcapkit.corekit.multidict` contains dictionaries that store multiple values per key, based on the `Werkzeug`_ project. -.. _Werkzeug: https://werkzeug.palletsprojects.com/ - .. autoclass:: pcapkit.corekit.multidict.MultiDict :no-members: :no-special-members: __init__ diff --git a/docs/source/pcapkit/foundation/engines/3rdparty.rst b/docs/source/pcapkit/foundation/engines/3rdparty.rst index c392e0bb1..a7d112d45 100644 --- a/docs/source/pcapkit/foundation/engines/3rdparty.rst +++ b/docs/source/pcapkit/foundation/engines/3rdparty.rst @@ -10,8 +10,6 @@ Scapy Support This module contains the implementation for `Scapy`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _Scapy: https://scapy.net - .. note:: Constructing this engine imports ``scapy.all``, which populates `Scapy`_'s @@ -28,7 +26,7 @@ support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. One side effect: ``scapy.all`` loads :mod:`scapy.layers.dcerpc`, which reaches `Scapy`_'s TLS layer and there triggers a - ``CryptographyDeprecationWarning`` from ``cryptography`` about + ``CryptographyDeprecationWarning`` from |cryptography|_ about finite-field Diffie-Hellman. It concerns a key-exchange code path :mod:`pcapkit` never executes, but it subclasses :exc:`UserWarning` rather than :exc:`DeprecationWarning`, so Python's default filters show it. @@ -65,8 +63,6 @@ DPKT Support This module contains the implementation for `DPKT`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _DPKT: https://dpkt.readthedocs.io - .. note:: ``dpkt.pcapng.Reader`` reads only the **first** Interface Description Block @@ -106,8 +102,6 @@ PyShark Support This module contains the implementation for `PyShark`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _PyShark: https://kiminewt.github.io/pyshark - .. important:: `PyShark`_ has requirements beyond installing it, and none is @@ -116,7 +110,7 @@ support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. :meth:`~pcapkit.foundation.engines.pyshark.PyShark.unsupported_reason` checks both up front. - **Python 3.13 or older.** ``pyshark`` 0.6 builds its event loop with + **Python 3.13 or older.** |pyshark|_ 0.6 builds its event loop with ``asyncio.get_event_loop_policy().get_event_loop()``. With no running loop, 3.10 and 3.11 return a loop silently, 3.12 returns one with a :exc:`DeprecationWarning`, and **3.14 raises** @@ -124,12 +118,12 @@ support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. 3.13 is unchecked and expected to work, being on the deprecated-but-functional side of that change. - **Wireshark's** :program:`tshark` **binary.** ``pyshark`` shells out to it and - parses nothing itself. It need not be on :envvar:`PATH`: ``pyshark`` consults + **Wireshark's** :program:`tshark` **binary.** |pyshark|_ shells out to it and + parses nothing itself. It need not be on :envvar:`PATH`: |pyshark|_ consults ``tshark_path`` in its :file:`config.ini` first, then :envvar:`PATH` on POSIX, both Program Files directories on Windows, and :file:`/Applications/Wireshark.app` on macOS. The check delegates to - ``pyshark``'s own resolver for exactly that reason, so a correctly configured + |pyshark|_'s own resolver for exactly that reason, so a correctly configured install off :envvar:`PATH` is not refused. .. autoclass:: pcapkit.foundation.engines.pyshark.PyShark @@ -157,8 +151,6 @@ PyPCAP Support This module contains the implementation for `PyPCAP`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _PyPCAP: https://github.com/pynetwork/pypcap - .. important:: `PyPCAP`_ publishes no wheels, so installing it compiles a C extension and @@ -247,7 +239,6 @@ pcap-ct Support This module contains the implementation for `pcap-ct`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _pcap-ct: https://pypi.org/project/pcap-ct/ .. _libpcap: https://pypi.org/project/libpcap/ .. important:: @@ -393,8 +384,6 @@ PyPCAPFile Support This module contains the implementation for `PyPCAPFile`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _PyPCAPFile: https://github.com/kisom/pypcapfile - .. important:: `PyPCAPFile`_ is a pure Python savefile reader that decodes Ethernet, IPv4, diff --git a/docs/source/pcapkit/foundation/engines/index.rst b/docs/source/pcapkit/foundation/engines/index.rst index c54b98a87..461e5509f 100644 --- a/docs/source/pcapkit/foundation/engines/index.rst +++ b/docs/source/pcapkit/foundation/engines/index.rst @@ -144,8 +144,8 @@ parser -- but the extraction then does not use the engine requested. | :class:`~pcapkit.foundation.engines.scapy.Scapy` | overrides ``unsupported_reason()`` | +-----------------------------------------------------------------+---------------------------------------------------------------+ | :class:`~pcapkit.foundation.engines.pyshark.PyShark` | Wireshark's :program:`tshark` binary -- on ``PATH``, or | -| | wherever ``pyshark``'s :file:`config.ini` points -- **and** | -| | Python **3.13 or older**: ``pyshark`` 0.6 builds its event | +| | wherever |pyshark|_'s :file:`config.ini` points -- **and** | +| | Python **3.13 or older**: |pyshark|_ 0.6 builds its event | | | loop with :func:`asyncio.get_event_loop`, which raises from | | | 3.14 | +-----------------------------------------------------------------+---------------------------------------------------------------+ @@ -240,10 +240,3 @@ actually got, via .. _PCAP-NG: https://wiki.wireshark.org/Development/PcapNg .. _libpcap: https://pypi.org/project/libpcap/ - -.. _Scapy: https://scapy.net -.. _DPKT: https://dpkt.readthedocs.io -.. _PyShark: https://kiminewt.github.io/pyshark -.. _PyPCAP: https://github.com/pynetwork/pypcap -.. _pcap-ct: https://pypi.org/project/pcap-ct/ -.. _PyPCAPFile: https://github.com/kisom/pypcapfile diff --git a/docs/source/pcapkit/foundation/extraction.rst b/docs/source/pcapkit/foundation/extraction.rst index bd5fc4619..0d3a4dcfc 100644 --- a/docs/source/pcapkit/foundation/extraction.rst +++ b/docs/source/pcapkit/foundation/extraction.rst @@ -109,10 +109,3 @@ Type Variables :type: typing.Any Frame type the extractor yields, as read by its engine. - -.. |pypcap| replace:: ``pypcap`` -.. _pypcap: https://github.com/pynetwork/pypcap -.. |pcap-ct| replace:: ``pcap-ct`` -.. _pcap-ct: https://pypi.org/project/pcap-ct/ -.. |pypcapfile| replace:: ``pypcapfile`` -.. _pypcapfile: https://github.com/kisom/pypcapfile diff --git a/docs/source/pcapkit/index.rst b/docs/source/pcapkit/index.rst index 56bd34edb..3ece229f8 100644 --- a/docs/source/pcapkit/index.rst +++ b/docs/source/pcapkit/index.rst @@ -7,8 +7,6 @@ Module Documentation :mod:`pcapkit` is an independent open source library, using only `DictDumper`_ as its formatted output dumper. -.. _DictDumper: https://dictdumper.jarryshaw.me - Unlike popular PCAP file extractors, such as `Scapy`_, `DPKT`_ and `PyShark`_, :mod:`pcapkit` tends to provide comprehensive support to all protocols, including but not limited to their @@ -16,10 +14,6 @@ parameters and/or options, etc. Therefore, :mod:`pcapkit` contains enumeration registries used by protocols, independent protocol schema definitions as well as various customisable interfaces. -.. _Scapy: https://scapy.net -.. _DPKT: https://dpkt.readthedocs.io -.. _PyShark: https://kiminewt.github.io/pyshark - .. toctree:: :maxdepth: 1 :caption: Subpackages @@ -61,7 +55,7 @@ Command Line Tool .. note:: - The ``emoji`` package (the ``cli`` extra) decorates the ``-v`` output. + The |emoji|_ package (the ``cli`` extra) decorates the ``-v`` output. Without it, importing the module warns with :class:`~pcapkit.utilities.warnings.EmojiWarning` and the CLI prints plain text instead. @@ -69,9 +63,6 @@ Command Line Tool :mod:`pcapkit.__main__` provides the CLI, merged in from the deprecated |jspcapy|_ project. -.. |jspcapy| replace:: ``jspcapy`` -.. _jspcapy: https://github.com/JarryShaw/jspcapy - .. code-block:: text usage: pcapkit-cli [-h] [-V] [-o file-name] [-f format] [-j] [-p] [-t] [-a] diff --git a/docs/source/pcapkit/interface/core.rst b/docs/source/pcapkit/interface/core.rst index 112bfd044..97ba1c12c 100644 --- a/docs/source/pcapkit/interface/core.rst +++ b/docs/source/pcapkit/interface/core.rst @@ -86,7 +86,3 @@ Extraction Engines :doc:`../foundation/engines/index` for the full engine list, what each one supports, and the installation prerequisites the third-party ones carry. - -.. _PyPCAP: https://github.com/pynetwork/pypcap -.. _pcap-ct: https://pypi.org/project/pcap-ct -.. _PyPCAPFile: https://github.com/kisom/pypcapfile diff --git a/docs/source/pcapkit/protocols/application/ngap.rst b/docs/source/pcapkit/protocols/application/ngap.rst index ed9358a22..d05545697 100644 --- a/docs/source/pcapkit/protocols/application/ngap.rst +++ b/docs/source/pcapkit/protocols/application/ngap.rst @@ -157,9 +157,6 @@ Data Models :members: :show-inheritance: -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate - .. rubric:: Footnotes .. [*] https://en.wikipedia.org/wiki/NG_Application_Protocol diff --git a/docs/source/pcapkit/protocols/internet/esp.rst b/docs/source/pcapkit/protocols/internet/esp.rst index 409bd57c2..730453a6e 100644 --- a/docs/source/pcapkit/protocols/internet/esp.rst +++ b/docs/source/pcapkit/protocols/internet/esp.rst @@ -163,9 +163,6 @@ Decryption requires the optional |cryptography|_ dependency without it; an SA that names an AES suite simply degrades to the opaque payload path, with a warning. -.. |cryptography| replace:: ``cryptography`` -.. _cryptography: https://cryptography.io - The supported set is anchored on the *mandatory to implement* algorithms of :rfc:`8221`. The tables below are the authority on what an SA may actually name; the rows marked ``yes`` are exactly the keys of @@ -175,7 +172,7 @@ registry lists is enumerated and rejected. ============================ =================== ============ ================================== Encryption :rfc:`8221` status Implemented Notes ============================ =================== ============ ================================== -``ENCR_NULL`` MUST yes :rfc:`2410`; needs no ``cryptography`` +``ENCR_NULL`` MUST yes :rfc:`2410`; needs no |cryptography|_ ``ENCR_AES_CBC`` MUST yes :rfc:`3602`; 128/192/256-bit keys ``ENCR_AES_GCM_16`` MUST yes :rfc:`4106`; 8-octet explicit IV ``ENCR_AES_GCM_8`` -- yes :rfc:`4106`, 8-octet ICV diff --git a/docs/source/pcapkit/protocols/internet/internet.rst b/docs/source/pcapkit/protocols/internet/internet.rst index 55474eb4c..d9eac23a5 100644 --- a/docs/source/pcapkit/protocols/internet/internet.rst +++ b/docs/source/pcapkit/protocols/internet/internet.rst @@ -40,7 +40,7 @@ kept as :class:`~pcapkit.protocols.misc.raw.Raw`, with one :exc:`~pcapkit.utilities.warnings.ProtocolWarning` (:issue:`1604`). The ``pyshark`` engine leaves the walk to its own parser, which does not stop there, so it may still find the upper layer past it (:issue:`1609`). So may the -``dpkt`` toolkit, which reads the chain as ``dpkt`` decodes it. So does the +``dpkt`` toolkit, which reads the chain as |dpkt|_ decodes it. So does the ``scapy`` engine behind IPv6, where Scapy dissects each extension header itself; behind IPv4, its toolkit steps over them in the octets to find TCP, up to :data:`~pcapkit.utilities.config.EXTENSION_HEADER_LIMIT` of them in a row. The diff --git a/docs/source/pcapkit/protocols/protocol.rst b/docs/source/pcapkit/protocols/protocol.rst index 93084459f..761eea084 100644 --- a/docs/source/pcapkit/protocols/protocol.rst +++ b/docs/source/pcapkit/protocols/protocol.rst @@ -130,7 +130,7 @@ starts at the first layer it dissects. One chain of extension headers is also bo its own, by :data:`~pcapkit.utilities.config.EXTENSION_HEADER_LIMIT`. The ``pyshark``, ``scapy`` and ``pypcapfile`` engines dissect with their own parsers, which do not apply the frame's layer bound (:issue:`1609`). The -``dpkt`` toolkit reads what ``dpkt`` decodes, which applies neither bound. +``dpkt`` toolkit reads what |dpkt|_ decodes, which applies neither bound. Data Models ----------- diff --git a/docs/source/pcapkit/toolkit/3rdparty.rst b/docs/source/pcapkit/toolkit/3rdparty.rst index cde0b4960..ceae7ca25 100644 --- a/docs/source/pcapkit/toolkit/3rdparty.rst +++ b/docs/source/pcapkit/toolkit/3rdparty.rst @@ -17,8 +17,6 @@ The reassembly and flow tracing adapters return the data their :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _Scapy: https://scapy.net - .. warning:: This module requires `Scapy`_ to be installed. @@ -54,8 +52,6 @@ The reassembly and flow tracing adapters return the data their :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _DPKT: https://dpkt.readthedocs.io - .. autofunction:: pcapkit.toolkit.dpkt.ipv4_reassembly .. autofunction:: pcapkit.toolkit.dpkt.ipv6_reassembly @@ -106,8 +102,6 @@ The reassembly and flow tracing adapters return the data their :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _PyShark: https://kiminewt.github.io/pyshark - .. note:: Some :mod:`pcapkit` functions may not be available with the `PyShark`_ engine, @@ -133,8 +127,6 @@ PyPCAP Tools :mod:`pcapkit.toolkit.pypcap` contains the adapters for the `PyPCAP`_ engine. -.. _PyPCAP: https://github.com/pynetwork/pypcap - .. note:: `PyPCAP`_ performs no protocol dissection, so the reassembly and flow tracing @@ -165,8 +157,6 @@ pcap-ct Tools :mod:`pcapkit.toolkit.pcap_ct` contains the adapters for the `pcap-ct`_ engine. -.. _pcap-ct: https://pypi.org/project/pcap-ct/ - .. note:: `pcap-ct`_ is an independent reimplementation of the `PyPCAP`_ interface, so @@ -205,8 +195,6 @@ The reassembly and flow tracing adapters return the data their :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _PyPCAPFile: https://github.com/kisom/pypcapfile - .. note:: `PyPCAPFile`_ has no IPv6 decoder, so :func:`~pcapkit.toolkit.pypcapfile.ipv6_reassembly` diff --git a/docs/source/pcapkit/utilities/chardet.rst b/docs/source/pcapkit/utilities/chardet.rst index c8277026f..1514e6653 100644 --- a/docs/source/pcapkit/utilities/chardet.rst +++ b/docs/source/pcapkit/utilities/chardet.rst @@ -10,8 +10,6 @@ turning the bytes of a text field into a :obj:`str`. It is shared by :meth:`ProtocolBase.decode `, which is why it lives here rather than beside either of them. -.. _chardet: https://chardet.readthedocs.io - .. autofunction:: pcapkit.utilities.chardet.detect The cache holds :data:`~pcapkit.utilities.config.DETECT_CACHE_SIZE` entries. diff --git a/docs/source/pcapkit/utilities/exceptions.rst b/docs/source/pcapkit/utilities/exceptions.rst index d0daeec3c..37ec542ea 100644 --- a/docs/source/pcapkit/utilities/exceptions.rst +++ b/docs/source/pcapkit/utilities/exceptions.rst @@ -300,6 +300,3 @@ Error Handling Utilities .. rubric:: Footnotes .. [*] See |tbtrim|_ project for a modern Pythonic implementation. - -.. |tbtrim| replace:: ``tbtrim`` -.. _tbtrim: https://github.com/gousaiyang/tbtrim diff --git a/docs/source/pcapkit/vendor/ngap.rst b/docs/source/pcapkit/vendor/ngap.rst index 69d00c96c..b11e66029 100644 --- a/docs/source/pcapkit/vendor/ngap.rst +++ b/docs/source/pcapkit/vendor/ngap.rst @@ -47,6 +47,3 @@ which is automatically generating :class:`pcapkit.const.ngap.protocol_ie.Protoco .. [*] 3GPP TS 38.413 .. [*] 3GPP TS 38.413 - -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate diff --git a/pcapkit/__init__.py b/pcapkit/__init__.py index c361fcfd8..9a58376a4 100644 --- a/pcapkit/__init__.py +++ b/pcapkit/__init__.py @@ -8,18 +8,12 @@ :mod:`pcapkit` is an independent open source library, using only `DictDumper`_ as its formatted output dumper. -.. _DictDumper: https://dictdumper.jarryshaw.me - Unlike popular PCAP file extractors, such as `Scapy`_, `DPKT`_ and `PyShark`_, :mod:`pcapkit` uses streaming strategy to read input files. That is to read frame by frame, decrease occupation on memory, as well as enhance efficiency in some way. -.. _Scapy: https://scapy.net -.. _DPKT: https://dpkt.readthedocs.io -.. _PyShark: https://kiminewt.github.io/pyshark - In :mod:`pcapkit`, all files can be described as the following components. diff --git a/pcapkit/__main__.py b/pcapkit/__main__.py index 1da1699c7..a6658f2ac 100644 --- a/pcapkit/__main__.py +++ b/pcapkit/__main__.py @@ -6,16 +6,13 @@ .. important:: - The ``emoji`` package is optional. Without it, importing this module + The |emoji|_ package is optional. Without it, importing this module warns :class:`~pcapkit.utilities.warnings.EmojiWarning` and the verbose messages print as plain text. :mod:`pcapkit.__main__` was originally the module file of |jspcapy|_, which is deprecated and merged with :mod:`pcapkit`. -.. |jspcapy| replace:: ``jspcapy`` -.. _jspcapy: https://github.com/JarryShaw/jspcapy - """ import argparse import sys diff --git a/pcapkit/const/ngap/__init__.py b/pcapkit/const/ngap/__init__.py index e228fa9d9..81f55d237 100644 --- a/pcapkit/const/ngap/__init__.py +++ b/pcapkit/const/ngap/__init__.py @@ -26,9 +26,6 @@ .. [*] 3GPP TS 38.413 .. [*] 3GPP TS 38.413 -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate - """ from pcapkit.const.ngap.procedure_code import ProcedureCode as NGAP_ProcedureCode diff --git a/pcapkit/corekit/enum.py b/pcapkit/corekit/enum.py index f8c8cf678..5a72a7d2c 100644 --- a/pcapkit/corekit/enum.py +++ b/pcapkit/corekit/enum.py @@ -123,7 +123,7 @@ class EnumLookup: an enumeration that already has members cannot be subclassed. Mixed in *before* the member type -- ``class Foo(EnumRegistry, IntFlag)``, or ``class Bar(EnumLookup, IntEnum)`` -- it contributes methods only, so - ``aenum`` still resolves the member data type from the enumeration base: + |aenum|_ still resolves the member data type from the enumeration base: ``int`` for ``aenum.IntEnum`` and ``aenum.IntFlag``, ``str`` for ``aenum.StrEnum``. That is what lets one base serve each of them, where a generated template fragment would have needed a separate rendering per @@ -138,8 +138,8 @@ class EnumLookup: become the member type itself and broken every one of these shapes at once. The methods deliberately touch only ``_member_map_``, ``_member_names_`` and - ``_value2member_map_``, which both :mod:`enum` and ``aenum`` maintain, so - nothing on this tier depends on ``aenum`` internals at all -- the + ``_value2member_map_``, which both :mod:`enum` and |aenum|_ maintain, so + nothing on this tier depends on |aenum|_ internals at all -- the ``aenum.extend_enum`` call in this module belongs to :class:`EnumRegistry`, which is the tier that mutates. @@ -390,7 +390,7 @@ def get(cls, key: 'Any', default: 'Any' = NO_DEFAULT) -> 'Self': typically :exc:`~pcapkit.utilities.exceptions.EnumValueError` from a subclass's :meth:`_validate_value` -- is re-raised as it stands rather than wrapped, so the subclass's own message survives and no second - error is constructed. Only ``aenum``'s and :mod:`enum`'s own "no + error is constructed. Only |aenum|_'s and :mod:`enum`'s own "no member carries this value" is converted, into a quiet :exc:`~pcapkit.utilities.exceptions.EnumValueError`. This is the same discrimination :meth:`EnumField.post_process @@ -521,7 +521,7 @@ def register(cls, value: 'Any', name: 'str') -> 'Self': Refuses a ``value`` that already has a member. Without this guard, ``aenum.extend_enum`` does not mint anything for an already-taken - value -- ``aenum`` treats that as a request to *alias* the existing + value -- |aenum|_ treats that as a request to *alias* the existing member under the caller's ``name`` instead, silently: the call returns the *existing* member, ``name`` becomes reachable in ``__members__`` pointing at it, and ``_member_names_`` does not grow. That is @@ -565,7 +565,7 @@ def register(cls, value: 'Any', name: 'str') -> 'Self': :meth:`~EnumLookup._validate_value` rejects ``value``. The base implementation of that hook accepts everything, so this cannot arise on a registry that does not override it. - ValueError: If ``name`` is already taken. ``aenum`` reports that + ValueError: If ``name`` is already taken. |aenum|_ reports that as :exc:`TypeError`; it is translated so that the ways one call can fail are one exception type. @@ -603,7 +603,7 @@ def _extend(cls, value: 'Any', name: 'str') -> 'Self': The member now reachable under ``name``, new or existing. Raises: - ValueError: If ``name`` is already taken. ``aenum`` reports that + ValueError: If ``name`` is already taken. |aenum|_ reports that as :exc:`TypeError`; it is translated so that the ways one call can fail are one exception type. @@ -691,7 +691,7 @@ def _unregistered_member(cls, value: 'Any', name: 'str') -> 'Self': that does. The member is constructed through ``cls._member_type_``, which - ``aenum`` sets from the enumeration base, so this serves ``int``- and + |aenum|_ sets from the enumeration base, so this serves ``int``- and ``str``-valued registries alike without either having to say which it is. diff --git a/pcapkit/corekit/fields/numbers.py b/pcapkit/corekit/fields/numbers.py index 830b1cb41..222a2ace3 100644 --- a/pcapkit/corekit/fields/numbers.py +++ b/pcapkit/corekit/fields/numbers.py @@ -693,7 +693,7 @@ def post_process(self, value: 'int | bytes', packet: 'dict[str, Any]') -> 'Stdli Notes: The registry is consulted through its constructor, which raises for a value no member and no ``_missing_`` rule accounts for. That raise - is ``aenum``'s own bare :exc:`ValueError`: not one of + is |aenum|_'s own bare :exc:`ValueError`: not one of :mod:`pcapkit.utilities.exceptions`, so a caller cannot tell it from a bug of its own, and not an :exc:`EOFError`, so :meth:`Extractor.record_frames @@ -731,7 +731,7 @@ def post_process(self, value: 'int | bytes', packet: 'dict[str, Any]') -> 'Stdli with one of :mod:`pcapkit.utilities.exceptions` has made a deliberate decision that this layer -- which sees only that the value arrived in a field of some width -- is in no position to overrule, so - an in-library error propagates unchanged and it is only ``aenum``'s + an in-library error propagates unchanged and it is only |aenum|_'s and :mod:`enum`'s "no member has this value" that becomes a pseudo-member. That is what keeps the fallback from being an unconditional ``except ValueError: pass``. @@ -761,7 +761,7 @@ def post_process(self, value: 'int | bytes', packet: 'dict[str, Any]') -> 'Stdli .. versionchanged:: 1.5.0 A value the registry has no member for resolves to a nameless - pseudo-member, where ``aenum``'s bare :exc:`ValueError` escaped + pseudo-member, where |aenum|_'s bare :exc:`ValueError` escaped (:issue:`701`). """ diff --git a/pcapkit/corekit/fields/strings.py b/pcapkit/corekit/fields/strings.py index 92d81e908..b7f6eee39 100644 --- a/pcapkit/corekit/fields/strings.py +++ b/pcapkit/corekit/fields/strings.py @@ -132,9 +132,6 @@ class StringField(_TextField[str]): the text, such as a dumper writing it back out as UTF-8, sees an ordinary :obj:`str`. A length of ``-1`` is resolved to the number of *encoded* octets. - .. |chardet| replace:: ``chardet`` - .. _chardet: https://chardet.readthedocs.io - """ def __init__(self, length: 'int | Callable[[dict[str, Any]], int]', diff --git a/pcapkit/corekit/multidict.py b/pcapkit/corekit/multidict.py index d92649c12..bcf374283 100644 --- a/pcapkit/corekit/multidict.py +++ b/pcapkit/corekit/multidict.py @@ -8,8 +8,6 @@ which are used to store multiple mappings of the same key. The implementation is inspired and based on the `Werkzeug`_ project. -.. _Werkzeug: https://werkzeug.palletsprojects.com/ - """ import copy diff --git a/pcapkit/dumpkit/common.py b/pcapkit/dumpkit/common.py index b09c53ecb..d02549156 100644 --- a/pcapkit/dumpkit/common.py +++ b/pcapkit/dumpkit/common.py @@ -233,7 +233,7 @@ def render_enum(o: 'enum.Enum | aenum.Enum') -> 'str': cannot be mistaken for a member name, since a Python identifier may not begin with a digit. - This is *not* an ``aenum`` quirk. A stdlib :class:`enum.IntFlag` built + This is *not* an |aenum|_ quirk. A stdlib :class:`enum.IntFlag` built from the same members answers ``name is None`` identically on CPython 3.14.7, so the guard belongs here rather than in a choice of enumeration library. diff --git a/pcapkit/foundation/engines/__init__.py b/pcapkit/foundation/engines/__init__.py index 00127f84b..a1574e23c 100644 --- a/pcapkit/foundation/engines/__init__.py +++ b/pcapkit/foundation/engines/__init__.py @@ -10,12 +10,6 @@ `PyShark`_, `DPKT`_, `PyPCAP`_, `pcap-ct`_ and `PyPCAPFile`_ 3rd party engine support. -.. _PyShark: https://kiminewt.github.io/pyshark -.. _DPKT: https://dpkt.readthedocs.io -.. _PyPCAP: https://github.com/pynetwork/pypcap -.. _pcap-ct: https://pypi.org/project/pcap-ct/ -.. _PyPCAPFile: https://github.com/kisom/pypcapfile - .. _PCAP-NG: https://wiki.wireshark.org/Development/PcapNg """ diff --git a/pcapkit/foundation/engines/_pcap_backend.py b/pcapkit/foundation/engines/_pcap_backend.py index 6d946bd52..a4fa447b2 100644 --- a/pcapkit/foundation/engines/_pcap_backend.py +++ b/pcapkit/foundation/engines/_pcap_backend.py @@ -26,9 +26,6 @@ It is *only* detection -- nothing here is engine behaviour -- and it imports nothing from :mod:`pcapkit`, so it cannot introduce an import cycle. -.. _PyPCAP: https://github.com/pynetwork/pypcap -.. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ import importlib import importlib.metadata @@ -124,9 +121,6 @@ def identify(module: 'ModuleType') -> 'str': Returns: :data:`PCAP_CT` or :data:`PYPCAP`. - .. _PyPCAP: https://github.com/pynetwork/pypcap - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ return PCAP_CT if hasattr(module, '_pcap') else PYPCAP diff --git a/pcapkit/foundation/engines/dpkt.py b/pcapkit/foundation/engines/dpkt.py index 47d4c3e90..2f57e3d81 100644 --- a/pcapkit/foundation/engines/dpkt.py +++ b/pcapkit/foundation/engines/dpkt.py @@ -7,8 +7,6 @@ This module contains the implementation for `DPKT`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _DPKT: https://dpkt.readthedocs.io - """ import decimal import functools @@ -80,7 +78,7 @@ class _DPKTLoopbackModule(Protocol): def Loopback(self) -> 'Type[DPKTLoopback]': ... class _DPKTPackage(Protocol): - """``dpkt``, as :class:`DPKT` reads it through :attr:`DPKT._expkg` (#1878). + """|dpkt|_, as :class:`DPKT` reads it through :attr:`DPKT._expkg` (#1878). Each attribute is a submodule, described by what the engine reads of it. mypy matches a module with a protocol at the top level only, so the @@ -131,8 +129,6 @@ class PCAPNGReader: parsed with :class:`~pcapkit.protocols.misc.pcapng.PCAPNG` as well (:issue:`1520`). - .. _DPKT: https://dpkt.readthedocs.io - Args: file: Source PCAP-NG stream, positioned at its first Section Header Block. @@ -308,15 +304,13 @@ def _pcap_reader() -> 'Type[PCAPReader]': Warns: DPKTWarning: If this `DPKT`_'s reader lacks the attributes the loop reads - -- they are private, and ``dpkt`` is not pinned. The reader then runs + -- they are private, and |dpkt|_ is not pinned. The reader then runs unchanged, and a traced PCAP record's original length is its captured length. Returns: The reader type. - .. _DPKT: https://dpkt.readthedocs.io - """ import dpkt # isort:skip @@ -396,8 +390,6 @@ def _loopback_protocol(base: 'Type[DPKTPacket]', ip: 'Type[DPKTPacket]', ip6: 'T `DPKT`_ module gets a class of its own instead of one holding stale classes. - .. _DPKT: https://dpkt.readthedocs.io - """ import dpkt # isort:skip @@ -435,14 +427,12 @@ class DPKT(EngineBase['DPKTPacket']): skips every other block without a warning. `DPKT`_'s own reader skips the Simple Packet Block too. - .. _DPKT: https://dpkt.readthedocs.io - Args: extractor: :class:`~pcapkit.foundation.extraction.Extractor` instance. """ if TYPE_CHECKING: - #: Engine extraction package, i.e. ``dpkt``. + #: Engine extraction package, i.e. |dpkt|_. _expkg: '_DPKTPackage' #: Engine extraction temporary storage. _extmp: 'Reader' @@ -477,7 +467,7 @@ def run(self) -> 'None': """Call :class:`dpkt.pcap.Reader` to extract PCAP files. This method assigns :attr:`self._expkg ` - as ``dpkt`` and :attr:`self._extmp ` + as |dpkt|_ and :attr:`self._extmp ` as an iterator from :class:`dpkt.pcap.Reader`, or from :class:`PCAPNGReader` for a PCAP-NG file. diff --git a/pcapkit/foundation/engines/pcap_ct.py b/pcapkit/foundation/engines/pcap_ct.py index e2d7d3360..af9b7cb75 100644 --- a/pcapkit/foundation/engines/pcap_ct.py +++ b/pcapkit/foundation/engines/pcap_ct.py @@ -7,8 +7,6 @@ This module contains the implementation for `pcap-ct`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ import os from typing import TYPE_CHECKING, cast @@ -108,9 +106,7 @@ class PCAP_CT(EngineBase['RawFrame']): ``pypcap`` 1.3.0), but that is a statement about the versions tested, not a guarantee from upstream. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ .. _libpcap: https://pypi.org/project/libpcap/ - .. _PyPCAP: https://github.com/pynetwork/pypcap Args: extractor: :class:`~pcapkit.foundation.extraction.Extractor` instance. @@ -222,8 +218,6 @@ def unsupported_reason(cls) -> 'Optional[str]': merely absent returns :data:`None`, since the import test reports that case in its own words. - .. _PyPCAP: https://github.com/pynetwork/pypcap - .. _pcap-ct: https://pypi.org/project/pcap-ct/ .. _libpcap: https://pypi.org/project/libpcap/ """ @@ -432,8 +426,6 @@ def read_frame(self) -> 'RawFrame': Please refer to :meth:`PCAP.read_frame ` for more operational information. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ from pcapkit.toolkit.pcap_ct import packet2dict # isort:skip ext = self._extractor diff --git a/pcapkit/foundation/engines/pypcap.py b/pcapkit/foundation/engines/pypcap.py index a979ede7d..dbed530d3 100644 --- a/pcapkit/foundation/engines/pypcap.py +++ b/pcapkit/foundation/engines/pypcap.py @@ -13,9 +13,6 @@ independent reimplementation of the same interface that installs on Python 3.12 and newer, where upstream `PyPCAP`_ does not. -.. _PyPCAP: https://github.com/pynetwork/pypcap -.. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ import os from typing import TYPE_CHECKING, cast @@ -80,9 +77,6 @@ class PyPCAP(EngineBase['RawFrame']): engine checks which distribution it got and refuses the other one rather than running under the wrong name -- see :meth:`__init__`. - .. _PyPCAP: https://github.com/pynetwork/pypcap - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - Args: extractor: :class:`~pcapkit.foundation.extraction.Extractor` instance. @@ -162,9 +156,6 @@ def unsupported_reason(cls) -> 'Optional[str]': own words and duplicating it would produce two warnings for one problem. - .. _PyPCAP: https://github.com/pynetwork/pypcap - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ return cls._reason_for(_pcap_backend.probe()) @@ -214,9 +205,6 @@ def __init__(self, extractor: 'Extractor') -> 'None': default engine with a warning, so this is a backstop rather than the usual path. - .. _PyPCAP: https://github.com/pynetwork/pypcap - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ # NOTE: the probe comes *before* ``import pcap`` so that this engine and # :class:`~pcapkit.foundation.engines.pcap_ct.PCAP_CT` behave the same way diff --git a/pcapkit/foundation/engines/pypcapfile.py b/pcapkit/foundation/engines/pypcapfile.py index a4d93952d..a076ef032 100644 --- a/pcapkit/foundation/engines/pypcapfile.py +++ b/pcapkit/foundation/engines/pypcapfile.py @@ -7,8 +7,6 @@ This module contains the implementation for `PyPCAPFile`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ import binascii import ctypes @@ -54,7 +52,7 @@ class _PCAPFileStructsModule(Protocol): def pcap_packet(self) -> 'Type[PCAPFilePacket]': ... class _PCAPFilePackage(Protocol): - """``pcapfile``, as :class:`PyPCAPFile` reads it through :attr:`PyPCAPFile._expkg` (#1878). + """|pcapfile|_, as :class:`PyPCAPFile` reads it through :attr:`PyPCAPFile._expkg` (#1878). Each attribute is a submodule, described by what the engine reads of it. mypy matches a module with a protocol at the top level only, so the @@ -97,21 +95,19 @@ class PyPCAPFile(EngineBase['PCAPFilePacket']): segment that :func:`~pcapkit.toolkit.pypcapfile.tcp_reassembly` needs in order to report an exact header/payload split. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - Args: extractor: :class:`~pcapkit.foundation.extraction.Extractor` instance. """ if TYPE_CHECKING: - #: Engine extraction package, i.e. ``pcapfile``. + #: Engine extraction package, i.e. |pcapfile|_. _expkg: '_PCAPFilePackage' #: Engine extraction temporary storage. _extmp: 'Iterator[PCAPFilePacket]' #: Data link layer protocol, from the savefile header. _dlink: 'Enum_LinkType' #: Link layer decoder for this savefile, or :data:`None` when - #: ``pcapfile`` has none for its link layer type. + #: |pcapfile|_ has none for its link layer type. _declf: 'Optional[Callable[..., Any]]' #: Number of layers to descend while decoding, i.e. link plus network. See @@ -129,7 +125,7 @@ def unsupported_reason(cls) -> 'Optional[str]': Consulted by :meth:`pcapkit.foundation.extraction.Extractor.run` *before* the import test, because the import test cannot answer this question. - ``pcapfile``'s top-level package imports perfectly well on Python 3.12 + |pcapfile|_'s top-level package imports perfectly well on Python 3.12 and newer -- it is ``pcapfile.linklayer`` that fails, and ``pcapfile.savefile`` imports it -- so a guard that only tries ``import pcapfile`` is satisfied and the :exc:`ModuleNotFoundError` then @@ -140,7 +136,7 @@ def unsupported_reason(cls) -> 'Optional[str]': The version is checked rather than the import attempted so that the answer does not depend on which of the package's submodules happens to be imported first, and so it is the same answer on a machine that has never - installed ``pcapfile`` at all. + installed |pcapfile|_ at all. Returns: A short phrase naming the limitation, or :data:`None`. @@ -196,7 +192,7 @@ def run(self) -> 'None': """Call ``pcapfile.savefile.load_savefile`` to extract PCAP files. This method assigns :attr:`self._expkg ` - as ``pcapfile`` and :attr:`self._extmp ` + as |pcapfile|_ and :attr:`self._extmp ` as an iterator over the lazily generated savefile packets. ``pcapfile.savefile.load_savefile`` loads and validates the savefile @@ -205,10 +201,10 @@ def run(self) -> 'None': ``pcapfile.savefile._read_a_packet`` with ``layers=0``, :meth:`_read_records` does *not* hand back each frame's bytes verbatim -- it hexlifies the whole frame into ASCII text and stops there, exactly as - ``pcapfile`` does for any layer at which it runs out of layers left to + |pcapfile|_ does for any layer at which it runs out of layers left to descend. It is :meth:`_decode` that un-hexlifies and then decodes each frame to :attr:`LAYERS` depth using ``pcapfile.linklayer.clookup`` -- - the very call ``pcapfile`` makes internally. Doing it this way round is + the very call |pcapfile|_ makes internally. Doing it this way round is what lets the link layer type be inspected (and reported on) *before* the first frame is decoded. @@ -219,14 +215,14 @@ def run(self) -> 'None': and/or :attr:`self.extractor._exptl ` is provided as the PyPCAPFile engine does not support such operations. - * if IPv6 reassembly is enabled, as ``pcapfile`` has no IPv6 + * if IPv6 reassembly is enabled, as |pcapfile|_ has no IPv6 decoder. - * if ``pcapfile`` has no decoder for the savefile's link layer + * if |pcapfile|_ has no decoder for the savefile's link layer type, in which case frames are left undecoded. Raises: FormatError: If the file format is not supported, i.e., not a PCAP - file. ``pcapfile`` reads libpcap savefiles only. + file. |pcapfile|_ reads libpcap savefiles only. """ from pcapkit.foundation.engines.pcap import PCAP # isort:skip @@ -330,13 +326,13 @@ def read_frame(self) -> 'PCAPFilePacket': ########################################################################## def _get_decoder(self, linktype: 'int') -> 'Optional[Callable[..., Any]]': - """Return the ``pcapfile`` link layer decoder for a link layer type. + """Return the |pcapfile|_ link layer decoder for a link layer type. Args: linktype: Link layer type code, from the savefile header. Returns: - The decoder class, or :data:`None` when ``pcapfile`` has none. + The decoder class, or :data:`None` when |pcapfile|_ has none. Warns: AttributeWarning: If no decoder is available, as frames will then be @@ -372,11 +368,11 @@ def _read_records(self, file: 'NamedStream', than its capture length, so a zero-length record drops itself and every record after it, and a truncated final record is dropped (:issue:`1571`). - This reads each record in the byte order of the header ``pcapfile`` + This reads each record in the byte order of the header |pcapfile|_ loaded, into the ``pcapfile.structs.pcap_packet`` that function builds with ``layers=0`` -- the frame hexlified, pointing at the savefile's own header -- so :meth:`_decode` and - :mod:`pcapkit.toolkit.pypcapfile` see what ``pcapfile`` hands them. + :mod:`pcapkit.toolkit.pypcapfile` see what |pcapfile|_ hands them. It stops at a tail too short for a record header, and keeps a record whose capture length runs past the data: the capture length as declared, the octets present as the frame. @@ -384,7 +380,7 @@ def _read_records(self, file: 'NamedStream', Args: file: The savefile, just past its header, where ``pcapfile.savefile.load_savefile`` leaves it. - header: The savefile header ``pcapfile`` loaded. + header: The savefile header |pcapfile|_ loaded. Yields: Undecoded savefile packets, one per record. @@ -429,7 +425,7 @@ def _decode(self, packet: 'PCAPFilePacket', frnum: 'int') -> 'PCAPFilePacket': decoded, or holds no octets at all. Warns: - AttributeWarning: If ``pcapfile`` could not decode the frame, e.g. + AttributeWarning: If |pcapfile|_ could not decode the frame, e.g. because the capture is truncated. One bad frame should not abort the extraction, but it should not pass silently either. diff --git a/pcapkit/foundation/engines/pyshark.py b/pcapkit/foundation/engines/pyshark.py index 8793f21d7..12d4a476a 100644 --- a/pcapkit/foundation/engines/pyshark.py +++ b/pcapkit/foundation/engines/pyshark.py @@ -7,8 +7,6 @@ This module contains the implementation for `PyShark`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _PyShark: https://kiminewt.github.io/pyshark - """ import sys from typing import TYPE_CHECKING, cast @@ -31,7 +29,7 @@ from pcapkit.foundation.extraction import Extractor class _PySharkPackage(Protocol): - """``pyshark``, as :class:`PyShark` reads it through :attr:`PyShark._expkg` (#1878).""" + """|pyshark|_, as :class:`PyShark` reads it through :attr:`PyShark._expkg` (#1878).""" @property def FileCapture(self) -> 'Type[PySharkFileCapture]': ... @@ -50,8 +48,6 @@ def _frame_layer(packet: 'PySharkPacket') -> 'Optional[BaseLayer]': :program:`tshark` 4.2.2 roots a Sysdig event at ``syscall`` and a Netflix custom block at ``bblog``, so those records have no ``frame`` layer. - .. _PyShark: https://kiminewt.github.io/pyshark - """ for layer in (packet.frame_info, *packet.layers): if layer.layer_name == 'frame': @@ -73,8 +69,6 @@ def _is_packet(packet: 'PySharkPacket') -> 'bool': in 4.6.9. The summary line is not used, because it differs between those versions. A record with no ``frame`` layer is not a packet either. - .. _PyShark: https://kiminewt.github.io/pyshark - """ frame = _frame_layer(packet) return frame is not None and 'encap_type' in frame.field_names @@ -88,7 +82,7 @@ class PyShark(EngineBase['PySharkPacket']): """ if TYPE_CHECKING: - #: Engine extraction package, i.e. ``pyshark``. + #: Engine extraction package, i.e. |pyshark|_. _expkg: '_PySharkPackage' #: Engine extraction temporary storage. _extmp: 'PySharkFileCapture' @@ -124,7 +118,7 @@ def unsupported_reason(cls) -> 'Optional[str]': when it is used, which without this hook escapes from :meth:`run` as a hard error rather than degrading to the default engine with a warning. - **The interpreter.** ``pyshark`` 0.6 does + **The interpreter.** |pyshark|_ 0.6 does ``asyncio.get_event_loop_policy().get_event_loop()`` at :file:`pyshark/capture/capture.py:183`, in a fresh interpreter with no running loop. Measured: 3.10 and 3.11 return a loop @@ -135,16 +129,16 @@ def unsupported_reason(cls) -> 'Optional[str]': deprecated-but-functional side of that progression, and that expectation is the one thing here that is inferred rather than observed. - **The** :program:`tshark` **binary.** ``pyshark`` is a wrapper around + **The** :program:`tshark` **binary.** |pyshark|_ is a wrapper around Wireshark's command-line tool and does no parsing itself, so it is useless - without it. The check delegates to ``pyshark``'s own + without it. The check delegates to |pyshark|_'s own ``get_process_path()`` rather than calling :func:`shutil.which`, because the two are not equivalent and ``which`` would refuse setups that work: - ``pyshark`` looks at ``tshark_path`` in its :file:`config.ini` *first*, and + |pyshark|_ looks at ``tshark_path`` in its :file:`config.ini` *first*, and then at :envvar:`PATH` on POSIX, at both Program Files directories on Windows, and at :file:`/Applications/Wireshark.app` on macOS. Asking - ``pyshark`` gets all of that for free and cannot disagree with what - ``pyshark`` will do a moment later. + |pyshark|_ gets all of that for free and cannot disagree with what + |pyshark|_ will do a moment later. Note: Deliberately **not cached**, and the cost was measured rather than @@ -164,8 +158,6 @@ def unsupported_reason(cls) -> 'Optional[str]': Returns: A short phrase naming the limitation, or :data:`None`. - .. _PyShark: https://kiminewt.github.io/pyshark - """ if sys.version_info[:2] >= cls.PYTHON_CEILING: return (f'pyshark does not support Python ' @@ -217,7 +209,7 @@ def run(self) -> 'None': """Call ``pyshark.FileCapture`` to extract PCAP files. This method assigns :attr:`self._expkg ` - as ``pyshark`` and :attr:`self._extmp ` + as |pyshark|_ and :attr:`self._extmp ` as an iterator from ``pyshark.FileCapture``. The global header is parsed and dumped first, by diff --git a/pcapkit/foundation/engines/scapy.py b/pcapkit/foundation/engines/scapy.py index cb8552645..c39dea98f 100644 --- a/pcapkit/foundation/engines/scapy.py +++ b/pcapkit/foundation/engines/scapy.py @@ -7,8 +7,6 @@ This module contains the implementation for `Scapy`_ engine support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. -.. _Scapy: https://scapy.net - .. note:: Constructing this engine imports ``scapy.all``, which is what populates @@ -17,7 +15,7 @@ One side effect is worth knowing about in advance: ``scapy.all`` loads :mod:`scapy.layers.dcerpc`, which reaches `Scapy`_'s TLS layer and there - triggers a ``CryptographyDeprecationWarning`` from ``cryptography`` about + triggers a ``CryptographyDeprecationWarning`` from |cryptography|_ about finite-field Diffie-Hellman. It is emitted by any complete registry load, not by ``scapy.all`` in particular, and it concerns a key-exchange code path :mod:`pcapkit` never executes -- but it subclasses :exc:`UserWarning`, not @@ -27,7 +25,7 @@ to emit and the consumer's to silence, on the same footing as every other category (see :mod:`pcapkit.utilities.warnings`); hiding a third-party deprecation notice from inside a constructor would suppress the only advance - warning that a future ``cryptography`` release breaks `Scapy`_'s TLS layer. + warning that a future |cryptography|_ release breaks `Scapy`_'s TLS layer. To silence it, filter it as usual:: import warnings @@ -100,8 +98,6 @@ def _read_tsoffset(options: 'bytes', endian: 'str') -> 'int': Returns: The offset (option 14), a signed count of seconds; 0 without one. - .. _Scapy: https://scapy.net - """ while len(options) >= 4: code, length = struct.unpack(f'{endian}HH', options[:4]) @@ -160,8 +156,6 @@ def _reader_type() -> 'Type[PcapReader]': VersionError: If :class:`scapy.utils.PcapNgReader` lacks a method in :data:`_READER_HOOKS`, which these subclasses override. - .. _Scapy: https://scapy.net - """ from scapy.error import warning # isort:skip from scapy.utils import EDecimal, PcapNgReader, PcapReader, RawPcapReader # isort:skip diff --git a/pcapkit/foundation/reassembly/data/data.py b/pcapkit/foundation/reassembly/data/data.py index 26ca5e29e..59009c9bf 100644 --- a/pcapkit/foundation/reassembly/data/data.py +++ b/pcapkit/foundation/reassembly/data/data.py @@ -50,7 +50,7 @@ class Completion(EnumLookup, StrEnum): state can be tested for without importing this class. :class:`pcapkit.const.pcapng.tls_key_label.TLSKeyLabel` is the same kind of string enumeration, but it is generated like its :mod:`pcapkit.const.pcapng` - siblings (:issue:`886`), so it derives from ``aenum``'s own + siblings (:issue:`886`), so it derives from |aenum|_'s own ``StrEnum`` (via :class:`~pcapkit.corekit.enum.EnumRegistry`). The properties above hold for it, as ``aenum.StrEnum`` is a :class:`str` subclass, but ``isinstance``/``issubclass`` against diff --git a/pcapkit/protocols/application/ngap.py b/pcapkit/protocols/application/ngap.py index 76b6742e0..8eb061200 100644 --- a/pcapkit/protocols/application/ngap.py +++ b/pcapkit/protocols/application/ngap.py @@ -93,9 +93,6 @@ vendor defined, so the generic conversion reports whatever ASN.1 shape the encoding declares and cannot name the fields. -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate - .. [*] https://en.wikipedia.org/wiki/NG_Application_Protocol """ diff --git a/pcapkit/protocols/internet/esp.py b/pcapkit/protocols/internet/esp.py index 3772f1b9f..db7f6d859 100644 --- a/pcapkit/protocols/internet/esp.py +++ b/pcapkit/protocols/internet/esp.py @@ -96,7 +96,7 @@ ============================ =================== ============ ================================== Encryption :rfc:`8221` status Implemented Notes ============================ =================== ============ ================================== -``ENCR_NULL`` MUST yes :rfc:`2410`; needs no ``cryptography`` +``ENCR_NULL`` MUST yes :rfc:`2410`; needs no |cryptography|_ ``ENCR_AES_CBC`` MUST yes :rfc:`3602`; 128/192/256-bit keys ``ENCR_AES_GCM_16`` MUST yes :rfc:`4106`; 8-octet explicit IV ``ENCR_AES_GCM_8`` -- yes :rfc:`4106`, 8-octet ICV @@ -155,9 +155,6 @@ * The ICV is *verified* but a failure is reported rather than raised, so that one bad packet does not abort a capture. -.. |cryptography| replace:: ``cryptography`` -.. _cryptography: https://cryptography.io - .. [*] https://en.wikipedia.org/wiki/IPsec .. versionadded:: 1.5.0 @@ -197,7 +194,7 @@ from pcapkit.protocols.protocol import ProtocolBase -#: Cached ``cryptography`` primitives, see :func:`load_cryptography`. +#: Cached |cryptography|_ primitives, see :func:`load_cryptography`. #: :data:`NotImplemented` means the import has not been attempted yet, and #: :data:`None` that it was attempted and |cryptography|_ is not installed, and #: a tuple holds the loaded primitives, so a missing dependency is not retried diff --git a/pcapkit/protocols/protocol.py b/pcapkit/protocols/protocol.py index 566673ba8..d78f0fe68 100644 --- a/pcapkit/protocols/protocol.py +++ b/pcapkit/protocols/protocol.py @@ -894,9 +894,6 @@ def decode(byte: bytes, *, encoding: 'Optional[str]' = None, as well as any other name registered with :func:`codecs.register_error` that can handle :exc:`UnicodeDecodeError`. - .. |chardet| replace:: ``chardet`` - .. _chardet: https://chardet.readthedocs.io - """ charset = encoding or detect(byte) try: diff --git a/pcapkit/toolkit/dpkt.py b/pcapkit/toolkit/dpkt.py index 1088cd930..ce3c233e4 100644 --- a/pcapkit/toolkit/dpkt.py +++ b/pcapkit/toolkit/dpkt.py @@ -9,8 +9,6 @@ :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _DPKT: https://dpkt.readthedocs.io - """ import copy import decimal @@ -96,8 +94,6 @@ class RecordTimestamp: ``type(self)(str(self))``, with the constructor's defaults for the rest (:issue:`1647`). - .. _DPKT: https://dpkt.readthedocs.io - """ __slots__ = () @@ -134,8 +130,6 @@ class Timestamp(RecordTimestamp, float): resolution: Timestamp resolution of the record, in units per second. orig_len: Original length of the packet, or :data:`None` if unknown. - .. _DPKT: https://dpkt.readthedocs.io - """ __slots__ = ('exact', 'resolution', 'orig_len') @@ -162,8 +156,6 @@ class DecimalTimestamp(RecordTimestamp, decimal.Decimal): resolution: Timestamp resolution of the record, in units per second. orig_len: Original length of the packet, or :data:`None` if unknown. - .. _DPKT: https://dpkt.readthedocs.io - """ __slots__ = ('exact', 'resolution', 'orig_len') @@ -188,8 +180,6 @@ def attach_timestamp(packet: 'Packet', timestamp: 'float') -> 'None': timestamp: Capture timestamp of the packet, as `DPKT`_'s reader yielded it beside the record's octets. - .. _DPKT: https://dpkt.readthedocs.io - """ setattr(packet, TIMESTAMP_ATTR, timestamp) @@ -236,11 +226,9 @@ def _capture_time(timestamp: 'float | Decimal') -> 'float': so can be an ulp off at a small ``tv_sec``: ``1.0036909999999999`` for ``1.003691`` (:issue:`1668`). A :class:`RecordTimestamp` is therefore read by its exact value. Any other - timestamp -- one passed by hand, or yielded by a ``dpkt`` whose reader + timestamp -- one passed by hand, or yielded by a |dpkt|_ whose reader the engine could not extend -- carries none, and is handed on as it is. - .. _DPKT: https://dpkt.readthedocs.io - """ if isinstance(timestamp, RecordTimestamp): return float(timestamp.exact) @@ -255,8 +243,6 @@ def attach_buffer(packet: 'Packet', buffer: 'bytes') -> 'None': buffer: Octets of the record the packet was parsed from, as `DPKT`_'s reader yielded them. - .. _DPKT: https://dpkt.readthedocs.io - """ setattr(packet, BUFFER_ATTR, bytes(buffer)) @@ -274,8 +260,6 @@ def packet2bytes(packet: 'Packet') -> 'bytes': serialised from a copy instead, so that `DPKT`_'s checksum and length recomputation never reaches the packet itself. - .. _DPKT: https://dpkt.readthedocs.io - """ buffer = getattr(packet, BUFFER_ATTR, None) if buffer is not None: @@ -305,13 +289,11 @@ def packet2frame(packet: 'Packet', timestamp: 'float | Decimal', *, and both it and ``orig_len`` come from the reader: :class:`~pcapkit.foundation.engines.dpkt.DPKT` reads PCAP and PCAP-NG alike into a :class:`RecordTimestamp`. A timestamp that is not one - -- a packet built by hand, or read by a ``dpkt`` whose reader the engine + -- a packet built by hand, or read by a |dpkt|_ whose reader the engine could not extend -- is read as `DPKT`_ yields it: a :class:`~decimal.Decimal` counts nanoseconds and a :obj:`float` microseconds, and the octets are taken to be whole. - .. _DPKT: https://dpkt.readthedocs.io - """ stamp = getattr(packet, TIMESTAMP_ATTR, timestamp) octets = packet2bytes(packet) @@ -356,8 +338,6 @@ def _ipv6_ext_hdrs(ipv6: 'IP6') -> 'list[Any]': otherwise the values of ``dpkt.ip6.IP6.extension_hdrs``, which ``dpkt.ip6.IP6.__len__`` falls back to in the same way. - .. _DPKT: https://dpkt.readthedocs.io - """ ext_hdrs = getattr(ipv6, 'all_extension_headers', None) if ext_hdrs: @@ -393,8 +373,6 @@ def _wire_hdr_len(packet: 'Packet', payload: 'Packet') -> 'int': wire where `DPKT`_ dropped some of its VLAN tags, which :func:`_layer2bytes` then finds (:issue:`1646`). - .. _DPKT: https://dpkt.readthedocs.io - """ if hasattr(packet, 'extension_hdrs'): return packet.__hdr_len__ + sum( @@ -430,8 +408,6 @@ def _layer2bytes(packet: 'Packet', layer: 'Packet') -> 'tuple[bytes, bool]': header followed by an 802.1Q tag, which it keeps in place of the ISL one. - .. _DPKT: https://dpkt.readthedocs.io - """ buffer = getattr(packet, BUFFER_ATTR, None) if buffer is not None: @@ -463,8 +439,6 @@ def _network(packet: 'Packet') -> 'Optional[IP | IP6]': payload of its link layer, which `DPKT`_ names ``ip`` or ``ip6`` -- a bare IP packet names neither -- or :data:`None` if it has none. - .. _DPKT: https://dpkt.readthedocs.io - """ import dpkt # isort:skip @@ -541,8 +515,6 @@ def packet2dict(packet: 'Packet', timestamp: 'float', *, data, as it keeps one `DPKT`_ cannot decode, has no header fields, so maps to an empty :obj:`dict`. - .. _DPKT: https://dpkt.readthedocs.io - """ def wrapper(packet: 'Packet') -> 'dict[str, Any]': dict_ = {} # type: dict[str, Any] @@ -591,10 +563,10 @@ def ipv4_reassembly(packet: 'Packet', timestamp: 'float', *, The header and payload are sliced out of the record octets :class:`~pcapkit.foundation.engines.dpkt.DPKT` attached to the frame, so they are the captured ones. A packet built by hand carries no such - buffer, and its octets are then ``dpkt``'s re-serialisation of it, in + buffer, and its octets are then |dpkt|_'s re-serialisation of it, in which a zeroed checksum or length comes back recomputed -- :func:`attach_buffer` is what makes them the wire's again. They are that - re-serialisation too where ``dpkt``'s own lengths misplace the layer + re-serialisation too where |dpkt|_'s own lengths misplace the layer in the record, as behind some stacks of VLAN tags (:issue:`1646`). The packet itself is left untouched either way. @@ -678,10 +650,10 @@ def ipv6_reassembly(packet: 'Packet', timestamp: 'float', *, The header and payload are sliced out of the record octets :class:`~pcapkit.foundation.engines.dpkt.DPKT` attached to the frame, so they are the captured ones. A packet built by hand carries no such - buffer, and its octets are then ``dpkt``'s re-serialisation of it, in + buffer, and its octets are then |dpkt|_'s re-serialisation of it, in which a zeroed checksum or length comes back recomputed -- :func:`attach_buffer` is what makes them the wire's again. They are that - re-serialisation too where ``dpkt``'s own lengths misplace the layer + re-serialisation too where |dpkt|_'s own lengths misplace the layer in the record, as behind some stacks of VLAN tags (:issue:`1646`). The packet itself is left untouched either way. @@ -760,8 +732,6 @@ def _tcp_flags(tcp: 'TCP') -> 'tuple[bool, bool, bool]': its position among the field's binary digits, every flag was one bit low with NS set, a SYN reading as a FIN (:issue:`1618`). - .. _DPKT: https://dpkt.readthedocs.io - """ from dpkt.tcp import TH_FIN, TH_RST, TH_SYN # isort:skip @@ -774,7 +744,7 @@ def tcp_reassembly(packet: 'Packet', timestamp: 'float', *, """Make data for TCP reassembly. A packet can be reassembled if its network layer carries a TCP layer - (:class:`dpkt.tcp.TCP`), behind any IPv6 extension headers ``dpkt`` decodes. TCP + (:class:`dpkt.tcp.TCP`), behind any IPv6 extension headers |dpkt|_ decodes. TCP in an IP-in-IP tunnel, or behind an AH or IPv6 extension header carried in IPv4, is not read. @@ -793,10 +763,10 @@ def tcp_reassembly(packet: 'Packet', timestamp: 'float', *, The header and payload are sliced out of the record octets :class:`~pcapkit.foundation.engines.dpkt.DPKT` attached to the frame, so they are the captured ones. A packet built by hand carries no such - buffer, and its octets are then ``dpkt``'s re-serialisation of it, in + buffer, and its octets are then |dpkt|_'s re-serialisation of it, in which a zeroed checksum or length comes back recomputed -- :func:`attach_buffer` is what makes them the wire's again. They are that - re-serialisation too where ``dpkt``'s own lengths misplace the layer + re-serialisation too where |dpkt|_'s own lengths misplace the layer in the record, as behind some stacks of VLAN tags (:issue:`1646`). The packet itself is left untouched either way. @@ -850,7 +820,7 @@ def tcp_traceflow(packet: 'Packet', timestamp: 'float', *, """Trace packet flow for TCP. A packet can be traced if its network layer carries a TCP layer - (:class:`dpkt.tcp.TCP`), behind any IPv6 extension headers ``dpkt`` decodes. TCP + (:class:`dpkt.tcp.TCP`), behind any IPv6 extension headers |dpkt|_ decodes. TCP in an IP-in-IP tunnel, or behind an AH or IPv6 extension header carried in IPv4, is not read. @@ -871,10 +841,10 @@ def tcp_traceflow(packet: 'Packet', timestamp: 'float', *, The header and payload are sliced out of the record octets :class:`~pcapkit.foundation.engines.dpkt.DPKT` attached to the frame, so they are the captured ones. A packet built by hand carries no such - buffer, and its octets are then ``dpkt``'s re-serialisation of it, in + buffer, and its octets are then |dpkt|_'s re-serialisation of it, in which a zeroed checksum or length comes back recomputed -- :func:`attach_buffer` is what makes them the wire's again. They are that - re-serialisation too where ``dpkt``'s own lengths misplace the layer + re-serialisation too where |dpkt|_'s own lengths misplace the layer in the record, as behind some stacks of VLAN tags (:issue:`1646`). The packet itself is left untouched either way. diff --git a/pcapkit/toolkit/pcap.py b/pcapkit/toolkit/pcap.py index 7ddd2b8d5..e11ada291 100644 --- a/pcapkit/toolkit/pcap.py +++ b/pcapkit/toolkit/pcap.py @@ -129,7 +129,7 @@ def tcp_segment(frame: 'ProtocolBase') -> 'Optional[TCPSegment]': design (:issue:`1404`), so the segment arrives as :class:`~pcapkit.protocols.misc.raw.Raw`. The ports, numbers and flags are all in the fixed header, so the segment still belongs to its flow, - as it does for ``dpkt``, :mod:`scapy` and Wireshark (:issue:`1518`). + as it does for |dpkt|_, :mod:`scapy` and Wireshark (:issue:`1518`). The header and payload are then the captured octets either side of the Data Offset. Inside a tunnel, this holds of the IP layer carrying the segment, as it does without one. diff --git a/pcapkit/toolkit/pcap_ct.py b/pcapkit/toolkit/pcap_ct.py index ce49eedc2..55c600ef4 100644 --- a/pcapkit/toolkit/pcap_ct.py +++ b/pcapkit/toolkit/pcap_ct.py @@ -6,8 +6,6 @@ :mod:`pcapkit.toolkit.pcap_ct` contains the adapters for the `pcap-ct`_ engine. -.. _pcap-ct: https://pypi.org/project/pcap-ct/ - .. important:: `pcap-ct`_ is a :mod:`ctypes` reimplementation of the `PyPCAP`_ interface over @@ -31,8 +29,6 @@ each engine names its own adapter so that a change made for one cannot quietly alter the other. -.. _PyPCAP: https://github.com/pynetwork/pypcap - """ from typing import TYPE_CHECKING @@ -71,8 +67,6 @@ def packet2chain(packet: 'bytes', *, data_link: 'Enum_LinkType') -> 'str': As `pcap-ct`_ does not dissect the packet, the chain is only ever the link layer type followed by ``Raw``, e.g. ``ETHERNET:Raw``. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ return f'{data_link.name}:Raw' @@ -93,8 +87,6 @@ def packet2dict(packet: 'bytes', timestamp: 'float', *, The mapping carries the captured bytes verbatim rather than a decoded protocol tree, since `pcap-ct`_ does not decode anything. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ return { 'timestamp': timestamp, @@ -116,8 +108,6 @@ def ipv4_reassembly(packet: 'bytes', *, count: 'int' = -1) -> 'IP_Packet[IPv4Add Raises: UnsupportedCall: Always, as `pcap-ct`_ provides no IPv4 layer to read. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ raise UnsupportedCall(f'IPv4 reassembly is not supported by the pcap-ct engine: {_NO_DISSECTION}') @@ -132,8 +122,6 @@ def ipv6_reassembly(packet: 'bytes', *, count: 'int' = -1) -> 'IP_Packet[IPv6Add Raises: UnsupportedCall: Always, as `pcap-ct`_ provides no IPv6 layer to read. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ raise UnsupportedCall(f'IPv6 reassembly is not supported by the pcap-ct engine: {_NO_DISSECTION}') @@ -148,8 +136,6 @@ def tcp_reassembly(packet: 'bytes', *, count: 'int' = -1) -> 'TCP_Packet | None' Raises: UnsupportedCall: Always, as `pcap-ct`_ provides no TCP layer to read. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ raise UnsupportedCall(f'TCP reassembly is not supported by the pcap-ct engine: {_NO_DISSECTION}') @@ -167,7 +153,5 @@ def tcp_traceflow(packet: 'bytes', timestamp: 'float', *, data_link: 'Enum_LinkT Raises: UnsupportedCall: Always, as `pcap-ct`_ provides no TCP layer to read. - .. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ raise UnsupportedCall(f'TCP flow tracing is not supported by the pcap-ct engine: {_NO_DISSECTION}') diff --git a/pcapkit/toolkit/pypcap.py b/pcapkit/toolkit/pypcap.py index 02796581b..a791d84b5 100644 --- a/pcapkit/toolkit/pypcap.py +++ b/pcapkit/toolkit/pypcap.py @@ -6,8 +6,6 @@ :mod:`pcapkit.toolkit.pypcap` contains the adapters for the `PyPCAP`_ engine. -.. _PyPCAP: https://github.com/pynetwork/pypcap - .. important:: `PyPCAP`_ is a thin :manpage:`libpcap(3)` binding: it hands back the @@ -30,8 +28,6 @@ :class:`~pcapkit.foundation.engines.pcap_ct.PCAP_CT` drives. Each engine names its own adapter, so a change made for one cannot quietly alter the other. -.. _pcap-ct: https://pypi.org/project/pcap-ct/ - """ from typing import TYPE_CHECKING @@ -70,8 +66,6 @@ def packet2chain(packet: 'bytes', *, data_link: 'Enum_LinkType') -> 'str': As `PyPCAP`_ does not dissect the packet, the chain is only ever the link layer type followed by ``Raw``, e.g. ``ETHERNET:Raw``. - .. _PyPCAP: https://github.com/pynetwork/pypcap - """ return f'{data_link.name}:Raw' @@ -92,8 +86,6 @@ def packet2dict(packet: 'bytes', timestamp: 'float', *, The mapping carries the captured bytes verbatim rather than a decoded protocol tree, since `PyPCAP`_ does not decode anything. - .. _PyPCAP: https://github.com/pynetwork/pypcap - """ return { 'timestamp': timestamp, @@ -115,8 +107,6 @@ def ipv4_reassembly(packet: 'bytes', *, count: 'int' = -1) -> 'IP_Packet[IPv4Add Raises: UnsupportedCall: Always, as `PyPCAP`_ provides no IPv4 layer to read. - .. _PyPCAP: https://github.com/pynetwork/pypcap - """ raise UnsupportedCall(f'IPv4 reassembly is not supported by the PyPCAP engine: {_NO_DISSECTION}') @@ -131,8 +121,6 @@ def ipv6_reassembly(packet: 'bytes', *, count: 'int' = -1) -> 'IP_Packet[IPv6Add Raises: UnsupportedCall: Always, as `PyPCAP`_ provides no IPv6 layer to read. - .. _PyPCAP: https://github.com/pynetwork/pypcap - """ raise UnsupportedCall(f'IPv6 reassembly is not supported by the PyPCAP engine: {_NO_DISSECTION}') @@ -147,8 +135,6 @@ def tcp_reassembly(packet: 'bytes', *, count: 'int' = -1) -> 'TCP_Packet | None' Raises: UnsupportedCall: Always, as `PyPCAP`_ provides no TCP layer to read. - .. _PyPCAP: https://github.com/pynetwork/pypcap - """ raise UnsupportedCall(f'TCP reassembly is not supported by the PyPCAP engine: {_NO_DISSECTION}') @@ -166,7 +152,5 @@ def tcp_traceflow(packet: 'bytes', timestamp: 'float', *, data_link: 'Enum_LinkT Raises: UnsupportedCall: Always, as `PyPCAP`_ provides no TCP layer to read. - .. _PyPCAP: https://github.com/pynetwork/pypcap - """ raise UnsupportedCall(f'TCP flow tracing is not supported by the PyPCAP engine: {_NO_DISSECTION}') diff --git a/pcapkit/toolkit/pypcapfile.py b/pcapkit/toolkit/pypcapfile.py index 838cd6198..76cc07b94 100644 --- a/pcapkit/toolkit/pypcapfile.py +++ b/pcapkit/toolkit/pypcapfile.py @@ -9,8 +9,6 @@ :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _PyPCAPFile: https://github.com/kisom/pypcapfile - .. important:: `PyPCAPFile`_ decodes Ethernet, IPv4, TCP and UDP and nothing else -- there @@ -139,8 +137,6 @@ def packet2timestamp(packet: 'Packet') -> 'float': off at a small ``timestamp``: ``1.0036909999999999`` for ``1.003691`` (:issue:`1668`). - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ divisor = RESOLUTION[bool(packet.header[0].ns_resolution)] return (packet.timestamp * divisor + packet.timestamp_us) / divisor @@ -172,8 +168,6 @@ def _parse_ipv4_address(value: 'Any') -> 'IPv4Address': Raises: ProtocolError: If ``value`` cannot be parsed as an IPv4 address. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ if not isinstance(value, bytes): raise ProtocolError(f'invalid PyPCAPFile IPv4 address: {value!r}') @@ -212,8 +206,6 @@ def _parse_mac_address(value: 'Any') -> 'str': Raises: ProtocolError: If ``value`` cannot be parsed as a MAC address. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ if not isinstance(value, (bytes, bytearray)): raise ProtocolError(f'invalid PyPCAPFile MAC address: {value!r}') @@ -258,8 +250,6 @@ def _maybe_unhex(value: 'bytes') -> 'bytes': acceptable here because that byte range is a small fraction of the possible values a real header or payload can take. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ try: return binascii.unhexlify(value) @@ -285,8 +275,6 @@ def ipv4_header(ipv4: 'IP') -> 'bytes': ProtocolError: If ``ipv4.src`` or ``ipv4.dst`` cannot be parsed as an IPv4 address. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ return struct.pack( '!BBHHHBBHII', @@ -330,8 +318,6 @@ def _network(packet: 'Packet') -> 'Optional[IP]': `PyPCAPFile`_ slices the header at its IHL however few octets there are, so a header cut short has fewer options than its IHL declares. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ link = packet.packet if _is_raw(link): @@ -361,8 +347,6 @@ def _ipv6_carries_tcp(packet: 'Packet') -> 'bool': (see :func:`_maybe_unhex`), so this reads the Next Header field of the raw IPv6 header and decodes nothing. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ link = packet.packet if _is_raw(link) or getattr(link, 'type', None) != Enum_EtherType.Internet_Protocol_version_6: @@ -459,8 +443,6 @@ def _transport(ipv4: 'IP') -> 'Optional[bytes]': capture taken on the sending host, keeps the rest of the frame (:issue:`1547`). - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ if ipv4.p != Enum_TransType.TCP or ipv4.off: return None @@ -629,8 +611,6 @@ def _layer2dict(layer: 'Any') -> 'dict[str, Any]': raw payload is left untouched, since nothing here establishes that it is `PyPCAPFile`_-hexlified rather than genuinely raw. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ if _is_raw(layer): raw = bytes(layer) @@ -668,8 +648,6 @@ def packet2chain(packet: 'Packet', *, data_link: 'Enum_LinkType') -> 'str': ends in ``Raw`` -- the transport segment is left undecoded on purpose, see the module notes. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ layer = packet.packet if _is_raw(layer): @@ -705,8 +683,6 @@ def packet2dict(packet: 'Packet', *, data_link: 'Enum_LinkType') -> 'dict[str, A it is un-hexed (see :func:`_maybe_unhex`) and reported as its octets, not as twice as many characters of hex text (:issue:`1667`). - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ frame = packet.packet if _is_raw(frame): @@ -777,8 +753,6 @@ def ipv6_reassembly(packet: 'Packet', *, count: 'int' = -1) -> 'IP_Packet[IPv6Ad Raises: UnsupportedCall: Always, as `PyPCAPFile`_ has no IPv6 decoder. - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ raise UnsupportedCall('IPv6 reassembly is not supported by the PyPCAPFile engine: ' "'pypcapfile' has no IPv6 decoder, so no IPv6 fragment header " @@ -813,8 +787,6 @@ def tcp_reassembly(packet: 'Packet', *, count: 'int' = -1) -> 'TCP_Packet | None See Also: :class:`pcapkit.foundation.reassembly.tcp.TCP` - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ ipv4 = _network(packet) if ipv4 is None: @@ -885,8 +857,6 @@ def tcp_traceflow(packet: 'Packet', *, data_link: 'Enum_LinkType', See Also: :class:`pcapkit.foundation.traceflow.tcp.TCP` - .. _PyPCAPFile: https://github.com/kisom/pypcapfile - """ ipv4 = _network(packet) if ipv4 is None: diff --git a/pcapkit/toolkit/pyshark.py b/pcapkit/toolkit/pyshark.py index 04ad39c88..1ff73108a 100644 --- a/pcapkit/toolkit/pyshark.py +++ b/pcapkit/toolkit/pyshark.py @@ -7,8 +7,6 @@ :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _PyShark: https://kiminewt.github.io/pyshark - .. note:: Due to the lack of functionality of `PyShark`_, some @@ -31,7 +29,7 @@ __all__ = ['packet2dict', 'tcp_traceflow', 'ENCAP_TYPE_TO_LINKTYPE', 'FILTER_NAME_TO_LINKTYPE'] -#: ``frame.encap_type`` -- Wireshark's internal ``WTAP_ENCAP_*`` number, which ``pyshark`` +#: ``frame.encap_type`` -- Wireshark's internal ``WTAP_ENCAP_*`` number, which |pyshark|_ #: exposes as ``packet.frame_info.encap_type`` -- to the matching #: :class:`~pcapkit.const.reg.linktype.LinkType` member. This is :func:`tcp_traceflow`'s primary #: link-type source; ``FILTER_NAME_TO_LINKTYPE`` below is only its fallback. @@ -51,7 +49,7 @@ #: # key := the frame.encap_type that `tshark -r out.pcap -c 1 -T pdml` then reports #: #: so each pair is one round trip through Wireshark's own ``wiretap/pcap-common.c``, in the read -#: direction ``pyshark`` actually exercises. In the source, the trailing comment on each line +#: direction |pyshark|_ actually exercises. In the source, the trailing comment on each line #: names the ``editcap -T`` encapsulation that produced it. #: #: What the sweep does **not** cover, since that bounds this table: of the 226 encapsulations @@ -243,7 +241,7 @@ #: cannot be mapped at any size: ``user_dlt`` (16 DLTs), ``bluetooth`` (6), ``usb`` (5), #: ``usbll`` (4), ``raw`` (3), and ``arcnet``, ``gfp``, ``i2c``, ``lapd``, ``mtp2``, #: ``netanalyzer``, ``null``, ``ppp``, ``sll``, ``wlan``, ``wpan`` (2 each). -#: * **Pseudo-protocols** -- ``fake-field-wrapper``, which ``pyshark`` reports as ``data`` and +#: * **Pseudo-protocols** -- ``fake-field-wrapper``, which |pyshark|_ reports as ``data`` and #: which was the root for 29 of the swept encapsulations (no dissector), and ``_ws.malformed``, #: the root for the one capture ``tshark`` could not parse. Neither names a link type. #: * **Unswept** -- the 69 encapsulations ``editcap -T`` would not write as pcap from an Ethernet diff --git a/pcapkit/toolkit/scapy.py b/pcapkit/toolkit/scapy.py index e528c5d78..ce8926b54 100644 --- a/pcapkit/toolkit/scapy.py +++ b/pcapkit/toolkit/scapy.py @@ -9,8 +9,6 @@ :mod:`pcapkit.foundation` counterpart consumes, or :data:`None` if the frame cannot be used for it. -.. _Scapy: https://scapy.net - .. warning:: This module requires installed `Scapy`_ engine. diff --git a/pcapkit/utilities/chardet.py b/pcapkit/utilities/chardet.py index 9718aa791..d3685836f 100644 --- a/pcapkit/utilities/chardet.py +++ b/pcapkit/utilities/chardet.py @@ -7,8 +7,6 @@ :mod:`pcapkit.utilities.chardet` wraps `chardet`_ with a bounded cache, for turning the bytes of a text field into a :obj:`str`. -.. _chardet: https://chardet.readthedocs.io - """ import functools diff --git a/pcapkit/utilities/exceptions.py b/pcapkit/utilities/exceptions.py index 4e37cca12..907035ade 100644 --- a/pcapkit/utilities/exceptions.py +++ b/pcapkit/utilities/exceptions.py @@ -10,9 +10,6 @@ .. [*] See |tbtrim|_ project for Pythonic implementation. -.. |tbtrim| replace:: ``tbtrim`` -.. _tbtrim: https://github.com/gousaiyang/tbtrim - """ import contextvars import inspect diff --git a/pcapkit/vendor/default.py b/pcapkit/vendor/default.py index 334db2143..0e0e1c5f9 100644 --- a/pcapkit/vendor/default.py +++ b/pcapkit/vendor/default.py @@ -146,9 +146,6 @@ def get_proxies() -> 'dict[str, str]': :envvar:`PCAPKIT_HTTPS_PROXY`, if any, on every call, for the proxy settings of |requests|_. - .. |requests| replace:: ``requests`` - .. _requests: https://requests.readthedocs.io - Returns: Proxy settings for |requests|_. diff --git a/pcapkit/vendor/ngap/__init__.py b/pcapkit/vendor/ngap/__init__.py index 33d8b3b61..e37f56d08 100644 --- a/pcapkit/vendor/ngap/__init__.py +++ b/pcapkit/vendor/ngap/__init__.py @@ -23,9 +23,6 @@ .. [*] 3GPP TS 38.413 .. [*] 3GPP TS 38.413 -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate - """ from pcapkit.vendor.ngap.procedure_code import ProcedureCode as NGAP_ProcedureCode diff --git a/pcapkit/vendor/ngap/procedure_code.py b/pcapkit/vendor/ngap/procedure_code.py index f00b301e8..6eed64a59 100644 --- a/pcapkit/vendor/ngap/procedure_code.py +++ b/pcapkit/vendor/ngap/procedure_code.py @@ -34,9 +34,6 @@ stops two different unrecognised keys from aliasing onto the same member -- the defect GitHub issue :issue:`880` exists to fix. -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate - """ import collections import sys diff --git a/pcapkit/vendor/ngap/protocol_ie.py b/pcapkit/vendor/ngap/protocol_ie.py index 2316b1242..a232314cb 100644 --- a/pcapkit/vendor/ngap/protocol_ie.py +++ b/pcapkit/vendor/ngap/protocol_ie.py @@ -16,9 +16,6 @@ rather than raising or minting a permanent, shared-value member -- the same GitHub issue :issue:`880` ruling, applied to the sibling registry. -.. |pycrate| replace:: ``pycrate`` -.. _pycrate: https://github.com/pycrate-org/pycrate - """ import collections import sys diff --git a/tests/project/test_docs_epilog_links_1936_unit.py b/tests/project/test_docs_epilog_links_1936_unit.py new file mode 100644 index 000000000..94baa2893 --- /dev/null +++ b/tests/project/test_docs_epilog_links_1936_unit.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +"""Pins the project links defined once in ``rst_epilog`` (#1936). + +A package with no ``py:`` objects to cross-reference (#934, #1919) is linked to +its project page as ``|pkg|_``, and every third-party project target is defined +once, in ``rst_epilog`` in :file:`docs/source/conf.py`. Sphinx appends that to +every page, so a second definition in a page or a docstring is a duplicate: an +error for a substitution, a warning for a target with another URL. + +* :class:`EpilogTests` -- the epilog defines a target for each substitution, + and a substitution for each package this module links. +* :class:`LinkedPackageTests` -- a bare literal of such a package appears only + where it names an engine, an adapter, a toolkit or a requirement. +* :class:`DefinitionTests` -- each ``|name|_`` used is defined, and no page or + docstring defines an epilog name again. + +Only the text Sphinx renders is read: ``.rst`` pages under :file:`docs/` apart +from the changelog, and docstrings and ``#:`` comments under :file:`pcapkit/`. + +""" + +from __future__ import annotations + +import ast +import io +import pathlib +import re +import tokenize +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[2] +CONF = ROOT / 'docs' / 'source' / 'conf.py' + +#: Packages linked as ``|pkg|_``. ``pcap`` is not one: two distributions install +#: it, so no one project page is its target, and it stays a literal. +LINKED = ('aenum', 'cryptography', 'dpkt', 'emoji', 'pcapfile', 'pyshark') + +#: Paths, relative to :data:`ROOT`, whose literals the scan does not read: the +#: generated constants and the vendor templates that generate them. +EXCLUDED = ( + 'pcapkit/vendor/pcapng/option_type.py', + 'pcapkit/vendor/reg/apptype/apptype.py', + 'pcapkit/const/', +) + +#: A list of literals that names engines, adapters or toolkits. +ENGINE_LIST = re.compile(r'``\w+``(?:,\s+``\w+``)*(?:,?\s+(?:and|or)\s+``\w+``)?' + r'\s+(?:engines?|toolkits?|adapters?)\b') + +#: The first cell of a simple-table row, which names an engine. +TABLE_CELL = re.compile(r'(?m)^[ \t]*``\w+``(?:[ \t]+\[[^\]]+\]_)?[ \t]{2,}\S') + +#: Literals that name engines or requirements without saying so in the words +#: :data:`ENGINE_LIST` matches, by path. +EXEMPT = { + 'docs/source/contributing/pep.rst': ( + r'``pyshark``, ``pypcap`` and\s+``pcap_ct`` disable', + ), + 'docs/source/index.rst': ( + r'``pypcap`` and ``pypcapfile`` stop at 3\.11, and ``pyshark`` at 3\.13', + r'core addons only: ``emoji``, ``cryptography``', + r'``dpkt``, ``scapy``, ``pyshark``, ``requests``', + ), +} + +TARGET = re.compile(r'(?m)^[ \t]*\.\. _([^:`\n]+):[ \t]*(\S+)[ \t]*$') +SUBST = re.compile(r'(?m)^[ \t]*\.\. \|([^|\n]+)\| replace::') +SUBST_REF = re.compile(r'(? 'str': + """A reference name as docutils compares it: case- and whitespace-folded.""" + return ' '.join(name.split()).lower() + + +def _epilog() -> 'str': + """The ``rst_epilog`` string, read from :file:`conf.py` without importing it.""" + for node in ast.parse(CONF.read_text(encoding='utf-8')).body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == 'rst_epilog' for target in node.targets): + return ast.literal_eval(node.value) + raise AssertionError('conf.py assigns no rst_epilog') + + +def _rendered(path: 'pathlib.Path') -> 'str': + """The text of *path* Sphinx renders, everything else blanked in place. + + Offsets into the result are offsets into the file, so a match maps back to + its line. A ``#:`` prefix is blanked too, so a comment reads as prose. + + """ + text = path.read_text(encoding='utf-8') + if path.suffix == '.rst': + return text + keep = set() + for node in ast.walk(ast.parse(text)): + if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) \ + and isinstance(node.value.value, str): + keep.update(range(node.lineno, node.end_lineno + 1)) + comments = {tok.start[0] for tok in tokenize.generate_tokens(io.StringIO(text).readline) + if tok.type == tokenize.COMMENT and tok.string.startswith('#:')} + lines = [] + for num, line in enumerate(text.split('\n'), 1): + if num in keep: + lines.append(line) + elif num in comments: + start = line.index('#:') + lines.append(' ' * (start + 2) + line[start + 2:]) + else: + lines.append(' ' * len(line)) + return '\n'.join(lines) + + +def _sources(*, changelog: 'bool' = False) -> 'list[pathlib.Path]': + """Every ``.rst`` under :file:`docs/` and ``.py`` under :file:`pcapkit/`.""" + pages = sorted((ROOT / 'docs').rglob('*.rst')) + if not changelog: + pages = [page for page in pages if 'changelog' not in page.relative_to(ROOT).parts] + return pages + sorted((ROOT / 'pcapkit').rglob('*.py')) + + +def _rel(path: 'pathlib.Path') -> 'str': + return path.relative_to(ROOT).as_posix() + + +def _line(text: 'str', offset: 'int') -> 'int': + return text.count('\n', 0, offset) + 1 + + +class EpilogTests(unittest.TestCase): + """``rst_epilog`` pairs each substitution with a target.""" + + def test_each_substitution_has_a_target(self) -> 'None': + """``|name|_`` resolves to the target of the same name.""" + epilog = _epilog() + targets = {_norm(name): url for name, url in TARGET.findall(epilog)} + for name in SUBST.findall(epilog): + self.assertIn(_norm(name), targets, name) + for name, url in targets.items(): + self.assertTrue(url.startswith('https://'), f'{name}: {url}') + + def test_each_linked_package_has_a_substitution(self) -> 'None': + """Every name in :data:`LINKED`, and not ``pcap``.""" + names = set(SUBST.findall(_epilog())) + for name in LINKED: + self.assertIn(name, names) + self.assertNotIn('pcap', names) + + def test_no_target_is_defined_twice(self) -> 'None': + """Docutils folds case, so ``_DPKT`` and ``_dpkt`` would be one name.""" + names = [_norm(name) for name, _ in TARGET.findall(_epilog())] + self.assertEqual(sorted(names), sorted(set(names))) + + +class LinkedPackageTests(unittest.TestCase): + """A package in :data:`LINKED` is written ``|pkg|_``.""" + + def test_the_scan_reads_the_sources_it_claims_to(self) -> 'None': + """An empty or misrooted scan would pass the next test vacuously.""" + names = {_rel(path) for path in _sources()} + for expected in ('docs/source/pcapkit/foundation/engines/3rdparty.rst', + 'docs/source/index.rst', + 'pcapkit/corekit/enum.py', + 'pcapkit/foundation/engines/pypcapfile.py', + 'pcapkit/toolkit/pyshark.py'): + self.assertIn(expected, names) + self.assertFalse(any(name.startswith('docs/source/changelog/') for name in names)) + + def test_rendered_text_blanks_code_and_keeps_doc_comments(self) -> 'None': + """Only docstrings and ``#:`` comments survive :func:`_rendered`.""" + text = _rendered(ROOT / 'pcapkit' / 'toolkit' / 'pyshark.py') + self.assertIn('|pyshark|_', text) + self.assertNotIn('import ', text) + self.assertNotIn('#:', text) + + def test_no_bare_literal_of_a_linked_package(self) -> 'None': + """An ``aenum`` or ``pyshark`` literal outside the exempt contexts fails here.""" + literal = re.compile(r'``(%s)``' % '|'.join(LINKED)) + offenders = [] + for path in _sources(): + rel = _rel(path) + if any(rel == ex or rel.startswith(ex) for ex in EXCLUDED): + continue + text = _rendered(path) + spans = [m.span() for m in ENGINE_LIST.finditer(text)] + spans += [m.span() for m in TABLE_CELL.finditer(text)] + for pattern in EXEMPT.get(rel, ()): + found = [m.span() for m in re.finditer(pattern, text)] + self.assertTrue(found, f'{rel}: stale exemption {pattern!r}') + spans += found + for match in literal.finditer(text): + if not any(start <= match.start() < end for start, end in spans): + offenders.append(f'{rel}:{_line(text, match.start())}: {match.group(0)}') + self.assertEqual(offenders, [], 'write these as |pkg|_, defined in rst_epilog') + + def test_an_engine_name_stays_a_literal(self) -> 'None': + """The exemptions match engine lists and table cells, not prose.""" + self.assertTrue(ENGINE_LIST.search('The ``pyshark``, ``scapy`` and\n``pypcapfile`` engines')) + self.assertTrue(ENGINE_LIST.search('the ``dpkt`` toolkit')) + self.assertIsNone(ENGINE_LIST.search('as ``dpkt`` decodes it')) + self.assertTrue(TABLE_CELL.search('``pyshark`` [*]_ yes yes')) + self.assertIsNone(TABLE_CELL.search('``pyshark`` 0.6 builds its event loop')) + + +class DefinitionTests(unittest.TestCase): + """Each ``|name|_`` is defined once.""" + + def test_every_substitution_link_is_defined(self) -> 'None': + """In ``rst_epilog``, or in the file using it for a name that is not a project.""" + epilog = set(SUBST.findall(_epilog())) + offenders = [] + for path in _sources(): + text = _rendered(path) + local = set(SUBST.findall(text)) + for match in SUBST_REF.finditer(text): + if match.group(1) not in epilog | local: + offenders.append(f'{_rel(path)}:{_line(text, match.start())}: |{match.group(1)}|_') + self.assertEqual(offenders, []) + + def test_no_file_defines_an_epilog_name_again(self) -> 'None': + """A repeated substitution is an error; a repeated target one URL edit from a warning.""" + epilog = _epilog() + substs = set(SUBST.findall(epilog)) + targets = {_norm(name) for name, _ in TARGET.findall(epilog)} + offenders = [] + for path in _sources(changelog=True): + text = path.read_text(encoding='utf-8') + for match in SUBST.finditer(text): + if match.group(1) in substs: + offenders.append(f'{_rel(path)}:{_line(text, match.start())}: |{match.group(1)}|') + for match in TARGET.finditer(text): + if _norm(match.group(1)) in targets: + offenders.append(f'{_rel(path)}:{_line(text, match.start())}: _{match.group(1)}') + self.assertEqual(offenders, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/project/test_docs_unresolvable_roles_1919_unit.py b/tests/project/test_docs_unresolvable_roles_1919_unit.py index 8858aebc3..d6c4929c7 100644 --- a/tests/project/test_docs_unresolvable_roles_1919_unit.py +++ b/tests/project/test_docs_unresolvable_roles_1919_unit.py @@ -116,8 +116,10 @@ def test_no_role_names_an_inventoryless_package(self) -> 'None': """A reintroduced ``:mod:`pcap``` or ``:func:`~aenum.extend_enum``` fails here. Matched on the target's first dotted component, so ``pcapkit`` and - ``pcapng`` are not mistaken for ``pcap``. The plain double-backtick - literal is allowed: it is the form #934 part C prescribes for these roles. + ``pcapng`` are not mistaken for ``pcap``. The package itself is linked as + ``|pkg|_`` (#1936, pinned in :file:`test_docs_epilog_links_1936_unit.py`), + and a name below it is a plain double-backtick literal, the form #934 + part C prescribes for these roles. """ offenders = [f'{path}:{line}: :{role}:`{target}`' @@ -125,8 +127,8 @@ def test_no_role_names_an_inventoryless_package(self) -> 'None': if target.split('.')[0] in INVENTORYLESS] self.assertEqual(offenders, [], 'these roles name a package whose objects.inv has no py: ' - 'objects, so they can never resolve; per #934 part C, write ' - 'them as double-backtick literals instead') + 'objects, so they can never resolve; write the package as ' + '|pkg|_ and a name below it as a double-backtick literal') class DeadTargetTests(unittest.TestCase):