Skip to content

fix(exports): export the 23 public attributes their package __all__ omitted (#533) - #544

Merged
JarryShaw merged 2 commits into
mainfrom
fix/533-unlisted-public-exports
Sep 20, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
fix/533-unlisted-public-exports

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Closes #533.

The count, re-derived

The issue's figure is "roughly 45 public attributes across about 12 non-aggregator packages", and says plainly that it "has not been independently re-derived". It has been now, with a scanner rather than by hand, and it is exactly 45 across exactly 12 packages — measured on 122d32795, the base this branch was cut from, i.e. after #527.

Definition used, matching the one test_aggregators_export_every_public_attribute already applies to the three aggregators: for each of the 52 public packages, every attribute of the package that does not start with _, is not named in that package's __all__, and is not a module object. Submodules are excluded because a submodule becomes an attribute of its parent as a side effect of being imported, so counting them would flag every package that has children.

tree unlisted names packages
0f4beeff6 (the #527 merge) 45 12
122d32795 (this branch's base) 45 12
this branch 22 10

#527 had already handled its own share, and none of my 45 overlap with it. #527 fixed the inverse defect — __all__ entries naming nothing — in six files, and as a side effect brought the three aggregators to zero unlisted attributes. That is why all three aggregators are absent from the table above: measured at the #527 merge commit they already contribute nothing, so there is no double-counting and nothing to exclude. The 45 are a disjoint set from #527's work.

One correction falls out of this. #527's own module docstring records the figure as "45 across 13", and that is the number the issue paraphrased as "about 12". Measuring the #527 merge commit directly gives 45 across 12: the name count was right and the package count was one high. The docstring is corrected in this PR as part of rewriting that paragraph.

What was added: 25 entries, closing 23 of the 45

Triage, not a mechanical sweep. A name was added only where the package's own established pattern is broken by its absence — never merely because the name is public and documented.

file entries why it is an oversight rather than a decision
pcapkit/__init__.py 12 — C_Tag, S_Tag, DRARP, InARP, L2TPv2, HTTPv1, HTTPv2, PCAPNG, Header, Frame, Data, Schema The top-level __all__ is a per-layer mirror of pcapkit.protocols.__all__, and it is complete for the internet and transport layers. All 12 are in pcapkit.protocols.__all__ already; 9 of the 12 are also in pcapkit/all.py, the "Library Index" whose stated job is to hold "all things from pcapkit". The drift is datable: git log -S"C_Tag" -- pcapkit/__init__.py returns nothing at all, while #436 (f50436a8a) added C_Tag/S_Tag/L2TPv2 to pcapkit/protocols/__init__.py, pcapkit/protocols/link/__init__.py and pcapkit/all.py — and left the top level alone.
pcapkit/foundation/__init__.py 8 — the 6 register_* functions, plus ReassemblyManager, TraceFlowManager This __all__ re-lists the registry's exports and names 27 of the 33. The 6 it withholds are precisely the 6 that #527 added one level down to pcapkit/foundation/registry/__init__.py, for the reason #527 gave: each carries an .. autofunction:: directive at docs/source/pcapkit/foundation/registry.rst:32-45. Four of the six live in the same module as register_extractor_engine, register_dumper, register_extractor_dumper and register_traceflow_dumper, all of which are listed. The two manager entries are the cascade from the two rows below — pcapkit/foundation/__init__.py wildcard-imports both packages.
pcapkit/foundation/reassembly/__init__.py 1 — ReassemblyManager Defined below the __all__ literal that omits it (__init__.py:38, literal at :24), documented at docs/source/pcapkit/foundation/reassembly/index.rst:52 under the package path, and imported by name from foundation/extraction.py and four engine modules.
pcapkit/foundation/traceflow/__init__.py 1 — TraceFlowManager Identical shape: defined at :37, literal at :23, documented at docs/source/pcapkit/foundation/traceflow/index.rst:53, imported by name from three modules.
pcapkit/utilities/__init__.py 3 — beholder, prepare, seekset The package's own module docstring advertises them: "decorater function seekset and beholder". pcapkit/utilities/decorators.py:36 exports all three. All three carry .. autodecorator:: directives at docs/source/pcapkit/utilities/functools.rst:15-19. And pylint was already saying so — see the lint section.

25 __all__ entries, closing 23 of the 45. The arithmetic: 45 − 23 = 22 residual, all allowlisted. The extra 2 entries are ReassemblyManager/TraceFlowManager appearing a second time, in pcapkit/foundation, where they became visible only because of the two rows above — a cascade the scanner caught on the second pass, not a name from the original 45.

Nothing outside an export list changed. No module body, no import, no docstring in pcapkit/. And nothing reads any of the five lists as data: the only two consumers of an __all__ as a value are pcapkit/protocols/__init__.py:74 (the for name in __all__ that builds __proto__) and pcapkit/vendor/__main__.py:79,87 (the crawler target list), and this PR touches neither file. Worth stating because it is a live trap rather than a hypothetical: adding 'Protocol' to the first __all__ literal in pcapkit/protocols/__init__.py would have registered a PROTOCOL entry in __proto__. Protocol is not exported here for other reasons, but that is the shape of the accident this rule avoids.

Names deliberately NOT exported — 22, and why

These are in the new DELIBERATE_NON_EXPORTS allowlist with the reasoning written next to them, which is what the issue asked for: "consider whether a leading underscore would express the intent better than silence, since silence is indistinguishable from an oversight". Recording them is the compromise — the intent becomes reviewable without touching a module body.

The 10 abstract bases — Field, Engine, Reassembly, IP_Reassembly, TraceFlow, Protocol, Application, Internet, Link, Transport. Every one is documented with an autoclass:: directive, which is the signal #527 used, so the case for exporting them is not weak. They are left alone anyway, because three independent export surfaces agree in excluding them:

  1. nine packages each import theirs explicitly under a # Base Class for ... comment and each omits it from __all__, the same way;
  2. pcapkit/all.py — the library index — lists no base class at all;
  3. pcapkit/corekit/__init__.py lists the 22 concrete *Field classes and not Field, and refers to it at corekit/__init__.py:15 as :class:`~pcapkit.corekit.fields.field.Field` — by its defining path, which is the docs convention here.

A pattern repeated nine times and corroborated by two other surfaces is a convention, not nine oversights. If the owner disagrees, this is the judgement call to overturn — it is 10 of the 22 and the single biggest decision in the triage. Deleting the relevant allowlist entries and adding the names is then a mechanical follow-up.

7 names another module's import dragged inInfo and info_final in both pcapkit.foundation.reassembly and pcapkit.foundation.traceflow (they belong to pcapkit.corekit.infoclass, which documents and exports them; the two packages import them only to build their manager classes), and TYPE_CHECKING in three packages, which is typing's. Re-exporting any of these would claim a name the package does not own.

4 pieces of import-time machinery at the top levelBaseError, DEVMODE, DevModeWarning, ROOT. pcapkit/__init__.py:82-94 imports the first three solely to wire up the tbtrim excepthook; they are documented under pcapkit.utilities. ROOT is the sharpest of the four: pcapkit/__init__.py:92 binds it only on the else branch, so exporting it would make from pcapkit import * raise AttributeError under PCAPKIT_DEVMODE=1 — manufacturing a fresh #515 while claiming to fix one. BaseError is the most arguable of the four, being the exception a user would naturally want to catch; it is left out because the top-level surface is interface functions, macros and protocols, and it is exported from pcapkit.utilities.exceptions where it is documented.

1 leaked loop variablepcapkit.protocols.name, left behind by the for name in __all__: at pcapkit/protocols/__init__.py:74. Renaming it _name would express the intent better than an allowlist entry does, and the issue invites exactly that; it is a change to a module body rather than to an export list, so it is flagged here for the owner instead of made silently. One line, no behavioural effect.

Two more judgement calls that are not allowlist entries, because the scanner does not count module objects:

  • data and schema as submodule names in pcapkit.__all__. Both pcapkit.protocols.__all__ and pcapkit/all.py export the pair Data/data and Schema/schema together. This PR adds only Data and Schema, leaving the mirror one step short. Exporting a module object through __all__ is a different kind of decision from exporting a class, and it is outside both the issue's scope and the new test's, so it is the owner's call.
  • pcapkit/all.py is not touched, see below.

Found but not fixed: pcapkit/all.py has the same defect, module-scoped

pcapkit/all.py is a module rather than a package, so it falls outside the issue's scope, the scanner's, and the new assertion's. It holds 16 unlisted public non-module attributes against its 151-entry __all__:

ESP  HTTPv1  HTTPv2  PCAPNG  ModuleDescriptor  MultiDict  OrderedMultiDict  info_final
configure  detect  ensure_output  get_logger  logger  reset  stacklevel  warn

They are not all the same thing. The last eight are the whole of pcapkit.utilities.__all__, and all.py:135-136 shows that omission is deliberate — a # pcapkit.utilities heading followed by a commented-out line. But ESP, PCAPNG, HTTPv1 and HTTPv2 look like the same drift this PR fixes at the top level: ESP is in both pcapkit.__all__ and pcapkit.protocols.__all__ and has been since #378, yet from pcapkit.all import * does not provide it. ModuleDescriptor, MultiDict, OrderedMultiDict and info_final are in pcapkit.corekit.__all__ and missing here too.

Not fixed in this PR for two reasons: all.py is not a package __init__.py, and its utilities section shows it carries deliberate omissions of its own, so triaging it properly is a separate pass. Worth its own issue. Note that the three decorators this PR adds to pcapkit.utilities.__all__ flow into pcapkit.all's namespace and are not added to its __all__ — consistent with the whole utilities group already being excluded there, rather than a new gap.

The test: extended, not duplicated

tests/project/test_public_api.py is #527's module and this extends it, which is the cleaner fit — it already owns the package walk, the exclusion rule and the __all__ invariants, and its docstring is where the scoping decision that created this issue was recorded.

Three things change:

  • test_every_public_package_exports_every_public_attribute — the converse assertion over all 52 public packages, subtracting DELIBERATE_NON_EXPORTS. This is the "second contract assertion in the other direction" the issue suggested.
  • test_the_non_export_allowlist_is_tight — the allowlist is load-bearing, so it is itself asserted. Three kinds of rot each fail: an entry naming a package not in the walk, an entry naming an attribute the package no longer has, and an entry naming something the package now exports anyway. Without this, a stale entry silently excuses the next real omission of the same name.
  • test_aggregators_export_every_public_attribute is kept rather than folded in, because its guarantee is strictly stronger: it admits no allowlist at all. Both now share one _unlisted_public_attributes helper, so the two assertions cannot drift apart in what they consider a public attribute.

The docstring paragraph that scoped the converse to the aggregators is rewritten, since that scoping is what this PR changes. The "2848 names across 286 modules" measurement stays — it is still the reason the assertion is package-scoped rather than module-scoped.

Fails without the fix

Reverting only the five pcapkit/ files and keeping the test (git apply -R of the source half of the diff):

1 failed, 10 passed, 1 warning, 427 subtests passed        EXIT=1
SUBFAILED(package='pcapkit')                       test_every_public_package_exports_every_public_attribute
SUBFAILED(package='pcapkit.foundation')            test_every_public_package_exports_every_public_attribute
SUBFAILED(package='pcapkit.foundation.reassembly') test_every_public_package_exports_every_public_attribute
SUBFAILED(package='pcapkit.foundation.traceflow')  test_every_public_package_exports_every_public_attribute
SUBFAILED(package='pcapkit.utilities')             test_every_public_package_exports_every_public_attribute

One subtest per file changed, and each names exactly the omission it was written for — 12 + 6 + 1 + 1 + 3 = the 23:

pcapkit:                        ['C_Tag', 'DRARP', 'Data', 'Frame', 'HTTPv1', 'HTTPv2',
                                 'Header', 'InARP', 'L2TPv2', 'PCAPNG', 'S_Tag', 'Schema']
pcapkit.foundation:             ['register_extractor_reassembly', 'register_extractor_traceflow',
                                 'register_reassembly_ipv4_callback', 'register_reassembly_ipv6_callback',
                                 'register_reassembly_tcp_callback', 'register_traceflow_tcp_callback']
pcapkit.foundation.reassembly:  ['ReassemblyManager']
pcapkit.foundation.traceflow:   ['TraceFlowManager']
pcapkit.utilities:              ['beholder', 'prepare', 'seekset']

With the fix restored: 10 passed, 432 subtests passed, EXIT=0.

Note what that output would have hidden. pytest 9.1.1's native subtests print the parent as PASSED even when subtests fail — the run above reads "10 passed" while five subtests failed. The exit code is the only honest signal, so every number here was read from a file (print $? > …) rather than from a pipe.

test_the_non_export_allowlist_is_tight gets its own fails-without, since an assertion that cannot fail is worth nothing. Each branch was provoked separately:

mutation EXIT message
added 'pcapkit.no.such.package': ('Whatever',) 1 ['pcapkit.no.such.package'] != [] — names packages that are not in the public walk
added 'GoneAwayLongAgo' to the pcapkit entry 1 {'pcapkit': ['GoneAwayLongAgo']} != {} — names attributes these packages do not have
added 'NoPayload' to the pcapkit entry 1 {'pcapkit': ['NoPayload']} != {} — names attributes these packages now export anyway

Sphinx: 3 duplicate-object warnings before, 3 after

Full builds of docs/ at base 122d32795, one without the change and one with:

total warnings duplicate object description
122d32795, pristine 50 3
122d32795 + this change 50 3

The 3 are the known pre-existing ones, unchanged: pcapkit.corekit.infoclass.Info.__post_init__, pcapkit.foundation.reassembly.data.tcp.BufferID, pcapkit.protocols.misc.pcap.header.Header.__post_init__. The other 47 are 31 more than one target found for cross-reference (TCP ×13, UDP ×6, type, _AT, _IT, _DT, _BT, Echo, Type) and 16 artefacts of pcap/pcapfile not being installed, plus two stray prints from scapy. None of the 50 names any of the 23 exported names.

That the count could not have moved is also true by construction, and cheaper for a reviewer to check than a build is to run: docs/source/conf.py:118 sets 'ignore-module-all': True, so autodoc ignores __all__ outright when selecting members, and there is no automodule directive anywhere under docs/ — every autodoc directive in the tree is an explicit autoclass::/autofunction::/autoexception::/autodecorator:: naming a fully-qualified object. An __all__-only change therefore cannot alter what Sphinx documents. The builds confirm the argument rather than replacing it.

Lint: three pylint messages removed, none added

Running the project's own pylint invocation over the five changed files:

messages
pristine 5 — 2 wrong-import-order, and unused-import for beholder, prepare, seekset
with this change 2 — the 2 wrong-import-order

The two wrong-import-order are pre-existing, on the from typing import TYPE_CHECKING that sits below __all__ in the reassembly and traceflow packages; this change only shifts those lines by one.

The three that disappear are the interesting ones. With --init-import=yes, pylint reported beholder, prepare and seekset as unused imports in pcapkit/utilities/__init__.py because no __all__ named them — the linter had been reporting this particular omission all along, in the one form that reads as a complaint about the import rather than about the export. Adding them to __all__ is what makes the import purposeful, and it is independent corroboration that those three were an oversight rather than a decision.

isort does not apply: Makefile:125 and cron-vendor.yml:79 both pass --skip-glob '**/__init__.py', and all five changed files are __init__.py.

Suite

CI's unit-tier selection, verbatim from unit-tests.yml and tests/_tiers.py:134:

pytest tests --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py'

On the rebased tree: 1038 passed, 8 skipped, 2540 subtests passed, EXIT=0. Fully green.

Run first on base 122d32795, it was 1036 passed, 8 skipped, 2536 subtests passed with one failure — test_docstring_contract.py::DocstringParameterTests::test_known_defects_are_still_defects, on the pcapkit/vendor/ipx/packet.py entry. Confirmed pre-existing rather than assumed: reverting this branch's entire diff reproduces it identically on pristine 122d32795 (1 failed, 5 passed, 9 subtests passed, EXIT=1). #538 has since fixed it on main, and rebasing onto that clears it.

One commit, rebased onto origin/main at 691f12ab5.

Every measurement was made with PYTHONSAFEPATH=1 PYTHONDONTWRITEBYTECODE=1 and PYTHONPATH pinned to this worktree, with pcapkit.__file__ asserted and printed to be inside it — the venv has pcapkit installed editable against a different checkout, which would otherwise shadow it and produce a plausible, wrong list:

pcapkit.__file__ = /…/.claude/worktrees/agent-a8c8ce34288a3196c/pcapkit/__init__.py
in worktree      = True

…mitted (#533)

* `pcapkit/__init__.py` mirrors `pcapkit.protocols.__all__` again, restoring the
  12 protocol names it drifted behind: `C_Tag`, `S_Tag`, `DRARP`, `InARP`,
  `L2TPv2`, `HTTPv1`, `HTTPv2`, `PCAPNG`, `Header`, `Frame`, `Data`, `Schema`.
  #436 updated the child list and left the parent alone.
* `pcapkit/foundation/__init__.py` lists the six registry functions it withheld
  while listing the other 27, plus the two manager classes. #527 fixed the same
  six one level down, in `registry/__init__.py`.
* `pcapkit/foundation/reassembly/__init__.py` and `traceflow/__init__.py` export
  `ReassemblyManager` and `TraceFlowManager`, defined below the `__all__` literal
  that forgot them and imported by name from six modules.
* `pcapkit/utilities/__init__.py` exports `beholder`, `prepare` and `seekset`,
  which its own module docstring advertises and which pylint reported as unused
  imports precisely because no `__all__` named them.
* `tests/project/test_public_api.py` gains the converse assertion over all 52
  public packages against `DELIBERATE_NON_EXPORTS`, plus a test that stops that
  allowlist rotting into a silencer.

Nothing outside an export list changed, and nothing reads the five lists as data.
Unit tier 1036 passed / 8 skipped / 2536 subtests, with one pre-existing
docstring-registry failure that reproduces identically on 122d327. pylint drops
three messages and adds none.
@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — independently re-derived the export-gap count from scratch with my own AST/introspection script (not copied from the PR's test code) and got an exact match at every stage: 45 unlisted names across 12 packages on main (122d327) and at the #527 merge commit (0f4beef, confirming the PR's docstring correction from "13" to "12" packages), dropping to exactly 22 across 10 after this PR's 25 additions -- name-for-name identical to the PR's tables. One judgement call for the owner: withholding the 10 abstract base classes is well-supported (corroborated independently via pcapkit/all.py genuinely exposing none of them, and pcapkit.corekit's docstring referencing Field by its defining path while listing its 25 -- not 22, a factual correction -- concrete *Field subclasses), but the PR's own "nine packages agree via a # Base Class for X comment" claim is slightly overstated -- I count 8, with Field's own package carrying no such comment at all, though Field's exclusion still holds up on the other two lines of evidence. Neither correction changes the substance or blocks the merge.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Detailed review (independent verification, falsify-not-bless)

Head sha reviewed: 7b211fae2ac4728d95a725c8faaba42fd3a8e302.

The core count -- re-derived from scratch, not copied from the PR's own scanner

I wrote my own script (not the PR's test_public_api.py code, though it necessarily applies the same defining rule the issue and the existing test_aggregators_export_every_public_attribute already use: for each public package, dir(module) minus underscore-prefixed names, minus anything already in __all__, minus module objects) and ran it independently at three points:

  • main (122d327): 45 unlisted names across exactly 12 packages. Every single name matched the PR's table exactly, package for package: pcapkit (16: the 12 it adds plus BaseError/DEVMODE/DevModeWarning/ROOT it withholds), pcapkit.corekit.fields (Field), pcapkit.foundation (the 6 register_*), pcapkit.foundation.engines (Engine), pcapkit.foundation.reassembly (6: ReassemblyManager added, 5 withheld), pcapkit.foundation.traceflow (5: TraceFlowManager added, 4 withheld), pcapkit.protocols (Protocol, TYPE_CHECKING, name), and the four layer packages (Application, Internet, Link, Transport).
  • The fix(protocols): repair __all__ entries that name nothing, and guard the class #527 merge commit (0f4beeff6): identical result, 45 across 12 -- confirming the PR's claim that fix(protocols): repair __all__ entries that name nothing, and guard the class #527's own docstring ("45 across 13") had the package count one high. I checked the actual pre-fix line (tests/project/test_public_api.py:60 on that commit and on main) and the corrected line on this PR's head (:67, now reading "45 across 12"), and the correction is real and matches what I independently measured.
  • This PR's head: 22 unlisted names across exactly 10 packages -- again an exact match, and the residual 22 map precisely onto the PR's DELIBERATE_NON_EXPORTS categories (10 abstract bases, 7 dragged-in names split across Info/info_final/TYPE_CHECKING, 4 import-time machinery, 1 leaked loop variable).

Fails-without proof -- reproduced, with one cosmetic tallying difference noted

Reverted the five source files to main (git checkout 122d32795 -- <5 files>), kept the extended test: pytest tests/project/test_public_api.py -v, exit code read from a file: exit 1, with exactly five SUBFAILED(package=...) entries under test_every_public_package_exports_every_public_attribute -- pcapkit (12 names), pcapkit.foundation (6), pcapkit.foundation.reassembly (1), pcapkit.foundation.traceflow (1), pcapkit.utilities (3), totalling 12+6+1+1+3 = 23, matching the PR's claim exactly, down to the specific names in each AssertionError. My run's top-line summary read "5 failed, 10 passed, 427 subtests passed" where the PR's reads "1 failed, 10 passed, 427 subtests passed" -- a cosmetic difference in how pytest aggregates a multi-subtest parent into the headline count (427 + 5 = 432, matching the clean run's total either way), not a substantive one; the exit code, the subtest count, and every named omission match precisely. Restored the five files and re-ran: exit 0, 10 passed, 432 subtests passed, matching exactly.

Also separately provoked test_the_non_export_allowlist_is_tight's three failure branches by reading the code rather than re-running each by hand, given time budget -- did not independently re-derive this table's numbers, flagged below.

The abstract-base exclusion argument -- two corrections found, conclusion unaffected

This is the PR's single biggest judgement call (10 of the 22 residual names), so I checked all three of its supporting claims independently:

  1. "Nine packages... each import theirs explicitly under a # Base Class for ... comment... the same way." Grepped for the literal pattern across every package __init__.py: it appears in exactly 7 files (protocols, foundation/reassembly -- which covers two of the ten names, Reassembly and IP_Reassembly, under one comment -- protocols/link, protocols/internet, protocols/application, foundation/traceflow, protocols/transport). pcapkit/foundation/engines/__init__.py has a differently worded marker, bare # Base class (no "for X"), which I found only by grepping case-insensitively for "base class" rather than the exact phrase -- bringing the real count to 8 packages, not 9, covering 9 of the 10 names. pcapkit/corekit/fields/__init__.py has no such comment at all before from pcapkit.corekit.fields.field import Field -- it is a bare import with nothing distinguishing it from the 22-odd concrete field imports around it. So the PR's "nine packages... the same way" overstates the convention: it is 8 packages with two different comment wordings, and the tenth base (Field) is the one exception that does not follow this pattern.
  2. "pcapkit/all.py lists no base class at all." Confirmed properly by importing pcapkit.all and checking hasattr/in __all__ for all ten names directly: all ten are absent. (My first pass at this, a bare text grep for the ten words, found five "hits" -- but every one was inside a # Section Header comment such as # Engine Macros or # Internet Layer, not an actual import or __all__ entry; re-checked against the live module object rather than the source text once I noticed this, and the PR's claim holds.)
  3. "pcapkit.corekit lists the 22 concrete *Field classes and not Field." Field is confirmed absent from pcapkit.corekit.__all__, and the module docstring does reference it by its defining dotted path (:class:`~pcapkit.corekit.fields.field.Field`, corekit/__init__.py:15) exactly as claimed. But the count of concrete *Field names in that same __all__ is 25, not 22 -- counted directly from pcapkit.corekit.__all__ and cross-checked against pcapkit.corekit.fields.__all__, both giving 25. This is a plain factual error in the PR's number, though it does not change the shape of the argument (Field itself is genuinely excluded while a large family of its concrete subclasses is listed either way).

Net: the exclusion of all 10 abstract bases still reads as correct to me -- the surviving evidence (8 packages via a comment convention plus 2 independent corroborations for Field specifically) is enough on its own -- but the PR overstates one supporting count ("nine" -> 8) and gets a second one wrong ("22" -> 25). Worth a glance from the owner since this is explicitly flagged as the call to overturn if wrong, but neither correction moves me toward NEEDS CHANGES.

pcapkit.utilities's docstring justification -- confirmed exactly

pcapkit/utilities/__init__.py's own module docstring: "including decorater function :func:~pcapkit.utilities.decorators.seekset and :func:~pcapkit.utilities.decorators.beholder, etc." -- confirms the PR's citation precisely (it names two of the three explicitly and covers the third, prepare, with "etc.").

pylint -- confirmed exactly, including the score delta

Ran the project's own Makefile:135 invocation over the five changed files, before and after:

  • Pristine (main's versions): 5 real messages -- 2 wrong-import-order (reassembly, traceflow) and 3 unused-import (beholder, prepare, seekset in pcapkit/utilities/__init__.py), rated 9.14/10.
  • This PR's versions: 2 messages -- the same 2 wrong-import-order, nothing else, rated 9.66/10 (+0.52).

Matches the PR's claim exactly, including the specific score delta.

Sphinx claim -- confirmed directly, without running either build

Rather than running the ~13-minute build twice, I confirmed the PR's mechanism claim directly: docs/source/conf.py:118 sets 'ignore-module-all': True, and grep -rn automodule docs/ returns zero hits anywhere in the tree -- every autodoc directive is an explicit autoclass::/autofunction::/etc. naming a fully-qualified object. This is a stronger and cheaper confirmation than running the build twice and diffing warning counts, since it shows why the warning count cannot have moved rather than merely observing that it did not.

CI status

Not run -- GitHub Actions backed up throughout this review session (25 completed / 31 queued / 1 in-progress at last check, not yet reaching this branch); verdict on local evidence only, per standing instruction.

What remains unverified

  • test_the_non_export_allowlist_is_tight's three failure-mode table rows (stale package, stale attribute, over-tight entry) were read from the code rather than independently re-provoked one at a time.
  • The full unit-tier suite (1038 passed, 8 skipped, 2540 subtests) was not independently re-run in full; only tests/project/test_public_api.py was run directly.
  • mypy/bandit/isort were not independently re-run (isort's inapplicability, via the --skip-glob '**/__init__.py' flags, was confirmed by reading the Makefile and workflow file rather than running it).

@JarryShaw

Copy link
Copy Markdown
Owner Author

Flagging a conflict with this PR's biggest judgement call, so it is visible where the merge decision gets made rather than only on another thread.

This PR deliberately withholds ten abstract bases from their package __all__Field, Engine, Reassembly, IP_Reassembly, TraceFlow, Protocol, Application, Internet, Link, Transport — and says so plainly, calling it "10 of the 22 and the single biggest decision in the triage" and "If the owner disagrees, this is the judgement call to overturn".

On #514 the owner has just disagreed. Asked whether his answer overturned this, he replied (#514, 2026-09-20T07:23:52Z):

Q9: yea now i rethink about this, we should probably export those so that they can be used for extension and/or the extracted until layer use cases. Your recommendation makes sense now.

His earlier answer in the same thread pointed the same way — "what public classes? If it's the layer's base classes, those are needed."

I am not proposing an edit to this PR and have not touched it. Two notes that may bear on how it is resolved:

  1. The recommendation he agreed to was narrower than all ten. I recommended exporting the five layer classes — Protocol, Link, Internet, Transport, Application — on the grounds that those are what a user subclasses, and leaving Field, Engine, Reassembly, IP_Reassembly and TraceFlow alone for now. "Your recommendation makes sense now" reads as agreement with that split rather than with all ten, but it is worth confirming which he means before either list is acted on.
  2. "the extracted until layer use cases" is the Extractor until semantics, i.e. a caller naming a layer to stop at. That is worth checking against the code before it is used as the justification: if until accepts the layer name or the concrete protocol class, the bases may not need exporting for that particular use case, and the extension argument would be carrying the decision on its own. I have not measured it and am not claiming either way.

Whichever way it goes, the allowlist reasoning in this PR is the right place for it to be recorded — that is what makes the conflict legible at all, rather than the names simply staying absent.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Follow-up to the above, now measured rather than flagged: one of the owner's two justifications is not a justification.

extract(layer=...) reduces a class to its __layer__ string — interface/core.py:154, layer = (layer.__layer__ or 'none').lower() — and __layer__ is inherited, not per-class:

Transport.__layer__ = 'Transport'    TCP.__layer__ = 'Transport'    '__layer__' in TCP.__dict__ -> False
Link.__layer__      = 'Link'         Ethernet.__layer__ = 'Link'

So extract(layer=TCP) and extract(layer=Transport) are already the same call, and the "extracted until layer" use case does not need any base class exported. The extension use case still does, which is why I read his answer as supporting the five layer classes (Protocol, Link, Internet, Transport, Application) rather than all ten — one reason, not two.

@JarryShaw
JarryShaw merged commit f3126e8 into main Sep 20, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the fix/533-unlisted-public-exports branch September 20, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

~45 public attributes are missing from their package __all__, invisible to star-import and to the export-integrity tests

1 participant