Conversation
…#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.
|
Pushed an update ( It reproduced all three counts (6 phantom
Local: contract 5 passed / 10 subtests, SACK 3 passed / 10 subtests, tier guard 25 passed / 19 subtests, all exit 0. |
|
✅ GOOD TO MERGE — the one behavioral fix (SACK length validation) independently fails-before/passes-after for all 6 invalid lengths with exit codes verified directly, the swallowed- |
Detailed review (independent verification, falsify-not-bless)Reviewed at head 1. The one real behavioral fix — SACK length validation, independently reproduced.
2. Honesty of scope. The PR states outright that the docstring corrections "cannot fail a test individually" and separately claims real fails-before coverage only for the SACK check, with its own reproduction numbers. It does not imply broader behavioral coverage than it has. Confirmed honest. 3. Counts re-derived independently, then reconciled against the PR's own explanation.
4.
5. #525 — confirmed present, not raised as a finding, per instruction. The union assertion ( What remains unverifiedThe broader VerdictAll load-bearing claims independently reproduce, including a full, honest reconciliation of the 13-vs-14 |
Closes #530. - `_hopopt_option_length` quoted "the length of the Option Data field of this option, in octets" and attributed it to RFC 8200 section 4.3. That sentence is the `Opt Data Len` definition from section 4.2; section 4.3 defines no `Opt Data Len` at all and its only length field is `Hdr Ext Len`, the whole header in 8-octet units. Now cites 4.2 for the quote and keeps 4.3 as the header this class implements, matching the two-part treatment #528 landed for the IPv6-Opts sibling. - `tests/test_docstring_contract.py` grows a fourth property: every verbatim RFC 8200 sentence this package quotes must be introduced by a citation naming the section that contains it. Keyed on the quote rather than the file, so it covers the next copy-paste of these paragraphs. - Drops the rotted `KNOWN_DEFECTS` entry for `pcapkit/vendor/ipx/packet.py`, whose `process` now takes and documents `data`. Unrelated to #530: #524 renamed the parameter and #535 added the rot guard four commits later, so `main` has been red on that subtest since the guard landed. No behavioural change: the docstring is prose, and `_hopopt_option_length` still returns `schema_len + 2`. Verified on .venv python 3.14.7 with PYTHONSAFEPATH=1 and pcapkit.__file__ asserted inside the worktree. The new check fails on unfixed hopopt.py (1 defect) and passes with the fix (0). Suite now fully green, exit code read from a file rather than a pipe: 66 passed, 449 subtests passed, EXIT=0.
…#530) (#538) Closes #530. - `_hopopt_option_length` quoted "the length of the Option Data field of this option, in octets" and attributed it to RFC 8200 section 4.3. That sentence is the `Opt Data Len` definition from section 4.2; section 4.3 defines no `Opt Data Len` at all and its only length field is `Hdr Ext Len`, the whole header in 8-octet units. Now cites 4.2 for the quote and keeps 4.3 as the header this class implements, matching the two-part treatment #528 landed for the IPv6-Opts sibling. - `tests/test_docstring_contract.py` grows a fourth property: every verbatim RFC 8200 sentence this package quotes must be introduced by a citation naming the section that contains it. Keyed on the quote rather than the file, so it covers the next copy-paste of these paragraphs. - Drops the rotted `KNOWN_DEFECTS` entry for `pcapkit/vendor/ipx/packet.py`, whose `process` now takes and documents `data`. Unrelated to #530: #524 renamed the parameter and #535 added the rot guard four commits later, so `main` has been red on that subtest since the guard landed. No behavioural change: the docstring is prose, and `_hopopt_option_length` still returns `schema_len + 2`. Verified on .venv python 3.14.7 with PYTHONSAFEPATH=1 and pcapkit.__file__ asserted inside the worktree. The new check fails on unfixed hopopt.py (1 defect) and passes with the fix (0). Suite now fully green, exit code read from a file rather than a pipe: 66 passed, 449 subtests passed, EXIT=0.
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 at the current base turned up one finding that is not a docstring defect at all, plus a third defect class the issue never mentions.The one behavioural change, and why it is not a docstring deletion
TCP._read_mode_sackdocumentedProtocolError: If length is **NOT** multiply of 8 plus 2and never checked it. The tempting reading — and the first one taken while working this — is that the clause was stale, like the other five phantoms, and should be deleted. It is the opposite. Three things settle it:2018gives the SACK option a 2-octet header followed by 8-octet left/right edge pairs, so a well-formed length really is8n + 2._read_mode_sackpmtonlength != 2,_read_mode_echoonlength != 6, with the same message._read_mode_sackwas the one option reader documenting a length rule it never enforced.ListField(length=lambda pkt: pkt['length'] - 2, item_type=SchemaField(length=8, schema=SACKBlock)), which consumes as many whole 8-octet items as it finds and silently ignores the tail.So the docstring was right and the code was wrong. Inferring the behaviour from a neighbouring docstring — inside an issue about docstrings not matching behaviour — is exactly the trap here, and fuzzing the real parse path is what caught it.
Re-derived census
Taken at this base, not from the issue, which counted against an older one:
c8fd97bcdRaises:Args:naming a parameter that does not existThe
Args:figure is 13 here and was 14 when #519 was written: #511 retired the IPX socket scrape and renamed that crawler'ssoupparameter todata, incidentally fixing one. The issue's own figure of 11 undercounted by three.The nine that remain sit in files owned by other in-flight changes (#506, #524, #527, #531, and the registry docstring change) and are recorded in
KNOWN_DEFECTSwith 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.What can and cannot fail a test — both framings, honestly
The docstring corrections cannot fail a test individually. A label is not executed, so there is no fails-before to show for them and none is manufactured here. What pins them is
tests/test_docstring_contract.py, which derives the answer from the code rather than snapshotting today's wrongness — so a docstring written next year is checked next year. Verified by reintroducing each class into a scratch tree:Two of those are not merely cosmetic, and both were measured rather than argued:
Returns:loses documentation from the built page. Run through napoleon'sGoogleDocstringdirectly, the over-indented form yields:param Returns:and no:returns:at all; the fixed form yields:returns:.napoleon_include_private_with_doc = Trueindocs/source/conf.py, so these private methods do publish.Args:mismatches are hardTypeErrors. None of the four functions takes**kwargs.Frame.register(code=..., module=IPv4)really does raiseTypeError: Frame.register() got an unexpected keyword argument 'module', whileprotocol=is accepted.The SACK check can and does fail before the fix. Lengths 3, 11, 14, 17, 19 and 25 all parsed clean on
c8fd97bcdwith the SACK option present intcp.info['options']; all six now raiseProtocolError: TCP: [OptNo 5] invalid format. Independently reproduced in a throwaway worktree atorigin/mainwith only the new test file copied in: exit 1 withSUBFAILEDfor all six lengths, against exit 0 on this branch.The two
Raises:assertions are complementary, not redundant, and_read_http_noneproves 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 withhttpv2.py:417 _read_http_none() documents 'ProtocolError' but its only raise is commented out. Delete either test and one of the six phantoms stops being covered.Deliberately not done
**kwargscompleteness in hip.py. All 12 functions carrying both**kwargsand anArgs:section without documenting**kwargsare in hip.py; 438 of 450 document it elsewhere. That set of 12 includes_make_param_reg_failedand_make_param_route_via, the very siblings this change copies its wording from — so fixing 4 of the 12 is what would make hip.py inconsistent. Left whole, reported rather than half-fixed.(length - 2) % 8 == 0.length=2is degenerate and still parses; five blocks or more cannot be expressed at all, since the 4-bit data offset caps the options area at 40 octets. Recorded in the test as a decision rather than quietly tightened past the docstring.Args:mismatches atpcapkit/vendor/ipx/packet.py, fourpcapkit/vendor/mh/*,pcapkit/vendor/pcapng/option_type.py,pcapkit/vendor/vlan/priority_level.py,pcapkit/foundation/registry/foundation.py,pcapkit/protocols/internet/ipv4.py:1212; and the swallowedReturns:atpcapkit/protocols/internet/ipv4.py:1550.ProtocolErrorfrom_read_mode_sackorFieldValueErrorfromListField.unpack, and they are siblings so no singleexceptcatches both. The test asserts the union, recording the problem rather than solving it. One correction to the original write-up: a deliberate attempt to force theFieldValueErrorpath by runningtests/protocols/schema/first did not reproduce it, so the trigger is not pinned down and the test docstring no longer claims it is.Tests
Both new files are unit tier by the guard's own path rule, read no capture under
examples/captures/, and need no_tiers.pyregistration.test_docstring_contract.pyimports no pcapkit at all — it reads source under its own root — so no editable install can shadow what it measures. Exit codes were read directly throughout, sincepytest-subtestsis not a dependency and pytest 9.1.1 prints a failing subtest's parent asPASSED.One commit, rebased onto
c8fd97bcd.