Skip to content

docs: fix Extractor's exception mismatch and 40 phantom/stale Args: labels - #501

Merged
JarryShaw merged 3 commits into
mainfrom
fix/490-docstrings-vs-code
Sep 19, 2026
Merged

JarryShaw merged 3 commits into
mainfrom
fix/490-docstrings-vs-code

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Fixes #490, except the two ipv4.py items which ship in #499 — that file was owned by another concurrent stream.

Defect 1 — Extractor.__call__ documented an exception it does not raise

extraction.py:1019-1023 promised IterableError; :1041 raises CallableError. These are siblings, both BaseError, TypeError, and neither subclasses the other — measured, both directions False — so except IterableError: written straight from the docstring did not catch it.

The docstring was wrong, not the raise. __call__ failing is a callability problem, the message already says "is not callable", and the IterableError text was copied from the sibling __iter__ three lines above, which raises it correctly under the identical condition. So the fix corrects the docstring and leaves the behaviour alone.

Now consistent:

__call__ docstring names: CallableError
__call__ actually raises:  CallableError
IterableError subclasses CallableError?  False
CallableError subclasses IterableError?  False

Defect 2 — six phantom Args: parameters

Each confirmed against inspect.signature before editing: IPv6_Route._read_data_type_rpl (length → schema/header), PCAPNG._make_option_if_hardware (os → hardware), _make_option_isb_starttime and _make_option_isb_endtime (both ip → timestamp, byte-identical docstrings copied from the preceding _make_option_ns_dnsip6addr), _make_secrets_wireguard (data → entries), and OSPF._make_encrypt_auth (phantom auth_type removed; it takes only auth_data).

Defect 3 — 34 option/options label mismatches, not "roughly thirty"

17 in hopopt.py and 17 in ipv6_opts.py, verified by counting the removed lines in the diff. The issue estimated ~30 across 14 _read_opt_* per file; the real figure is 15 per file plus make() and _make_hopopt_options/_make_ipv6_opts. Zero phantom option: labels remain in either file.

Found by the sweep and also fixed — same class of bug, same files

  • Four timestmap → timestamp typos, in PCAPNG._make_block_epb, _make_block_isb, _make_block_packet and _read_timestamp's summary.
  • A phantom options: Block options. in PCAPNG._make_block_systemd, which takes no options parameter at all.

Independently verified, not taken on trust

I re-ran my own signature checker across all six touched classes rather than relying on the agent's: 320 methods with Args: blocks, 2 phantom parameters remaining — and both are the single case deliberately left alone below.

Deliberately NOT fixed — wants its own issue

IPv6_Route.__post_init__ documents src_ip and dst_ip, absent from the runtime signature (file, length, extension, **kwargs). This is not a copy-paste phantom like the others: an @overload stub at ipv6_route.py:353-354 genuinely declares both as typed keyword parameters. Confirmed they appear nowhere else in pcapkit/ or tests/ — only in that overload and this docstring. It reads as dead or unfinished plumbing behind a type-checking overload, and correcting the docstring alone would put it in direct contradiction with the overload. Filing separately.

Three further flags were false positives and correctly left alone: PCAPNG.__index__'s self is a deliberate Optional[PCAPNG] = None hack (the code says so) letting it work as both instance method and pseudo-classmethod, so documenting it is right; and PCAPNG.read's _read/_seek_set are documented as RST-escaped \_read:/\_seek_set:, which a naive regex misses.

Also included

docs/source/pcapkit/foundation/reassembly/tcp.rst framed the algorithm as RFC 815 "dealing with RCVBT", whereas RFC 815 presents hole descriptors as an alternative to RFC 791's bit table. Reworded in its own commit, since it is unrelated to the code changes. This was raised in #490 for judgement rather than filed as a defect.

Verification

Revert-proof. A docstring-only change cannot be proven by reverting the docstring, so I reverted the code instead — made __call__ raise IterableError, the class the old docstring claimed:

FAILED tests/foundation/test_extraction.py::ExtractorTests::test_auto_mode_call_raises_the_class_the_docstring_names
1 failed, 11 deselected

Restored: tests/foundation/test_extraction.py 12 passed, 28 subtests passed.

Blast radius test_ipv6_extension_unit.py + test_pcapng_unit.py: 89 passed, 217 subtests passed. Full tests/foundation/: 214 passed, 11 skipped.

The new test builds a real Extractor(auto=True) rather than a stand-in, and asserts extractor() raises CallableError, iter(extractor) raises IterableError, and neither is a subclass of the other — so the docstring and the raise cannot drift apart again.

No Sphinx build was run. The rendering claims rest on the automethod directives still pointing at the same, now-corrected methods, not on built HTML.

- Extractor.__call__ documented IterableError but raises CallableError
  (a sibling exception class, not a subclass); fixed the docstring to
  name the class actually raised, since the message and semantics
  ("is not callable") back CallableError.
- Fixed six Args: blocks documenting phantom parameters left over from
  copy-paste: IPv6_Route._read_data_type_rpl (length -> schema/header),
  PCAPNG._make_option_if_hardware/_make_option_isb_starttime/
  _make_option_isb_endtime/_make_secrets_wireguard, and
  OSPF._make_encrypt_auth (phantom auth_type removed).
- Renamed ~34 stale option: labels to options: across every
  _read_opt_*, make(), and _make_hopopt_options/_make_ipv6_opts in
  hopopt.py and ipv6_opts.py, matching each method's real parameter.
- Also fixed, found via the same signature sweep: a "timestmap" typo
  in three PCAPNG block-maker docstrings and one summary line, and a
  phantom `options` param on PCAPNG._make_block_systemd.
- Added a test pinning Extractor(auto=True)() to raise CallableError
  (and __iter__ to IterableError) against a real constructed instance,
  so the raise and the docstring can't drift apart unnoticed again.

Verified every touched method's Args: block against inspect.signature
with a standalone script; tests/foundation, test_ipv6_extension_unit
and test_pcapng_unit all pass.
…490)

The prose read as though RFC 815 itself uses the RCVBT bit table, when
RFC 815 actually presents hole descriptors as an alternative to the
RCVBT approach used by RFC 791. The eight algorithm steps were already
accurate; this only fixes the historical framing sentence.
@JarryShaw
JarryShaw merged commit 2c375f7 into main Sep 19, 2026
22 checks passed
@JarryShaw
JarryShaw deleted the fix/490-docstrings-vs-code branch September 19, 2026 03:24
JarryShaw added a commit that referenced this pull request Sep 20, 2026
…#519)

Closes #519.

#501 fixed one wrong exception name and forty stale `Args:` labels by hand.
#519 showed the class was not exhausted. Re-deriving its census turned up one
finding that is not a docstring defect at all, plus a third class the issue
never mentions.

`TCP._read_mode_sack` documented `ProtocolError: If length is **NOT** multiply
of 8 plus 2` and never checked it. The tempting reading -- and the first one
taken here -- is that the clause was stale, like the other five phantoms. It is
the opposite, and three things settle it: RFC 2018 gives SACK a 2-octet header
followed by 8-octet edge pairs; both neighbouring readers validate their own
lengths, `_read_mode_sackpmt` on `!= 2` and `_read_mode_echo` on `!= 6`, with
the same message; and nothing upstream enforced it either, since the schema's
`ListField` consumes as many whole 8-octet items as it finds and ignores the
tail. So the docstring was right and the code was wrong.

- tcp.py: add the `(length - 2) % 8` check to `_read_mode_sack`, raising
  `ProtocolError` with the message its two siblings already use.
- httpv2.py: drop the `Raises: ProtocolError` clause from the five
  `_read_http_*` readers that cannot raise it. `_read_http_none` shows the
  mechanism -- its `raise` is commented out and replaced by a `ProtocolWarning`
  on the next line. All 11 `_read_http_*` methods carried the clause; the 6
  that genuinely raise keep it.
- tcp.py, twice: un-indent a `Returns:` header that sat inside the `Args:`
  block. This is the class the issue missed, and it loses documentation rather
  than merely misnaming it -- napoleon parses the header as a parameter, so the
  page rendered `:param Returns:` and carried no `:returns:` at all. Measured
  through `GoogleDocstring` directly, before and after.
- frame.py, schema/application/httpv2.py, schema/misc/pcapng.py: correct four
  `Args:` labels naming a parameter that does not exist. None of the four takes
  `**kwargs`, so each is a hard `TypeError`, not a cosmetic slip:
  `Frame.register(code=..., module=IPv4)` really does raise `TypeError: got an
  unexpected keyword argument 'module'`, while `protocol=` is accepted.
- hip.py: document the parameters `_make_param_reg_response` and
  `_make_param_route_dst` omit, in the wording their own siblings
  `_make_param_reg_failed` and `_make_param_route_via` already use.
- tests: `test_tcp_sack_length_unit.py` pins the length rule, accepted and
  rejected cases both. `test_docstring_contract.py` walks every function under
  `pcapkit/` and checks each documented name against the real signature, each
  documented exception against the real `raise` statements, and each section
  header against its indentation. It imports no pcapkit, reading source under
  its own root, so no editable install can shadow what it measures.

Census re-derived at this base rather than taken from the issue, which counted
against an older one:

    phantom Raises:                  6   -> 0 remaining
    Args: naming a missing parameter 13  -> 9 remaining, all allowlisted
    swallowed section headers        3   -> 1 remaining, allowlisted

The `Args:` figure is 13 here and was 14 when #519 was written: #511 retired the
IPX socket scrape and renamed that crawler's `soup` parameter to `data`,
incidentally fixing one. The nine that remain are in files owned by other
in-flight changes and are recorded in `KNOWN_DEFECTS` with a reason each, under
a test asserting every one still reproduces -- so the list cannot rot into a
description of bugs that are gone. That test has already earned its keep: it
failed on exactly the IPX entry during the rebase, which is how the stale entry
was found and removed.

Be clear about what is provable. A docstring label is not executed, so the
corrections cannot fail a test individually -- the contract test is what pins
them, by deriving the answer from the code instead of snapshotting today's
wrongness. Verified by reintroducing each class into a scratch tree: baseline
exit 0, phantom `Raises:` exit 1, bad `Args:` label exit 1, re-indented
`Returns:` exit 1. The two `Raises:` assertions are complementary rather than
redundant, and `_read_http_none` proves it -- `any(header.flags)` in its body
makes the conservative reachability check judge it possibly-raising, so only the
commented-out-raise check catches it. Measured: reachability exit 0, commented
raise exit 1.

The SACK change is the one with a real behavioural fails-before. Lengths 3, 11,
14, 17, 19 and 25 all parsed clean on `c8fd97bcd` with the SACK option present
in `tcp.info['options']`, and all six now raise `ProtocolError: TCP: [OptNo 5]
invalid format`.

Two things deliberately not done. All 12 functions carrying both `**kwargs` and
an `Args:` section without documenting `**kwargs` are in hip.py -- 438 of 450
document it elsewhere -- and that set includes the two siblings this change
copies its wording from, so fixing 4 of the 12 is what would make hip.py
inconsistent. And the SACK check implements exactly the documented rule, not
RFC 2018's one-to-four block bound; `length=2` is degenerate and still parses,
recorded in the test as a decision.

Tests: 5 passed / 10 subtests (contract), 3 passed / 10 subtests (SACK),
25 passed / 19 subtests (tier guard), 81 passed / 73 subtests (transport and
schema units, unchanged by the new check).

An independent second scanner, written from scratch against the same baseline,
reproduced all three counts (6 / 13 / 3) and the residuals (0 / 9 / 1), and
turned up three things now recorded in the test module:

- `_documented_names` justified its relative-indent measurement by saying
  `__doc__` is dedented at compile time. True of `__doc__` and irrelevant here,
  since this module reads `ast.get_docstring(..., clean=False)`, which preserves
  raw source indentation -- measured [0, 8, 12, 12] against [0, 0, 4, 4] for the
  dedented forms. The implementation was right for a different reason (nesting
  depth moves the absolute column); the rationale is corrected rather than left
  wrong in a checker for wrong rationales.
- `_raised_names` resolves only `Name` and `Attribute` raise targets, and
  `ast.walk` attributes a nested `def`'s raise to the enclosing function. Both
  can only miss a phantom, never fail a correct docstring, and both are now
  documented with their measurements: 856 `Name` + 2 `Attribute` + 0 `Subscript`
  across 858 raise targets, and 11 documented functions with a nested `def`, 3
  raising inside it. The second is the correct answer rather than a gap --
  `_read_param_locator_set` documents `ProtocolError` and raises none itself,
  but calls a `_read_locator` helper that does.
- One reported defect was a false positive and is recorded as a trap:
  `Raw.__post_init__` documents `error` and `alias`, has neither in its
  signature, and assigns a local `alias` -- but forwards `**kwargs` to `read`,
  which declares both as keyword-only and uses them. Reporting it would ask for
  correct documentation to be deleted.
JarryShaw added a commit that referenced this pull request Sep 20, 2026
…#519) (#535)

Closes #519.

#501 fixed one wrong exception name and forty stale `Args:` labels by hand.
#519 showed the class was not exhausted. Re-deriving its census turned up one
finding that is not a docstring defect at all, plus a third class the issue
never mentions.

`TCP._read_mode_sack` documented `ProtocolError: If length is **NOT** multiply
of 8 plus 2` and never checked it. The tempting reading -- and the first one
taken here -- is that the clause was stale, like the other five phantoms. It is
the opposite, and three things settle it: RFC 2018 gives SACK a 2-octet header
followed by 8-octet edge pairs; both neighbouring readers validate their own
lengths, `_read_mode_sackpmt` on `!= 2` and `_read_mode_echo` on `!= 6`, with
the same message; and nothing upstream enforced it either, since the schema's
`ListField` consumes as many whole 8-octet items as it finds and ignores the
tail. So the docstring was right and the code was wrong.

- tcp.py: add the `(length - 2) % 8` check to `_read_mode_sack`, raising
  `ProtocolError` with the message its two siblings already use.
- httpv2.py: drop the `Raises: ProtocolError` clause from the five
  `_read_http_*` readers that cannot raise it. `_read_http_none` shows the
  mechanism -- its `raise` is commented out and replaced by a `ProtocolWarning`
  on the next line. All 11 `_read_http_*` methods carried the clause; the 6
  that genuinely raise keep it.
- tcp.py, twice: un-indent a `Returns:` header that sat inside the `Args:`
  block. This is the class the issue missed, and it loses documentation rather
  than merely misnaming it -- napoleon parses the header as a parameter, so the
  page rendered `:param Returns:` and carried no `:returns:` at all. Measured
  through `GoogleDocstring` directly, before and after.
- frame.py, schema/application/httpv2.py, schema/misc/pcapng.py: correct four
  `Args:` labels naming a parameter that does not exist. None of the four takes
  `**kwargs`, so each is a hard `TypeError`, not a cosmetic slip:
  `Frame.register(code=..., module=IPv4)` really does raise `TypeError: got an
  unexpected keyword argument 'module'`, while `protocol=` is accepted.
- hip.py: document the parameters `_make_param_reg_response` and
  `_make_param_route_dst` omit, in the wording their own siblings
  `_make_param_reg_failed` and `_make_param_route_via` already use.
- tests: `test_tcp_sack_length_unit.py` pins the length rule, accepted and
  rejected cases both. `test_docstring_contract.py` walks every function under
  `pcapkit/` and checks each documented name against the real signature, each
  documented exception against the real `raise` statements, and each section
  header against its indentation. It imports no pcapkit, reading source under
  its own root, so no editable install can shadow what it measures.

Census re-derived at this base rather than taken from the issue, which counted
against an older one:

    phantom Raises:                  6   -> 0 remaining
    Args: naming a missing parameter 13  -> 9 remaining, all allowlisted
    swallowed section headers        3   -> 1 remaining, allowlisted

The `Args:` figure is 13 here and was 14 when #519 was written: #511 retired the
IPX socket scrape and renamed that crawler's `soup` parameter to `data`,
incidentally fixing one. The nine that remain are in files owned by other
in-flight changes and are recorded in `KNOWN_DEFECTS` with a reason each, under
a test asserting every one still reproduces -- so the list cannot rot into a
description of bugs that are gone. That test has already earned its keep: it
failed on exactly the IPX entry during the rebase, which is how the stale entry
was found and removed.

Be clear about what is provable. A docstring label is not executed, so the
corrections cannot fail a test individually -- the contract test is what pins
them, by deriving the answer from the code instead of snapshotting today's
wrongness. Verified by reintroducing each class into a scratch tree: baseline
exit 0, phantom `Raises:` exit 1, bad `Args:` label exit 1, re-indented
`Returns:` exit 1. The two `Raises:` assertions are complementary rather than
redundant, and `_read_http_none` proves it -- `any(header.flags)` in its body
makes the conservative reachability check judge it possibly-raising, so only the
commented-out-raise check catches it. Measured: reachability exit 0, commented
raise exit 1.

The SACK change is the one with a real behavioural fails-before. Lengths 3, 11,
14, 17, 19 and 25 all parsed clean on `c8fd97bcd` with the SACK option present
in `tcp.info['options']`, and all six now raise `ProtocolError: TCP: [OptNo 5]
invalid format`.

Two things deliberately not done. All 12 functions carrying both `**kwargs` and
an `Args:` section without documenting `**kwargs` are in hip.py -- 438 of 450
document it elsewhere -- and that set includes the two siblings this change
copies its wording from, so fixing 4 of the 12 is what would make hip.py
inconsistent. And the SACK check implements exactly the documented rule, not
RFC 2018's one-to-four block bound; `length=2` is degenerate and still parses,
recorded in the test as a decision.

Tests: 5 passed / 10 subtests (contract), 3 passed / 10 subtests (SACK),
25 passed / 19 subtests (tier guard), 81 passed / 73 subtests (transport and
schema units, unchanged by the new check).

An independent second scanner, written from scratch against the same baseline,
reproduced all three counts (6 / 13 / 3) and the residuals (0 / 9 / 1), and
turned up three things now recorded in the test module:

- `_documented_names` justified its relative-indent measurement by saying
  `__doc__` is dedented at compile time. True of `__doc__` and irrelevant here,
  since this module reads `ast.get_docstring(..., clean=False)`, which preserves
  raw source indentation -- measured [0, 8, 12, 12] against [0, 0, 4, 4] for the
  dedented forms. The implementation was right for a different reason (nesting
  depth moves the absolute column); the rationale is corrected rather than left
  wrong in a checker for wrong rationales.
- `_raised_names` resolves only `Name` and `Attribute` raise targets, and
  `ast.walk` attributes a nested `def`'s raise to the enclosing function. Both
  can only miss a phantom, never fail a correct docstring, and both are now
  documented with their measurements: 856 `Name` + 2 `Attribute` + 0 `Subscript`
  across 858 raise targets, and 11 documented functions with a nested `def`, 3
  raising inside it. The second is the correct answer rather than a gap --
  `_read_param_locator_set` documents `ProtocolError` and raises none itself,
  but calls a `_read_locator` helper that does.
- One reported defect was a false positive and is recorded as a trap:
  `Raw.__post_init__` documents `error` and `alias`, has neither in its
  signature, and assigns a local `alias` -- but forwards `**kwargs` to `read`,
  which declares both as keyword-only and uses them. Reporting it would ask for
  correct documentation to be deleted.
@JarryShaw JarryShaw added the docs Pull requests that change documentation only (docs: subject prefix) label Sep 22, 2026
JarryShaw added a commit that referenced this pull request Oct 3, 2026
…issue (#719)

- Nine citations in tests/ called a pull request "GitHub issue"
  (#921, #764, #906, #815, #936, #721, #501, #983) or lumped PR #428 in
  with issue #425; each now names the right kind.
- test_dispatch_default_resolution_unit: issue #425 reported the registry
  leak and PR #428 fixed it, so the sentence says "reported" and "fixed"
  instead of crediting the issue with the fix.
- Prose only: docstrings and comments, no assertion or logic touched.
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Pull requests that change documentation only (docs: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

docs: Extractor.__call__ documents IterableError but raises CallableError, plus seven Args: blocks naming parameters that do not exist

1 participant