Skip to content

fix(reassembly): the deferred TCP analyser dispatches on AppType, not bare ports - #2421

Merged
JarryShaw merged 1 commit into
mainfrom
fix/2417-apptype-ports
Oct 29, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/2417-apptype-ports

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

  • fix — corrects a defect

Description of your pull request and other information

Follow-up to #2418 (#2417), which made Deferred generic but left the TCP constraint as tuple[int, int]. Your note on it — we are still using int tuples for transport ports, let's move to AppType tuples — is what this does.

TCP.submit resolves bufid[1]/bufid[3] through Transport._make_port before building the Deferred (new TCP._make_ports), so a deferred analysis dispatches on the same AppType member TCP.src/TCP.dst already read off a parsed header. _KT's second constraint and Transport.analyze both become tuple[AppType, AppType].

Unregistered ports. Measured: AppType.get answers every port a 16-bit field can hold — an assignment with its canonical service (443 → https), an IANA span with that span's label (0 → reserved), anything else with an unregistered unknown (50000, 65535) — never None, without growing the registry (#860). It rejects only a port outside 0-65535, a non-int, or a bool (#2287), none of which an adapter can read out of a TCP header. So this adds no way for reassembly to raise.

Three judgement calls for your eye.

  1. Transport.analyze is in v1.4.1, so the annotation is tightened without a new rejection: the chosen port is tested against __proto__ as it arrived and only that one goes through int(), so the registry is read with the int key it declares and a bare-int caller keeps working (test_transport_unit.py needed no edit). Labelled breaking for the annotation and because a custom __protocol_type__ now receives members — drop it if you read this as a typing refinement.
  2. _make_ports reaches _make_port through TCP, not through self.protocol where the analyser comes from: __protocol_type__ is an extension point whose analyser need only define analyze (the reassembly tests' own stubs do exactly that), so self.protocol._make_port would be an AttributeError.
  3. DatagramID's ports stay bare ints, with a docstring saying why — it is the flow identity split out of BufferID, a dict key every toolkit adapter builds from a plain port number and which is dumped as part of the report, where the deferred pair is a dispatch key. Say the word if you want it moved too.

UDP and SCTP need no change — neither has a reassembler, so Transport.analyze has no caller but TCP.submit. One pre-existing oddity left alone: SCTP inherits Transport.analyze but keys __proto__ by PPID, so a direct SCTP.analyze((src, dst), ...) searches the wrong registry. Nothing calls it; say if you want an issue.

Verification. mypy over pcapkit/ byte-identical to mainline (98 errors, diffed line for line). pylint over the four touched files reports the same two pre-existing _decode_next_layer messages as mainline, measured in a scratch worktree at 867616b8; isort -l100 -ppcapkit clean. Tests: reassembly + transport 412/1408 subtests, interface const dumpkit cli vendor 702/27798, the annotation-name/tier/docstring/public-API guards 204/3371. The full local CI selection (pytest --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py') is green on the pushed commit: 6552 passed, 112 skipped, 69451 subtests. Reassembly over the sample captures resolves the same next layer as before, port for port. Reverting submit to bare ints fails four of the new cases plus the tcp subtest of test_declared_type_admits_what_each_reassembler_passes; reverting only _KT fails that subtest alone.

Review round at d323feef → 5c38fc0ba. Applied, with thanks: the ports are now typed as the per-transport registry pcapkit.const.reg.apptype.tcp.TCP rather than the base AppType (confirmed type(...) is TCP for a registered and an unassigned port alike) — Transport.analyze keeps the base, being the one method TCP/UDP/SCTP share, and now says so; the bool rejection is correctly attributed to _make_port, not to AppType.get, which really does answer AppType.get(True, proto=tcp) with <TCP.tcpmux: 1 [tcp]> (that is why _make_ports goes through _make_port); the "same member TCP.src reads" claim is qualified — for an unassigned port the two paths build equal, differently-named members ('<unassigned>' vs 'unknown'), now pinned by a test; docs entries added for TCP._make_ports and AppType.__hash__. Also fixed, unprompted from the same note: analyze((None, None)) and (54321, '80') fall back to Raw as on mainline again, instead of raising / dispatching.

The pickle regression IS now fixed, in this PR. The earlier round deliberately deferred it as a pre-existing registry hole out of scope here; a cross-review pushed back with the exhaustive measurement (90.7% of ports, not just the unknown subset) and it landed instead: EnumRegistry._make_reducible (pcapkit/corekit/enum.py) installs a per-instance pickle reducer for dynamically-created unregistered members, mirrored into the vendor template so make vendor won't revert it, opted into by AppType._unregistered_member with its own constructor args. Verified: 0 pickle failures across all 65,536 ports × 4 transports (TCP/UDP/SCTP/DCCP), registry provably unchanged, no ripple into the other 122 EnumRegistry subclasses that don't opt in.

CI-break fix. The sample-capture round trip test (SampleCaptureDatagramPickleTests) read generated/gitignored captures directly from a _unit.py module; CI's unit-tests leg never runs make_samples.py for that tier, so it failed on every Python leg though it passed locally. Moved to test_datagram_deferred_pickle_2417_runtime.py (ALL tier, matching the precedent at test_ipv6_cut_non_final_fragment_1567_runtime.py) — verified failing without captures present and passing with them, both before and after this move.

Verification after both rounds. mypy over pcapkit/ still byte-identical to mainline (98 errors, same set); pylint unchanged; isort clean. tests/foundation/reassembly 157 passed with captures generated (151 under CI's selection without them); base is 145, delta is the 12 test functions this PR adds (4 pickle round-trip, 8 proto-type). Full pickle sweep 262,144 members / 1,572,864 round trips, 0 failures. The full CI selection is green on the pushed commit.

@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) python Pull requests that update Python code breaking Breaks public-facing behaviour or API (apply alongside the type label) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 28, 2026
@github-actions github-actions Bot added this to the PyPCAPKit v1.5 milestone Oct 28, 2026
@JarryShaw JarryShaw moved this to In review in PyPCAPKit Oct 28, 2026
@JarryShaw JarryShaw added review: running A cross-review is in flight against the current head - no verdict yet and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Can we actually use TCP's AppType for TCP's Reassembly?

@JarryShaw

Copy link
Copy Markdown
Owner Author

Yes — it already does. TCP._make_ports resolves both ports via TCP_Protocol._make_port(bufid[N], Enum_TransportProtocol.tcp), explicitly passing proto=tcp, so each port is resolved against TCP's own AppType partition — the same resolution TCP.src/TCP.dst use, not a generic/cross-protocol lookup.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Coverage: 94.22% (unit tier, Python 3.15, 4466f52ed, Unit Tests run success)

Package Statements Missed Branches Partial Cover
pcapkit (top level) 104 4 20 2 95.16%
pcapkit/const 19609 133 2620 120 98.84%
pcapkit/corekit 2605 74 818 37 96.52%
pcapkit/dumpkit 282 3 102 3 98.44%
pcapkit/foundation 3050 24 1062 26 98.78%
pcapkit/interface 138 4 52 3 96.32%
pcapkit/protocols 17593 193 4678 179 98.32%
pcapkit/toolkit 924 36 334 10 95.55%
pcapkit/utilities 502 7 144 5 98.14%
pcapkit/vendor 4449 1961 1020 167 52.64%

Per-file detail: the coverage-html artifact of this run.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Review: NEEDS CHANGES at d323feef (Sonnet). Mechanism, AppType.get coverage (swept all 65,536 ports — none raise, none return None), mypy/pylint/isort, and backward-compat for bare-int Transport.analyze callers all confirmed exactly as claimed. Reassembly over all 7 sample captures gives identical next-layer resolution to main (243/243 datagrams).

Blocking: the pickle regression is not an edge case. Every sample capture with TCP flows (http.pcap, stream.pcap, tcp.pcap, http6.cap) now produces a pickle that loads can't read — 59,413 of 65,536 ports (90.7%) fail to round-trip, not just "unknown" ports as the PR's proposed mitigation assumed (that only covers the 16,524 truly-unregistered ports; the 42,889 span-label ports fail too). Root-cause fix, prototyped and discarded in review: ~13 lines in AppType._unregistered_member installing a per-instance __reduce_ex__, the same technique EnumField._unregistered_member already uses (precedent: test_a_miss_through_the_field_pickles_and_renders_as_unassigned, #1774). That fixes the registry itself rather than routing around it, and all 65,536 ports round-trip afterward.

Non-blocking prose/doc fixes: AppType.get(True) actually returns <TCP.tcpmux: 1> — it's Transport._make_port that rejects bools, not AppType.get as the PR body and docstring claim. "The same member TCP.src exposes" is only true by equality — for an unknown port the parse path returns a different, picklable object named <unassigned>. TCP._make_ports and AppType.__hash__ have no docs entry (#1940 convention).

My read on your three judgment calls: keep breaking (confirmed a custom __protocol_type__ genuinely receives a str-subclass AppType now, where isinstance(p, int), arithmetic, struct.pack, etc. all break); fix pickling at the registry root rather than the partial parse-path variant; keep DatagramID on bare ints (moving it would extend the same pickle breakage to every pickled datagram even after .packet resolves, with no dispatch benefit since equality/hashing already match ints).

Minor: Transport.analyze((None, None)) now raises TypeError where main fell back to Raw; (54321, '80') now dispatches HTTP where main fell back to Raw. Two of the six new tests pass even with submit reverted (pin invariants, not the change).

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

why _make_port? why can't we directly call from TCP enum's constructor or get?

and I was referring to the typing annotation. why dont we directly use TCP enum, rather the base AppType?

@JarryShaw

Copy link
Copy Markdown
Owner Author

Both good questions, and both point at a real improvement.

Why _make_port instead of calling get/the constructor directly: AppType.get(port, proto=...) alone would accept True/False as ports 1/0, since bool is an int subclass — confirmed in review (AppType.get(True) returns <TCP.tcpmux: 1>). _make_port exists specifically to add the bool-rejection that closed #2287; calling get directly would reopen that bug. It also adds an idempotency check (isinstance(port, Enum_AppType): return port) so re-resolving an already-resolved value is a no-op.

Why the annotation uses the base AppType rather than the TCP subclass: this one's a real gap, not a necessity. AppType.get(key, proto=...) dispatches via _dispatch(key, proto) to the proto-specific registry and genuinely returns a member of pcapkit.const.reg.apptype.tcp.TCP at runtime when proto='tcp' — the object IS the TCP subclass. The type annotations (Enum_AppType throughout _make_ports, Transport.analyze, Deferred's _KT) just never tightened to match. Folding this into the fix in flight.

@JarryShaw
JarryShaw force-pushed the fix/2417-apptype-ports branch from d323fee to 5c38fc0 Compare October 29, 2026 00:28
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Oct 29, 2026
@JarryShaw
JarryShaw force-pushed the fix/2417-apptype-ports branch from 5c38fc0 to 2365640 Compare October 29, 2026 00:47
@JarryShaw JarryShaw added review: running A cross-review is in flight against the current head - no verdict yet and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 29, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Live CI break found on this head (236564017b1) by the agent that authored the original AppType rework, while confirming its own work had survived the two amends on top of it: tests/foundation/reassembly/test_datagram_deferred_pickle_2417_unit.py's SampleCaptureDatagramPickleTests reads generated sample captures from a _unit.py module — CI's unit-tests leg doesn't generate those for that tier, so Python 3.11–3.15 all fail with .../http.pcap is missing. It passed locally only because make_samples.py had already been run.

Fix dispatched: moving that class (and its capture-reading helpers) into a new _runtime.py file per the repo's existing tests/dumpkit/test_capture_dump_runtime.py precedent, verified under both "fixtures present" and "fixtures absent" conditions.

Also found: tests/test_tier_guard.py claims to catch exactly this shape and doesn't — filed as #2426, separately, not blocking.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 29, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Review: NEEDS CHANGES at 236564017 (Sonnet). One blocking issue, the rest cosmetic.

Blocking: tests/foundation/reassembly/test_datagram_deferred_pickle_2417_unit.py's SampleCaptureDatagramPickleTests.test_every_capture_with_tcp_flows_round_trips reads http.pcap/stream.pcap/tcp.pcap/http6.cap straight from SAMPLE_ROOT with a hard assertTrue(path.is_file()). Those captures are gitignored/generated; CI's test job never runs regenerate_samples.py, so this fails on every Python leg in CI even though it passed locally (worker's checkout had the captures generated already). Fix: either move the class to a *_runtime.py module (reserved tier, matches test_tcp_runtime.py's pattern), or guard the read with try/except FileNotFoundError: skipTest per tests/toolkit/test_dpkt_unit.py:534.

Also before merge: the PR body still says the pickle regression is "NOT fixed here, deliberate call" — stale, contradicts the current head.

Minor, non-blocking: the full 65,536-port sweep sits in an ALL-tier module (reserved for latest per #1958/#1963); two same-named helper pairs now exist in both corekit/enum.py and corekit/fields/numbers.py; the "mypy byte-identical" claim is off by one line number (578→579, same error); fail-without-fix counts were reported as 3/307, actually 4/316 — direction and named items match, just the exact numbers.

Everything else verified independently: commit layering intact (other agent's work not lost), pickle mechanism sound across Py3.8–3.15/protocols 0–5/multiprocessing, full 262,144-member sweep 0 failures, registry non-growth confirmed, ripple-check across all 127 EnumRegistry subclasses (only the 4 AppType ones differ), #1774 canary genuinely inverted.

… bare ports

Follow-up to #2418, which made `Deferred` generic over what its analyser
dispatches on but left the TCP side of that constraint as `tuple[int, int]`.
The owner's note on it: we are still using int tuples for transport ports, so
move to AppType tuples.

* `TCP.submit` resolves `bufid[1]` / `bufid[3]` through `Transport._make_port`
  before building the `Deferred`, in a new `TCP._make_ports` helper. A deferred
  analysis now dispatches on a named service rather than on a number.
* `_KT`'s second constraint is `tuple[AppType_TCP, AppType_TCP]` -- the
  per-transport registry `pcapkit.const.reg.apptype.tcp.TCP`, which is what the
  resolution really answers with (`type(...) is TCP`, measured), not the base
  `AppType`. `Transport.analyze` keeps the base, being the one method TCP, UDP
  and SCTP share, and says so.
* `Transport.analyze` tests the chosen port against `__proto__` exactly as it
  arrived and unwraps it to the registry's `int` key only when it is a member,
  so a bare `int` caller keeps working -- the method is in v1.4.1 -- and
  `(None, None)` or `(54321, '80')` still fall back to `Raw` as on mainline
  rather than raising or dispatching.
* `_make_ports` reaches `_make_port` through `TCP` rather than through
  `self.protocol`: `__protocol_type__` is an extension point whose analyser need
  only define `analyze`, so `self.protocol._make_port` would be an
  `AttributeError` for one that is not a `Transport`. Going through `_make_port`
  rather than calling `AppType.get` directly is what keeps #2287's bool
  rejection: `AppType.get(True, proto=tcp)` resolves `True` as port 1 and
  answers `<TCP.tcpmux: 1 [tcp]>`, measured.
* `DatagramID`'s ports stay bare `int`s, and its docstring now says why: it is
  the flow identity split out of `BufferID`, a dict key every toolkit adapter
  builds from a plain port number and which is dumped as part of the report.
* Docs entries for `TCP._make_ports` and for `AppType.__hash__`, both now named
  in public prose (#1940).

All 65,536 ports resolve to a member -- an assignment to its canonical service,
an IANA span to that span's label, anything else to an unregistered `unknown` --
none raise and the registry does not grow, so this adds no way for reassembly to
raise. For a registered port the member is the one `TCP.src` reads off a parsed
header; for an unassigned one the two are equal but not identical and named
apart, which the docstrings record rather than overstate. UDP and SCTP need no
change: neither has a reassembler, so `Transport.analyze` has no caller but
`TCP.submit`.

Make an unregistered member survive pickling
--------------------------------------------

Dispatching on members rather than numbers put them inside the `Deferred` that
`TCP.submit` stores as a `Datagram`'s `packet`, and that made an unresolved
datagram unpicklable for most ports. `AppType.get` answers a port it holds no
assignment for through `AppType._unregistered_member`, which builds a member
deliberately absent from every one of the registry's lookup tables so the
lookup does not grow it (#860). `Enum.__reduce_ex__` reduces a member to
`(cls, (value,))` -- a value-keyed lookup, i.e. the one table such a member is
missing from -- so it pickled fine and then raised on read-back:

    ValueError: 'unknown [50000 - tcp]' is not a valid TCP

Measured before the fix: 59,413 of the 65,536 possible TCP ports failed to
round-trip, 90.7% of the space, and not merely the truly unregistered `unknown`
ones but every IANA-span label too. All four sample captures with TCP flows
failed, every datagram, because a client socket is ephemeral.

* `EnumRegistry._make_reducible` installs a per-instance `__reduce_ex__` that
  rebuilds through `cls._unregistered_member(*args)` instead of through a value
  lookup, with module-level `_rebuild_unregistered_member` /
  `_reduce_unregistered_member` so that pickle protocols below 4 can name the
  callable. This is the technique `EnumField._unregistered_member` already uses
  for the same class of problem on the *parse* path (#575, #1774).
* `AppType._unregistered_member` opts in, reducing to its own
  `(value, name, proto)` rather than to the crafted `'svc [port - proto]'`
  string it hands its base -- reducing to the latter would rebuild a member
  whose value had been formatted twice. It is opt-in per registry for exactly
  that reason; a registry whose `_missing_` calls the base method directly is
  unchanged.
* Rebuilding re-enters `_unregistered_member` and never `cls.__new__`, so an
  unpickled member is unregistered just as the original was and no table grows
  -- `__members__`, `_value2member_map_` and `__registry__` all measured
  unchanged before and after, on every transport. A restored member is itself
  reducible, so a datagram read back from a dump can be written out again.

After: all 65,536 ports round-trip on every transport (262,144 members), on
every pickle protocol 0-5, under `copy.copy`/`copy.deepcopy`, and all four
named captures load back -- 231 deferred datagrams across `http.pcap`,
`stream.pcap`, `tcp.pcap` and `http6.cap`. A registered member still unpickles
to the identical shared singleton, which pins that the reducer went on the
unregistered path only.

* `tests/const/test_const_apptype_unregistered_pickle_2417_unit.py` pins the
  member-level round trip over the whole port space, every protocol, and the
  no-growth property.
* `tests/foundation/reassembly/test_datagram_deferred_pickle_2417_unit.py` pins
  the symptom on a synthesised datagram, unassigned and assigned alike.
  `test_datagram_deferred_pickle_2417_runtime.py` pins the same thing end to
  end over the four sample captures, split into its own fixture-dependent
  module because CI's unit-tests leg never generates them -- the unit file
  reading a generated capture directly is what broke that leg. Both scope to
  pickle protocols 2-5: `Deferred` declares `__slots__` and no `__getstate__`,
  which protocols 0 and 1 cannot serialise at all. That is pre-existing and
  independent of these members -- it reproduces for an assigned flow and for a
  bare `Deferred(..., 42, b'x')` holding a plain `int` -- so it is recorded
  there rather than worked around, and wants its own issue.
* The #1774 canary asserted that `get`'s miss member was *not* picklable, which
  was a defect rather than a distinction. It now asserts the round trip works
  and that the member's `name` is the only remaining difference from what a
  parse returns -- which is what still justifies the peek. `AppType.__hash__`
  gained the docstring its new `#1940` entry requires.

mypy over `pcapkit/` is byte-identical to mainline (98 errors in 32 files, same
set); `pylint` with the Makefile flags reports the same message set and counts
over the touched files as the pre-review revision; `isort -l100 -ppcapkit` is
clean. Tests: 876 tests / 25,888 subtests over `foundation/reassembly`,
`protocols/transport`, the `const` registries, the field-pickling suites, the
`#1940` docs guard and the docstring contract, with 4 skips that predate this
branch (the local venv has sphinx-autodoc-typehints 3.13.6, not the 3.17.2
`docs/requirements.txt` pins). Reassembly over the sample captures resolves the
same next layer as before, port for port.
@JarryShaw
JarryShaw force-pushed the fix/2417-apptype-ports branch from 2365640 to 4466f52 Compare October 29, 2026 01:30
@JarryShaw JarryShaw added review: running A cross-review is in flight against the current head - no verdict yet review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment review: running A cross-review is in flight against the current head - no verdict yet review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 29, 2026
@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 29, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Review: GOOD TO GO at 4466f52ed (Sonnet). Only finding was two inaccuracies in the PR body text itself (no code/test/CI issue): the test-count delta (actual: 157 passed with captures / 151 without; base 145, delta is the 12 functions this PR adds — 4 pickle + 8 proto-type) and the cited _runtime.py tier precedent (test_tcp_runtime.py has no __tier__, so it's LATEST; the correct ALL-tier precedent is test_ipv6_cut_non_final_fragment_1567_runtime.py). Both corrected in the body directly — no re-review needed.

Everything else independently re-confirmed at this head: diff vs the prior head is only the two test files with an unchanged parent; CI-break reproduced and fixed (old _unit.py fails 4 subtests without captures, new one passes; _runtime.py fails the same 4 without captures, passes after make_samples.py); moved class AST/text-identical to its old location; falsification (disabling _make_reducible) reproduces the original ValueError; lint/mypy/pylint clean vs baseline; CI job-level green on the new head where it was red on the old one.

JarryShaw added a commit that referenced this pull request Oct 29, 2026
…evel tuple

The AST audit in tests/_tiers.py only recognised a generated-capture read as a
literal argument to tests._support.sample_path(...). It missed the
indirection PR #2421's first revision hit: a module-level CAPTURES tuple read
in a for loop whose body joined SAMPLE_ROOT / name directly, bypassing
sample_path(). The guard's own static pass never flagged it, which is the
defect this fixes -- CI's unit-tests leg did fail on that revision, on every
affected Python interpreter, because the capture genuinely was not on disk;
it only passed locally where make_samples.py had already been run.

- tests/_tiers.py: add sample_root_join_calls(), which follows a SAMPLE_ROOT
  join back to a module-level name bound to a single string, or to a for
  loop's target over a module-level (or inline) tuple/list of strings.
  audit_module() now merges these findings with the existing
  sample_path_calls() ones, in source order. Deliberately narrow, per the
  module docstring's existing rationale for rejecting a blanket ".pcap
  literal" rule: no new false positive across the real suite (verified).
- tests/test_tier_guard.py: new AuditTests cases pin the loop and direct-join
  shapes being flagged, and that a committed capture, a try/except-handled
  read, and a for loop over sorted(CAPTURES) (a module-level tuple wrapped in
  a call, rather than read bare) stay unflagged.

Verified: reverting the tests/_tiers.py change makes the three new
"is flagged" tests fail (0 findings where 1-2 are expected, plus an
AttributeError for the new function); restoring it makes them pass. Full
tests/test_tier_guard.py + test_tier_guard_xdist.py +
test_capture_tracking.py suite green (156 passed, 3 skipped), and a
whole-suite audit_module() sweep over every unit-tier module on disk still
reports zero findings. mypy and pylint report no new findings against either
file.

Closes #2426
@JarryShaw
JarryShaw merged commit 53f1e57 into main Oct 29, 2026
69 checks passed
@JarryShaw
JarryShaw deleted the fix/2417-apptype-ports branch October 29, 2026 02:29
@github-project-automation github-project-automation Bot moved this from In review to Done in PyPCAPKit Oct 29, 2026
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Oct 29, 2026
JarryShaw added a commit that referenced this pull request Oct 29, 2026
…evel tuple (#2427)

The AST audit in tests/_tiers.py only recognised a generated-capture read as a
literal argument to tests._support.sample_path(...). It missed the
indirection PR #2421's first revision hit: a module-level CAPTURES tuple read
in a for loop whose body joined SAMPLE_ROOT / name directly, bypassing
sample_path(). The guard's own static pass never flagged it, which is the
defect this fixes -- CI's unit-tests leg did fail on that revision, on every
affected Python interpreter, because the capture genuinely was not on disk;
it only passed locally where make_samples.py had already been run.

- tests/_tiers.py: add sample_root_join_calls(), which follows a SAMPLE_ROOT
  join back to a module-level name bound to a single string, or to a for
  loop's target over a module-level (or inline) tuple/list of strings.
  audit_module() now merges these findings with the existing
  sample_path_calls() ones, in source order. Deliberately narrow, per the
  module docstring's existing rationale for rejecting a blanket ".pcap
  literal" rule: no new false positive across the real suite (verified).
- tests/test_tier_guard.py: new AuditTests cases pin the loop and direct-join
  shapes being flagged, and that a committed capture, a try/except-handled
  read, and a for loop over sorted(CAPTURES) (a module-level tuple wrapped in
  a call, rather than read bare) stay unflagged.

Verified: reverting the tests/_tiers.py change makes the three new
"is flagged" tests fail (0 findings where 1-2 are expected, plus an
AttributeError for the new function); restoring it makes them pass. Full
tests/test_tier_guard.py + test_tier_guard_xdist.py +
test_capture_tracking.py suite green (156 passed, 3 skipped), and a
whole-suite audit_module() sweep over every unit-tier module on disk still
reports zero findings. mypy and pylint report no new findings against either
file.

Closes #2426
JarryShaw added a commit that referenced this pull request Oct 29, 2026
…nst as a commit-type

* pcapkit/protocols/protocol.py: flip the 11 remaining unqualified
  `ProtocolBase.<member>` cross-references to `Protocol.<member>`, display and
  target both, matching the mechanical flip #2420/#2422/#2428 already did for
  37+ other sites. The `__init_subclass__` sites and `register`'s `Raises:`
  clause are untouched, per the existing #2316/#1016 carve-out.
* pcapkit/protocols/transport/transport.py: same flip for the
  `ProtocolBase._lookup_registry` reference #2421 added after #2422's sweep
  had already run, so it was left behind.
* CONTRIBUTING.md, .github/PULL_REQUEST_TEMPLATE.md: add `const` to the valid
  commit-type list and PR template checkbox. process.rst already documented
  `const` as a Type label; CONTRIBUTING.md and the template had not caught up.
  `tests/project/test_pr_template_commit_types_1102_unit.py` derives its
  expected set from CONTRIBUTING.md, so no test change was needed.
* tests/project/test_conventions_doc_claims.py: drop the now-false "const has
  no tickbox" example from
  test_the_page_names_every_commit_type_the_template_ticks's docstring --
  the template gained one above -- and restate why the check is one-way
  without relying on that example.
* docs/source/conf.py's two "Sphinx 9.1" comments were re-verified against
  the pinned toolchain (Sphinx 9.2.0, per docs/requirements.txt) by instrumenting
  each code path directly: autodoc still hands `is_enum_member` the plain int
  for an enum member, and still prefers a pass-through `__new__` over `__init__`
  for a class signature. Both comments are accurate at 9.2.0 and left as is.

Prose-only: an AST-structure diff (docstrings stripped) against origin/main is
identical for both touched .py files. `make -C docs html` under the pinned
toolchain is warning-for-warning identical to a main build (1 pre-existing,
unrelated `TransType` ambiguity). `tests/project/` is failure-for-failure
identical to main (234 failed/11 errors, all pre-existing and environmental,
unrelated to this change; 2010 passed either way).
JarryShaw added a commit that referenced this pull request Oct 29, 2026
…nst as a commit-type (#2431)

* pcapkit/protocols/protocol.py: flip the 11 remaining unqualified
  `ProtocolBase.<member>` cross-references to `Protocol.<member>`, display and
  target both, matching the mechanical flip #2420/#2422/#2428 already did for
  37+ other sites. The `__init_subclass__` sites and `register`'s `Raises:`
  clause are untouched, per the existing #2316/#1016 carve-out.
* pcapkit/protocols/transport/transport.py: same flip for the
  `ProtocolBase._lookup_registry` reference #2421 added after #2422's sweep
  had already run, so it was left behind.
* CONTRIBUTING.md, .github/PULL_REQUEST_TEMPLATE.md: add `const` to the valid
  commit-type list and PR template checkbox. process.rst already documented
  `const` as a Type label; CONTRIBUTING.md and the template had not caught up.
  `tests/project/test_pr_template_commit_types_1102_unit.py` derives its
  expected set from CONTRIBUTING.md, so no test change was needed.
* tests/project/test_conventions_doc_claims.py: drop the now-false "const has
  no tickbox" example from
  test_the_page_names_every_commit_type_the_template_ticks's docstring --
  the template gained one above -- and restate why the check is one-way
  without relying on that example.
* docs/source/conf.py's two "Sphinx 9.1" comments were re-verified against
  the pinned toolchain (Sphinx 9.2.0, per docs/requirements.txt) by instrumenting
  each code path directly: autodoc still hands `is_enum_member` the plain int
  for an enum member, and still prefers a pass-through `__new__` over `__init__`
  for a class signature. Both comments are accurate at 9.2.0 and left as is.

Prose-only: an AST-structure diff (docstrings stripped) against origin/main is
identical for both touched .py files. `make -C docs html` under the pinned
toolchain is warning-for-warning identical to a main build (1 pre-existing,
unrelated `TransType` ambiguity). `tests/project/` is failure-for-failure
identical to main (234 failed/11 errors, all pre-existing and environmental,
unrelated to this change; 2010 passed either way).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) fix Pull requests that fix a defect (fix: subject prefix) python Pull requests that update Python code

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant