From 970796ee6fb0fb234bbb8920159264be038e5c38 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 23 Oct 2026 16:25:12 -0400 Subject: [PATCH] docs: regenerate the engine benchmark with Python 3.15 measured - python-images.txt: pin the released python:3.15-slim-bookworm (3.15.1) in place of the 3.15 rc, and make it a default-tier row. - requirements-common.txt: pin typing-extensions for 3.10; since pcapkit's compat imports it unconditionally there, the 3.10 pypcap virtualenv failed to import pcapkit and the 3.10 image no longer built. - report.py: underline the snippet headings with `-`, the level they take in docs/source/index.rst, so the snippets paste verbatim. - index.rst: replace the hand-measured macOS Test Environment/Test Results with a full examples/benchmark/run.sh run (3.10-3.15, cfabfb2d3), and drop the "3.15 inferred" and "tshark absent" footnotes from the support table now that 3.15 and pyshark are measured. - 3rdparty.rst: pyshark on 3.13 is checked, not "expected to work". - tests: exempt the generated snippets' engine/pcap literals from the rst_epilog link checks (#1936, #1948). Benchmark self-tests and the docs-pin tests pass. --- docs/source/index.rst | 183 +++++++++++++----- .../pcapkit/foundation/engines/3rdparty.rst | 3 +- examples/benchmark/README.md | 34 +--- examples/benchmark/python-images.txt | 19 +- examples/benchmark/report.py | 8 +- examples/benchmark/requirements-common.txt | 1 + .../test_docs_epilog_links_1936_unit.py | 5 + .../test_docs_pcap_module_link_1948_unit.py | 5 + 8 files changed, 171 insertions(+), 87 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index f606bf6860..358a12494a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -122,8 +122,8 @@ Engine Comparison ----------------- Being a **comprehensive** packet processor costs speed: :mod:`pcapkit`'s own -engine takes about *0.2* milliseconds per packet, slower than most of the -third-party engines below. +engine is slower than every third-party engine below except `PyShark`_, as +`Test Results`_ measures. :mod:`pcapkit` also supports alternative extraction engines for speed: `Scapy`_, `DPKT`_, `PyShark`_, `PyPCAP`_, `pcap-ct`_ and `PyPCAPFile`_, @@ -173,23 +173,19 @@ Engine Support by Python Version -------------------------------- Which engines can run at all, by interpreter. Verified by installing each engine -and extracting a capture on 3.10 through 3.14; 3.15 remains inferred. - -================== ======== ======== ======== =========== ======== =========== -Engine 3.10 3.11 3.12 3.13 3.14 3.15 [*]_ -================== ======== ======== ======== =========== ======== =========== -``pcapkit`` yes yes yes yes yes yes -``dpkt`` yes yes yes yes yes yes -``scapy`` yes yes yes yes yes yes -``pcap_ct`` yes yes yes yes yes yes -``pypcap`` yes yes no no no no -``pypcapfile`` yes yes no no no no -``pyshark`` [*]_ yes yes yes yes no no -================== ======== ======== ======== =========== ======== =========== - -.. [*] Inferred rather than measured: no 3.15 interpreter was available. -.. [*] |pyshark|_ also needs Wireshark's :program:`tshark`, which was absent here, - so only the interpreter half of each verdict in this row was verified. +and extracting a capture on 3.10 through 3.15. + +================== ======== ======== ======== ======== ======== ======== +Engine 3.10 3.11 3.12 3.13 3.14 3.15 +================== ======== ======== ======== ======== ======== ======== +``pcapkit`` yes yes yes yes yes yes +``dpkt`` yes yes yes yes yes yes +``scapy`` yes yes yes yes yes yes +``pcap_ct`` yes yes yes yes yes yes +``pypcap`` yes yes no no no no +``pypcapfile`` yes yes no no no no +``pyshark`` yes yes yes yes no no +================== ======== ======== ======== ======== ======== ======== ``pypcap`` and ``pypcapfile`` stop at 3.11, and ``pyshark`` at 3.13, for the reasons under `Engine prerequisites`_. **Python 3.11 is the last version on which @@ -202,42 +198,135 @@ Test Environment .. list-table:: - * - Operating System - - macOS 26.6.2 - * - Chip - - Apple M2 Pro - * - Memory - - 16 GB + * - Image (3.10) + - ``pcapkit-benchmark:py3.10 (sha256:6308e38915a2613714069a05800c1a14e4486ee260eddb5619727e82b971a004)`` + * - Image (3.11) + - ``pcapkit-benchmark:py3.11 (sha256:1af87ef232d03c867d0c504d75586132994a41bc4f6c3da36db0d6dd37d22fa7)`` + * - Image (3.12) + - ``pcapkit-benchmark:py3.12 (sha256:26a6605738070c5ff5670a12a41c88086745865096363e680577523e5957546a)`` + * - Image (3.13) + - ``pcapkit-benchmark:py3.13 (sha256:584e631d430bd7abdd696492229dd4b0e13a0d92687dba8968c0910ef4727b67)`` + * - Image (3.14) + - ``pcapkit-benchmark:py3.14 (sha256:6c09093cf744db366ad7f1e5fa00ccf8f6ba9e167c298acc52e87acf393d2fa3)`` + * - Image (3.15) + - ``pcapkit-benchmark:py3.15 (sha256:78d25409346c3c97f1df809a69cd021418c6146ba2c096ff00e7d97530addd53)`` + * - ``pcapkit`` revision + - ``cfabfb2d3`` + * - Python + - CPython 3.10.21, 3.11.16, 3.12.14, 3.13.15, 3.14.7, 3.15.1 + * - Architecture + - ``x86_64`` + * - Capture + - ``in.pcap`` -- 605 bytes, SHA-256 ``7aabadca49b329c6...`` + * - Packets per extraction + - 6 + * - Iterations + - 1000 timed extractions per engine per pass, the first discarded as a warm-up + * - Passes + - 3 over the whole engine set, in 8 environments: ``3.10-pcap_ct``, ``3.10-pypcap``, ``3.11-pcap_ct``, ``3.11-pypcap``, ``3.12-pcap_ct``, ``3.13-pcap_ct``, ``3.14-pcap_ct``, ``3.15-pcap_ct`` + * - tshark + - ``TShark (Wireshark) 4.0.17 (Git v4.0.17 packaged as 4.0.17-0+deb12u3).`` + * - libpcap + - ``libpcap version 1.10.3 (with TPACKET_V3)`` + +Resolved package versions: + +============== ============ =========== ============ =========== ============ ============ ============ ============ +Package 3.10-pcap_ct 3.10-pypcap 3.11-pcap_ct 3.11-pypcap 3.12-pcap_ct 3.13-pcap_ct 3.14-pcap_ct 3.15-pcap_ct +============== ============ =========== ============ =========== ============ ============ ============ ============ +``dpkt`` 1.9.8 1.9.8 1.9.8 1.9.8 1.9.8 1.9.8 1.9.8 1.9.8 +``libpcap`` 1.11.0b29 *absent* 1.11.0b29 *absent* 1.11.0b29 1.11.0b29 1.11.0b29 1.11.0b29 +``lxml`` 6.1.3 6.1.3 6.1.3 6.1.3 6.1.3 6.1.3 6.1.3 6.1.3 +``pcap-ct`` 1.3.0b3 *absent* 1.3.0b3 *absent* 1.3.0b3 1.3.0b3 1.3.0b3 1.3.0b3 +``pypcap`` *absent* 1.3.0 *absent* 1.3.0 *absent* *absent* *absent* *absent* +``pypcapfile`` 0.12.0 0.12.0 0.12.0 0.12.0 0.12.0 0.12.0 0.12.0 0.12.0 +``pypcapkit`` 1.5.0b14 1.5.0b14 1.5.0b14 1.5.0b14 1.5.0b14 1.5.0b14 1.5.0b14 1.5.0b14 +``pyshark`` 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 +``scapy`` 2.8.0 2.8.0 2.8.0 2.8.0 2.8.0 2.8.0 2.8.0 2.8.0 +============== ============ =========== ============ =========== ============ ============ ============ ============ Test Results ------------ -Measured with ``examples/legacy_smoke/test_time.py``: 1,000 timed -extractions of ``examples/captures/in.pcap`` per engine and Python version. The -first extraction is discarded as a warm-up. Values are milliseconds per packet. - -=================== ======= ======= ======= ======= ======= -Engine 3.10 3.11 3.12 3.13 3.14 -=================== ======= ======= ======= ======= ======= -``pypcapfile`` [1]_ 0.0133 0.0093 -- -- -- -``dpkt`` 0.0163 0.0113 0.0118 0.0117 0.0125 -``pypcap`` [2]_ 0.0289 0.0212 -- -- -- -``pcap_ct`` 0.0364 0.0320 0.0366 0.0322 0.0396 -``scapy`` 0.1082 0.0822 0.0877 0.0832 0.0967 -``pcapkit`` 0.2342 0.1906 0.1985 0.1920 0.2392 -``pyshark`` [3]_ 18.8158 18.7852 19.2694 20.3751 -- -=================== ======= ======= ======= ======= ======= +Measured with ``examples/benchmark/run.sh``: 1000 timed +extractions of ``examples/captures/in.pcap`` per engine and Python version. +The first extraction is discarded as a warm-up. Values are milliseconds per +packet, the median of 3 passes. + +All columns come from one run on one machine, differing in the interpreter +and nothing else, so they may be compared with each other. They may not be +compared with figures from another machine: absolute times are a property of +the host as much as of the engine. + +================ ======= ======= ======= ======= ======= ======= +Engine 3.10 3.11 3.12 3.13 3.14 3.15 +================ ======= ======= ======= ======= ======= ======= +``pypcapfile`` † 0.0320 0.0259 -- -- -- -- +``dpkt`` 0.0583 0.0465 0.0503 0.0533 0.0530 0.0523 +``pypcap`` † 0.1018 0.0890 -- -- -- -- +``pcap_ct`` 0.1134 0.0988 0.1149 0.1167 0.1178 0.2702 +``scapy`` 0.1896 0.1607 0.1882 0.1813 0.1816 0.1776 +``pcapkit`` 0.5204 0.4301 0.4869 0.4949 0.4887 0.4814 +``pyshark`` † 30.9862 31.0405 30.7182 31.4615 -- -- +================ ======= ======= ======= ======= ======= ======= The unavailable cells were attempted. They are not zeroes and must not be compared with a measured row. -.. [1] `PyPCAPFile`_ 0.12.0 imports :mod:`imp`, which Python removed in 3.12. - -.. [2] `PyPCAP`_ 1.3.0 does not build on Python 3.12+: its pre-generated - :file:`pcap.c` does not compile against that C API. - -.. [3] `PyShark`_ 0.6 cannot create the implicit event loop it expects on Python - 3.14; it ran on 3.10--3.13. +``†`` not measured, or not measured in full, for the +reason recorded at the time: + +* ``pypcapfile`` on 3.12 -- pypcapfile does not support Python 3.12; its linklayer module imports \`imp\`, removed in Python 3.12 +* ``pypcapfile`` on 3.13 -- pypcapfile does not support Python 3.13; its linklayer module imports \`imp\`, removed in Python 3.12 +* ``pypcapfile`` on 3.14 -- pypcapfile does not support Python 3.14; its linklayer module imports \`imp\`, removed in Python 3.12 +* ``pypcapfile`` on 3.15 -- pypcapfile does not support Python 3.15; its linklayer module imports \`imp\`, removed in Python 3.12 +* ``pypcap`` on 3.12, 3.13, 3.14, 3.15 -- pypcap is not installable on this interpreter, so it was not built into this image: pypcap 1.3.0 ships a pcap.c pre-generated by Cython 0.29.32, which does not compile against the Python 3.12+ C API. The engine is unsupported here rather than broken here. +* ``pyshark`` on 3.14 -- pyshark does not support Python 3.14; it builds its event loop with \`asyncio.get\_event\_loop\_policy().get\_event\_loop()\`, which raises RuntimeError from Python 3.14 when no current event loop exists +* ``pyshark`` on 3.15 -- pyshark does not support Python 3.15; it builds its event loop with \`asyncio.get\_event\_loop\_policy().get\_event\_loop()\`, which raises RuntimeError from Python 3.14 when no current event loop exists + +Test Results (Relative) +----------------------- + +Times relative to ``pcapkit``, which is 1 by definition; lower is +faster. Each ratio is taken against the baseline measured in the same +environment on the same pass, so the machine cancels out. "Observed range" is +the smallest and largest ratio actually seen across the passes. + +Ratios are pooled across every environment in the run, which means across +every Python version measured as well as both ``pcap`` providers. The +observed range therefore includes any disagreement between interpreters +about an engine; the per-version table is where that is broken out. + +============== ============= ============== ======= +Engine Relative time Observed range Samples +============== ============= ============== ======= +``pypcapfile`` 0.0613 0.0597-0.0623 12 +``dpkt`` 0.108 0.101-0.139 24 +``pypcap`` 0.203 0.198-0.209 6 +``pcap_ct`` ‡ 0.236 0.215-0.566 18 +``scapy`` ‡ 0.372 0.355-0.388 24 +``pcapkit`` 1 *baseline* 24 +``pyshark`` ¶ 63.6 58.7-72.9 18 +============== ============= ============== ======= + +The single absolute figure, for converting the ratios back: ``pcapkit`` +itself ran at **0.4879 ms per packet** on this run +(0.425-0.522 across 24 passes). Absolute times are a property of the +machine as much as of the engine, which is why every other figure here is a +ratio. + +Run-to-run spread: the median row varied by 8.9% of its ratio across +the passes and the widest by 149%. A gap between two rows that is +smaller than that is noise, not a result. + +``‡`` this run does not separate these rows: their observed +ranges overlap, so the order their medians suggest is not established. + +``¶`` measured, but on less evidence than the rows beside it. +What was collected is real; what was lost is named here rather than left to +be inferred from the pass count: + +* ``pyshark`` -- 2 individual extraction(s) failed and were discarded Installation ============ diff --git a/docs/source/pcapkit/foundation/engines/3rdparty.rst b/docs/source/pcapkit/foundation/engines/3rdparty.rst index bf6f3c5310..988c5e17ef 100644 --- a/docs/source/pcapkit/foundation/engines/3rdparty.rst +++ b/docs/source/pcapkit/foundation/engines/3rdparty.rst @@ -115,8 +115,7 @@ support, as is used by :class:`pcapkit.foundation.extraction.Extractor`. 3.10 and 3.11 return a loop silently, 3.12 returns one with a :exc:`DeprecationWarning`, and **3.14 raises** ``RuntimeError: There is no current event loop in thread 'MainThread'``. Python - 3.13 is unchecked and expected to work, being on the deprecated-but-functional - side of that change. + 3.13 works, 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 diff --git a/examples/benchmark/README.md b/examples/benchmark/README.md index f3c6bdc793..3a89c31c8a 100644 --- a/examples/benchmark/README.md +++ b/examples/benchmark/README.md @@ -28,8 +28,7 @@ pair: | Python | environments | why | | --- | --- | --- | | 3.10, 3.11 | `-pypcap` and `-pcap_ct` | both `pcap` providers install, and they are mutually exclusive | -| 3.12, 3.13, 3.14 | `-pcap_ct` only | `pypcap` cannot be installed at all, so a second virtualenv would compile for minutes to produce nothing | -| 3.15 | opt-in, `-pcap_ct` only | no released image; see below | +| 3.12 to 3.15 | `-pcap_ct` only | `pypcap` cannot be installed at all, so a second virtualenv would compile for minutes to produce nothing | A version named with `--pythons` that is not in the file is a typo rather than a column. @@ -169,7 +168,7 @@ them silently narrows the table: mechanism `/opt/install-failures/` uses for `pypcap`, one level up. - the reporting pass turns each of those files into a column of `--` **with the reason attached** -- in the per-version table's notes, and in the `Test - Environment` block as a `Python 3.15 (not measured)` row, so neither snippet + Environment` block as a `Python (not measured)` row, so neither snippet reads as a complete matrix when it is not. - only a run in which *every* version failed exits without a table, since then there is genuinely nothing to report. @@ -358,7 +357,7 @@ would undo that, and would leak machine identity into a public README. - **measure: 1.5-2 hours.** 1,000 rounds x 3 passes in every environment, and **almost all of it is** `pyshark`, which spawns a `tshark` process per extraction and is two to three orders of magnitude slower than anything else in the table. It runs in - every environment but 3.14's, where it cannot run at all. + every environment but 3.14's and 3.15's, where it cannot run at all. Ways to cut it, in descending order of how much they save: @@ -384,7 +383,7 @@ so. --repeats N passes over the whole engine set (default 3) --engines LIST comma-separated; must include 'default', which every ratio uses --pythons LIST comma-separated Python versions (default: every 'default'-tier - row of python-images.txt, i.e. 3.10 through 3.14) + row of python-images.txt, i.e. 3.10 through 3.15) --quick --rounds 50 --repeats 2 --platform P docker platform; defaults to this machine's architecture --out DIR where to write results (default examples/benchmark/out) @@ -395,26 +394,13 @@ From the repository root: `make bench` runs `run.sh` with no arguments, `make bench-quick` runs a reduced smoke matrix, and `make bench-test` runs the self-tests below. -## Python 3.15 +## A new Python version -Opt-in, and allowed to fail -- the benchmark's own choice, not CI's: the repository's CI -runs 3.15 as a blocking, supported leg (#1633). There is no released -`python:3.15-slim-bookworm`; the -pins file names a `3.15-rc` digest, which freezes one particular release candidate -and will name an older one as time passes. Measure it with: - -```shell -examples/benchmark/run.sh --pythons 3.10,3.11,3.12,3.13,3.14,3.15 -``` - -If that image cannot be built or the run fails in it, the result is a `3.15` column -of `--` with the reason attached, not a missing column and not an aborted run. - -As it happens the pinned rc **does** build and measure: every pinned requirement -installs on it, and `default`, `dpkt`, `scapy` and `pcap_ct` all produced -figures. `pypcap`, `pypcapfile` and `pyshark` come back unmeasured with the same -reasons they give on 3.14. That is a fact about one release candidate on one day -rather than a promise about 3.15, which is why the row stays opt-in. +A version joins the default matrix once its `python:-slim-bookworm` image +is released; the pins file says how to add the row. A release candidate can be +measured before then as an `opt-in` row, named with `--pythons`. If its image cannot +be built or the run fails in it, the result is a column of `--` with the reason +attached, not a missing column and not an aborted run. ## Self-tests diff --git a/examples/benchmark/python-images.txt b/examples/benchmark/python-images.txt index f8931274c5..72111a8d30 100644 --- a/examples/benchmark/python-images.txt +++ b/examples/benchmark/python-images.txt @@ -54,17 +54,14 @@ # can say the engine is unsupported on the interpreter rather than that it failed # here. # -# ## 3.15 +# ## Adding a version # -# Opt-in, and allowed to fail. There is no released `python:3.15-slim-bookworm`; -# `3.15-rc-slim-bookworm` is a release candidate and the tag moves with every rc, so -# the digest below freezes one particular candidate and will name an older rc as -# time passes -- bump it deliberately, or drop the row when 3.15 ships. Opt-in is -# this file's own tier, not CI's: the repository's CI runs 3.15 as a blocking, -# supported leg (#1633). `run.sh --pythons ...,3.15` measures it, a plain -# `run.sh` does not, and a -# version that cannot be built is reported as a missing column with the reason -# rather than dropped. +# A new interpreter gets a row once `python:-slim-bookworm` is a released +# image, pinned by the digest of that day's index. A release candidate stays out of +# the default tier: its tag moves with every rc, so a pinned rc names an older +# candidate as time passes. `opt-in` exists for such a row -- `run.sh --pythons` +# measures it, a plain `run.sh` does not -- and a version that cannot be built is +# reported as a missing column with the reason rather than dropped. # # version base image pypcap tier 3.10 python:3.10-slim-bookworm@sha256:68d914ec641a0b69267ce65184d000a2bc3a9ee2590ab702b82250ab2385735a pypcap default @@ -72,4 +69,4 @@ 3.12 python:3.12-slim-bookworm@sha256:782412e85d0f0984994c290652577d4018aff08145c85b262bb63dc0c7522254 no-pypcap default 3.13 python:3.13-slim-bookworm@sha256:ed86c82274b3c69b52fb5820f358f0bd7df0b603332063cb5c6e32bd220c3e6e no-pypcap default 3.14 python:3.14-slim-bookworm@sha256:9ab8d9c8514b44f90cf0029dd42fdd7e9e211e639c8b995304cc04568dee900f no-pypcap default -3.15 python:3.15-rc-slim-bookworm@sha256:984bee69a9c4abf834e9c458a0d81ef10332a3c9d3a8666e566f46cc23935599 no-pypcap opt-in +3.15 python:3.15-slim-bookworm@sha256:575f8bc12ec26542e0678bc6b60e385218248019d433157ec327264a78c3a853 no-pypcap default diff --git a/examples/benchmark/report.py b/examples/benchmark/report.py index 3e23a246e9..a260a65abf 100644 --- a/examples/benchmark/report.py +++ b/examples/benchmark/report.py @@ -929,8 +929,10 @@ def render_versions_rst(rows: 'Sequence[Row]', documents: 'Sequence[dict[str, An if version in measured_series} lines = [] # type: list[str] + # Underlined with `-`, the level these sections take in docs/source/index.rst, so + # the snippet pastes there verbatim; `render_rst` follows suit. lines.append('Test Results') - lines.append('~~~~~~~~~~~~') + lines.append('------------') lines.append('') lines.append(f"Measured with ``examples/benchmark/run.sh``: {first['rounds']} timed") lines.append('extractions of ``examples/captures/in.pcap`` per engine and Python version.') @@ -1044,7 +1046,7 @@ def render_rst(rows: 'Sequence[Row]', documents: 'Sequence[dict[str, Any]]', lines = [] # type: list[str] lines.append('Test Environment') - lines.append('~~~~~~~~~~~~~~~~') + lines.append('----------------') lines.append('') lines.append('.. list-table::') lines.append('') @@ -1066,7 +1068,7 @@ def render_rst(rows: 'Sequence[Row]', documents: 'Sequence[dict[str, Any]]', # about. heading = 'Test Results (Relative)' lines.append(heading) - lines.append('~' * len(heading)) + lines.append('-' * len(heading)) lines.append('') lines.append(f'Times relative to ``{BASELINE_LABEL}``, which is 1 by definition; lower is') lines.append('faster. Each ratio is taken against the baseline measured in the same') diff --git a/examples/benchmark/requirements-common.txt b/examples/benchmark/requirements-common.txt index 72404dc143..39ad3cda4c 100644 --- a/examples/benchmark/requirements-common.txt +++ b/examples/benchmark/requirements-common.txt @@ -12,6 +12,7 @@ dictdumper==0.8.4.post6 chardet==7.7.0 aenum==3.1.17 tbtrim==0.3.1 +typing-extensions==4.16.0; python_version < "3.11" # Engines available in every environment. Only `pypcap` and `pcap_ct` have to be # separated -- see requirements-pypcap.txt for why. diff --git a/tests/project/test_docs_epilog_links_1936_unit.py b/tests/project/test_docs_epilog_links_1936_unit.py index b6fde49b42..8ebef16901 100644 --- a/tests/project/test_docs_epilog_links_1936_unit.py +++ b/tests/project/test_docs_epilog_links_1936_unit.py @@ -56,6 +56,11 @@ r'``pypcap`` and ``pypcapfile`` stop at 3\.11, and ``pyshark`` at 3\.13', r'core addons only: ``emoji``, ``cryptography``', r'``dpkt``, ``scapy``, ``pyshark``, ``requests``', + # The benchmark tables, pasted from examples/benchmark/report.py, which + # keeps them plain docutils and so cannot use an rst_epilog link: a row + # with its note marker, and a note naming the row it explains. + r'(?m)^``\w+`` [\u2020\u2021\u00b6]', + r'(?m)^\* ``\w+`` (?:on 3\.\d+|--) ', ), } diff --git a/tests/project/test_docs_pcap_module_link_1948_unit.py b/tests/project/test_docs_pcap_module_link_1948_unit.py index 53f608b200..0f97cd8d23 100644 --- a/tests/project/test_docs_pcap_module_link_1948_unit.py +++ b/tests/project/test_docs_pcap_module_link_1948_unit.py @@ -74,6 +74,11 @@ 'docs/source/ext.rst': ( r'PCAP \(``cap``/``pcap``\)', ), + # Pasted from examples/benchmark/report.py, which keeps its output plain + # docutils and so cannot use the rst_epilog link. + 'docs/source/index.rst': ( + r'both ``pcap`` providers', + ), } #: Pages and modules that name the module, each of which links it.