Skip to content

test(utilities,foundation): pin the quiet StreamEOFError convention and register_extractor_engine's keyword - #577

Merged
JarryShaw merged 3 commits into
mainfrom
test/keyword-name-and-quiet-eof-regressions
Sep 21, 2026
Merged

JarryShaw merged 3 commits into
mainfrom
test/keyword-name-and-quiet-eof-regressions

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Summary

Recovered and triaged an abandoned, uncommitted local change (never pushed, found only in a stale worktree) from the same #514 keyword-naming audit that produced #547, #557 and #570. Most of it turned out to already be shipped or actively contradicted by what shipped; this PR keeps only the part that is still novel.

  • StreamEOFError's docstring did not document that @prepare always raises it with quiet=True -- the same end-of-stream convention StructError follows via its own eof=True -- and nothing pinned that silence with a test. Added the docstring note and two regression tests: one proving @prepare's StreamEOFError logs nothing, and a loud control proving the silence comes from quiet=True specifically rather than StreamEOFError having stopped logging altogether.
  • register_extractor_engine's real keyword is name, not engine -- an earlier audit (noted in fix(foundation): rename Engine's registry keyword to engine, which works on Python 3.10 (#514) #557's own description) found the docstring said engine while the signature said name. The docstring has since been corrected, but the keyword itself was never under test, so a future rename could put the two back out of step exactly as silently as before. Added a test pinning both directions: name= registers, engine= raises TypeError.

What was dropped, and why

The inherited diff also touched Engine/Reassembly/TraceFlow's __init_subclass__ docstrings (fixing a name:/protocol: Args: mismatch) and register_extractor_engine's docstring (engine: -> name:), plus two new tests asserting that an unrecognised name= class keyword on Reassembly/TraceFlow is silently swallowed and falls back to the class name.

All of this is superseded by #547 and #557, which:

  • Rewrote all three __init_subclass__ docstrings from scratch for opt-in registration (confirmed by rebasing the inherited diff onto current main: real conflicts in all three files, because the Args: text being edited no longer exists in that shape).
  • Already carry the corrected register_extractor_engine docstring verbatim (git diff origin/main on that file, before any edit, was empty).
  • Made an unrecognised class keyword loud (UnsupportedCall) instead of silently falling back to the class name. Running the two "swallowed keyword" tests from the inherited diff against current main fails them outright, since the exact behaviour they pin no longer exists.

Test plan

  • Both new tests proven to fail without their fix (temporarily reverted quiet=True in decorators.py, and temporarily renamed register_extractor_engine's name parameter to engine; each broke the corresponding new test, then reverted cleanly).
  • tests/utilities/test_quiet_exceptions.py, tests/foundation/registry/test_foundation_keyword_names.py, tests/foundation/registry/test_foundation.py, tests/utilities/test_decorators.py: 36 passed, 14 subtests passed, exit 0.
  • tests/project/test_public_api.py, test_documentation_claims.py, test_changelog_md.py: 47 passed, 469 subtests passed, exit 0 (no new public API surface added).
  • util/changelog_md.py --check: CHANGELOG.md in step with docs/source/changelog/1.5.0.rst.
  • Full unit tier (CI's selection, samples regenerated first via examples/generators/make_samples.py): pytest -q --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py' -- 1123 passed, 5 skipped, 2704 subtests passed, exit 0.
  • mypy pcapkit/utilities/exceptions.py: Success: no issues found.
  • pylint pcapkit/utilities/exceptions.py (unused-import/undefined-variable/unused-variable/no-member): 10.00/10.

…nd register_extractor_engine's keyword

Two gaps the #514 keyword audit turned up while checking whether an
abandoned local change was still needed. Neither had a test before,
and both turned out to already be correct on main -- just unguarded.

- StreamEOFError's docstring did not say that @prepare always raises
  it with quiet=True (the same end-of-stream convention StructError
  follows via its own eof=True), and nothing pinned that silence.
  Document it, and add a test proving @prepare's StreamEOFError logs
  nothing, with a loud control proving the silence comes from
  quiet=True and not from StreamEOFError having stopped logging
  altogether.
- register_extractor_engine's real keyword is `name`, not `engine`;
  an earlier audit (noted on #557) found it documented the other way
  around. The docstring was already fixed, but the keyword itself was
  never under test, so a future rename could put docstring and
  signature back out of step exactly as quietly as before. Pin both
  directions: `name=` registers, `engine=` raises TypeError.

The rest of the inherited local change -- docstring edits to
Engine/Reassembly/TraceFlow's __init_subclass__ and two tests
asserting that an unrecognised `name=` class keyword is silently
swallowed -- is superseded by #547/#557, which made registration
opt-in and rejects an unrecognised class keyword with
UnsupportedCall instead of swallowing it. Confirmed by rebase
conflict (the Args: text they edited no longer exists) and by
running the swallowed-keyword tests against current main, where they
fail because the keyword is now rejected loudly rather than ignored.

Build: full unit tier (pytest -q --ignore=tests/integration
--ignore-glob='*_runtime.py' --ignore-glob='*_regression.py'):
1123 passed, 5 skipped, 2704 subtests passed, exit 0. Both new tests
proven to fail without their fix.
@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — at db84b4935.

Cross-model review (Opus 5; PR authored on Sonnet). Reviewed first at 118daf844, then re-pointed after the head moved: db84b4935 is a merge commit bringing in the six PRs that landed on main this afternoon, and this PR's own three reviewed files are byte-identical across it (git diff 118daf844 db84b4935 -- pcapkit/utilities/exceptions.py tests/utilities/test_quiet_exceptions.py tests/foundation/registry/test_foundation_keyword_names.py is empty). Own content against current main 1c5833e00: 5 files, +137/−0.

What I derived myself rather than took from the description:

  • The docstring Note: is not just accurate, it is complete. StreamEOFError has exactly one raise site in all of pcapkit/decorators.py:270, raise StreamEOFError('prepare: end of stream', quiet=True). So "@prepare always raises this with quiet=True" cannot be falsified by some other site.
  • The coverage gap was real. tests/utilities/test_decorators.py:227 and :251 are bare with self.assertRaises(self.exceptions.StreamEOFError): — type only, never silence.
  • I proved both new tests fail without their fix, myself. Removing quiet=Truetest_prepare_raises_stream_eof_error_quietly fails, exit 1 read from a file, with [('CRITICAL', 'StreamEOFError: prepare: end of stream')] != []; the loud control still passed, which is exactly what makes the silence assertion meaningful. Renaming register_extractor_engine's nameengineboth keyword tests fail, exit 1. Both reverts restored with empty git diff/git status.
  • The "what was dropped" account holds, measured through real class statements (never type()): Reassembly/TraceFlow with name=UnsupportedCall: unexpected class keyword(s): name; Engine with engine= accepted and with name=UnsupportedCall; the register_extractor_engine docstring already says name: engine name with no engine: label.
  • 36 passed, 14 subtests, exit 0 at this head; mypy pcapkit/utilities/exceptions.py clean; util/changelog_md.py --check in step.

Two things I did not verify, and one note worth acting on eventually, in the detailed comment. Nothing blocking.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Detailed cross-review — #577 @ db84b4935

Reviewer: Opus 5, per the standing rule that an agent-raised PR gets a cross-review from a different model than the one that wrote it. All runs with PYTHONSAFEPATH=1 and PYTHONPATH pinned to my worktree, asserting pcapkit.__file__ resolves inside it before importing anything else. Exit codes read from files.

Scope, after the head moved

I reviewed 118daf844, then main advanced by six merges (#561, #562, #565, #568, #569, #570) and this branch picked them up. db84b4935 is a merge commit, not a rebase, so the PR now carries two commits:

db84b4935 Merge branch 'main' into test/keyword-name-and-quiet-eof-regressions
118daf844 test(utilities,foundation): pin the quiet StreamEOFError convention and …

Merge base is current main 1c5833e00, so it is up to date. Own content is unchanged — 5 files, +137/−0 — and the three files I actually reviewed are byte-identical across the merge, so the review transfers rather than needing redoing. Neither pcapkit/utilities/decorators.py nor pcapkit/foundation/registry/foundation.py was touched by any of the six merged PRs, so my revert-proofs below (performed at 118daf844) still describe this head's code.

A merge commit is not a defect in this repo — flagging it only because it makes the "one commit" reading of the PR wrong, and because the diff between the two heads is 26 files and 1871 insertions, which looks alarming until you notice it is the six merged PRs arriving.

Claim 1 — the quiet StreamEOFError convention

The gap was real. The two existing tests that reach this path assert the exception type and nothing else:

tests/utilities/test_decorators.py:227   with self.assertRaises(self.exceptions.StreamEOFError):
tests/utilities/test_decorators.py:251   with self.assertRaises(self.exceptions.StreamEOFError):

Neither looks at logging. So "nothing pinned that silence" is accurate.

The new Note: is accurate, and stronger than it claims. grep -rn "StreamEOFError(" pcapkit/ returns exactly two lines — the class definition at exceptions.py:420 and a single raise site:

pcapkit/utilities/decorators.py:270    raise StreamEOFError('prepare: end of stream', quiet=True)

One raise site in the entire package, and it passes quiet=True. "@prepare always raises this with quiet=True" is therefore not merely true of the sites checked, it is exhaustive. I also confirmed the surrounding code reaches it only for a derived zero length (declared_length = length is not None; if length == 0 and not declared_length), which is what the existing #458 distinction turns on — so the Note: does not accidentally overclaim about the declared-zero case.

I proved the test fails without the fix rather than accepting the checkbox. Removing quiet=True from decorators.py:270:

FAILED tests/utilities/test_quiet_exceptions.py::QuietExceptionTests::test_prepare_raises_stream_eof_error_quietly
E  AssertionError: Lists differ: [('CRITICAL', 'StreamEOFError: prepare: end of stream')] != []
1 failed, 1 passed, 7 deselected        exit code from file: 1

The 1 passed there is the point worth drawing out: the loud control test still passed under the reverted code. That is what distinguishes this pair from a test that would also pass if StreamEOFError had simply stopped logging altogether, and it is the reason the control earns its place rather than being redundant. Restored, git diff and git status both empty.

The test's construction is also right in a way that is easy to get wrong: DemoSchema.unpack(io.BytesIO(b''), None, None) puts length in args[2] as None, so declared_length is False and the derived length is 0 — genuinely the exhausted-stream branch, not the declared-zero one. And @self.decorators.prepare inside a class body inside a method does resolve, since a class body can read enclosing-function locals.

Claim 2 — register_extractor_engine's keyword

Verified against the real signature, not the docstring:

[signature] (name: 'str', module: 'ModuleDescriptor[Engine] | Type[Engine] | str', class_: 'str' = '(null)') -> 'None'
[docstring] "name: engine name" present?  True
[docstring] any "engine:" Args label?     False

So the docstring on main is already correct, which corroborates the PR's account of why the salvaged registry/foundation.py edit merged with zero conflict — there was nothing left to change.

I proved both tests fail under the rename. Renaming the parameter to engine (and its two body references):

FAILED …::test_register_extractor_engine_signature_names_name
FAILED …::test_register_extractor_engine_takes_name_not_engine
E  TypeError: register_extractor_engine() got an unexpected keyword argument 'name'
2 failed        exit code from file: 1

Restored; git diff empty. The mocked Extractor.register_engine means neither test leaves a real registration behind, so there is no global-state pollution.

One honest correction to how the PR frames this. The test is described as pinning "the pairing", and it pins one side of it: that the function parameter is name. It does not pin that the class keyword is engine — a future tidy-up that unified them by renaming the class keyword back to name would sail past this file. That direction is already guarded elsewhere, and guarded well:

  • tests/foundation/engines/test_engine_base.py:82-93, :129-135 — explains the mcls/name/bases/namespace collision with abc.ABCMeta.__new__, positional-or-keyword on 3.10 and positional-only from 3.11, with results measured on 3.10.21 and 3.14.7.
  • tests/foundation/reassembly/test_reassembly_base.py:186-190, :226-231 — the same, for Reassembly.

I confirmed that behaviour myself through real class statements rather than trusting the docstrings, because a type(name, bases, ns, **kw) probe consumes metaclass= itself and never reaches __init_subclass__, which has already misled one probe in this programme:

class DemoReassembly(Reassembly, name='…')     -> UnsupportedCall: unexpected class keyword(s): name
class DemoTraceFlow(TraceFlow, name='…')       -> UnsupportedCall: unexpected class keyword(s): name
class DemoEngine(Engine[str], engine='…')      -> accepted
class DemoEngine2(Engine[str], name='…')       -> UnsupportedCall: unexpected class keyword(s): name

That last pair is the deliberate divergence: engine is the class keyword, name is the function parameter, and they are different on purpose.

So the recommendation, non-blocking: the new test file's docstring records only the docstring-drift history. It does not say that the two spellings differ deliberately, nor that unifying them in the other direction would reintroduce the 3.10 metaclass collision. One sentence cross-referencing test_engine_base.py would close that, and is worth adding, because a reader who hits this test while "tidying up the inconsistency" currently gets the weaker of the two available reasons not to. I am not blocking on it: the coverage is correct, the rationale exists in the suite, and arguably test_engine_base.py is the better home for the metaclass story than a registry-keyword test is.

Also worth recording, since it is the same "silently swallowed" behaviour the salvage dropped: the two discarded tests would indeed fail against main, because an unrecognised class keyword now raises UnsupportedCall rather than falling back to the class name. Measured above. The decision to drop them rather than update them is right — they pinned behaviour #547 deliberately removed.

Runs at this head

run exit result
pytest tests/utilities/test_quiet_exceptions.py tests/foundation/registry/test_foundation_keyword_names.py tests/utilities/test_decorators.py tests/foundation/registry/test_foundation.py 0 36 passed, 14 subtests
mypy pcapkit/utilities/exceptions.py 0 Success: no issues found in 1 source file
util/changelog_md.py --check 0 CHANGELOG.md in step with docs/source/changelog/1.5.0.rst

The 36/14 tally matches the PR body's claim exactly.

What I could not verify

Stating these rather than implying coverage I do not have:

  1. The full unit tier — not reproduced by me. I started pytest -q --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py' at 118daf844 and stopped it at roughly 51% when the head moved, rather than report a measurement of a superseded tree. So the body's 1123 passed, 5 skipped, 2704 subtests is unconfirmed by me. I judged the residual risk low: the only production change is an eight-line docstring Note:, and the plausible cross-file hazard — the new test file's purge_modules(['pcapkit']) polluting later tests — is already exercised by the four-file run above, and purge_modules is an established pattern in this suite rather than something this PR introduces. Low is not zero, and CI covers it.
  2. The abandoned worktree's original contents, and the rebase conflicts in engine.py/reassembly.py/traceflow.py. The uncommitted source tree is not available to me, so the account of what was found and what conflicted is unfalsifiable from here. What I can confirm is the consequence the PR draws from it — that those three __init_subclass__ docstrings no longer contain the Args: text being edited, and that the dropped tests' behaviour no longer exists — and both hold.
  3. CI, which I did not wait on, per standing practice (the ruleset grants an admin bypass, and waiting costs ten minutes a round).

Verdict

GOOD TO MERGE at db84b4935. Test-only in substance, the one production line is a docstring Note: that is exhaustively true, both new tests are proven sensitive to their subject by my own reverts rather than by assertion, and the salvage triage — keeping two claims and discarding the rest as superseded — is the right call and is accurately described.

One recommendation carried forward rather than blocked on: add a sentence to test_foundation_keyword_names.py recording that name (function) and engine (class keyword) differ deliberately, pointing at test_engine_base.py for the 3.10 metaclass reason.

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — re-pointed to 35534d5b0.

The delta from db84b4935, where the cross-review was done, is exactly your merge of main — the four already-merged PRs #578, #579, #580, #581 — and it touches none of this PR's own files: git diff db84b4935 35534d5b0 -- tests/utilities tests/foundation is empty. So the earlier verdict carries over unchanged rather than needing a fresh pass.

CI at this head is complete and clean: 22 CheckRuns SUCCESS, 0 FAILURE, 2 SKIPPED (Docs test gate, Gate (full suite, Python 3.14)), plus pyup.io/safety-ci SUCCESS.

Worth noting for the failure you hit before the merge: Integration Python 3.14 passed here, so it did not recur and this PR did not cause it. The mechanism is a pre-existing unclosed in.pcap handle interacting with the exact-warning-count assertion in tests/utilities/test_stacklevel.py::test_a_truncated_traceback_limit_does_not_blind_the_walk, which fails AssertionError: 2 != 1 when any unrelated ResourceWarning lands in its window. The likely reason it cleared is that #580 edited the two test files #578 had just added, shifting collection order — that part is inference, not measured. Both underlying defects are still there, so the failure can return on a future run with no code change. They deserve their own issue; neither is a reason to hold this PR.

@JarryShaw
JarryShaw merged commit 9c240a6 into main Sep 21, 2026
25 checks passed
@JarryShaw
JarryShaw deleted the test/keyword-name-and-quiet-eof-regressions branch September 21, 2026 20:18
JarryShaw added a commit that referenced this pull request Sep 22, 2026
… own (#606)

The stacklevel probes record with `simplefilter('always')`, which un-ignores the
categories Python hides by default, then asserted their window held exactly one
record and took the last of it. A `ResourceWarning` for a file handle an earlier
test left for the collector therefore failed them, and did so unpredictably: the
same failure was watched migrating between #596 and #600 nine minutes apart with
neither branch touched and neither head moved, so what trips is decided by
garbage-collection timing and collection order rather than by any test's code.
It had reddened #577, #596 and #600 by then, none of which the test exercises.

* `tests/utilities/test_stacklevel.py`: count the window's pcapkit warnings via
  a new `emissions()` helper and check the one found is the expected category,
  instead of requiring the probe's to be the only record there and picking it by
  position. Counting over `BaseWarning` rather than the exact category keeps what
  the old assertion did buy -- a second complaint from the package is still a
  failure -- and gives up only its sensitivity to warnings pcapkit never raised.
  Both probe helpers were affected, `warning_site()` and `emit()`. Adds a
  regression test that frames the probe with foreign warnings on both sides.
* `tests/utilities/test_logging.py`: enter each `Extractor` as a context manager
  so `__exit__` closes the input file, removing the leaked `in.pcap` handle these
  three constructions were shedding into sibling modules' tests.

`pytest tests/utilities/` 103 passed, and with `-W always::ResourceWarning` the
unclosed-`in.pcap` warnings go from 2 to 0. The new test fails `3 != 1` without
the fix. The leak's root cause is left alone deliberately: `Extractor._cleanup()`
closes the handle only when the caller supplied the stream, never when pcapkit
opened it, and that is production code wanting its own review.

Fixes #606
@JarryShaw JarryShaw added the test Pull requests that add or correct tests (test: subject prefix) label Sep 22, 2026
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…two false packaging claims (#642)

Three names appeared in string annotations that their module never imported, and
they were mypy's complete set of ``name-defined`` findings for the package:

* ``pcapkit/utilities/logging.py:350`` used ``Any``; added to the
  ``TYPE_CHECKING`` block beside ``IO``, ``Optional`` and ``Union``.
* ``pcapkit/protocols/schema/internet/ipv6_route.py:136`` used ``Protocol`` and
  ``:271`` used ``Optional``; added, ``Protocol`` as
  ``ProtocolBase as Protocol``. Twenty-one sibling schema modules already spell
  it that way, in the same ``payload:`` stub; this makes twenty-two.

mypy 2.3.1 over ``pcapkit``: 3 ``name-defined`` errors before, 0 after; 115 total
errors before, 112 after, so nothing else moved.

``MANIFEST.in:14-17`` asserted that ``include README.md`` was "the only thing
that puts it in an sdist" and that an sdist without it "cannot be installed at
all". Both halves are false, and the file had contradicted itself since #631
wrote the correct mechanism seven lines below without correcting this. Deleting
the lines and rebuilding gives a byte-identical sdist listing -- empty diff --
that installs with exit 0: ``setuptools/command/sdist.py:59-60`` ships the README
unconditionally and ``setuptools/dist.py:460``'s default ``license_files`` glob
ships ``LICENSE``. Of the original three ``include`` lines only ``CHANGELOG.md``
is load-bearing. The comment now says that, and the same correction is applied to
the #619 entry in ``docs/source/changelog/1.5.0.rst``, which carried the
identical claim.

Three changelog bullets gained the citation they were missing: ``(#619)`` for the
README entry, ``(#570)`` for the ``code=`` keyword and ``(#577)`` for the
``StreamEOFError`` gaps. Two of the five remain uncited because no number could
be recovered for them.

``CHANGELOG.md`` regenerated with ``util/changelog_md.py``; ``--check`` exits 0.

New ``tests/project/test_annotation_names.py`` resolves every string annotation
in the package -- following a nested forward reference such as
``'list["Nested"]'``, while treating ``Literal`` members and ``Annotated``
metadata as the values they are -- against the names its own module binds. It
reports the same three findings as mypy on the unfixed tree and none after.
128 -> 132 passed over ``tests/project`` and ``tests/utilities/test_logging.py``,
both exit 0, subtests unchanged at 487.
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…two false packaging claims (#642)

Three names appeared in string annotations that their module never imported, and
they were mypy's complete set of ``name-defined`` findings for the package:

* ``pcapkit/utilities/logging.py:350`` used ``Any``; added to the
  ``TYPE_CHECKING`` block beside ``IO``, ``Optional`` and ``Union``.
* ``pcapkit/protocols/schema/internet/ipv6_route.py:136`` used ``Protocol`` and
  ``:271`` used ``Optional``; added, ``Protocol`` as
  ``ProtocolBase as Protocol``. Twenty-one sibling schema modules already spell
  it that way, in the same ``payload:`` stub; this makes twenty-two.

mypy 2.3.1 over ``pcapkit``: 3 ``name-defined`` errors before, 0 after; 115 total
errors before, 112 after, so nothing else moved.

``MANIFEST.in:14-17`` asserted that ``include README.md`` was "the only thing
that puts it in an sdist" and that an sdist without it "cannot be installed at
all". Both halves are false, and the file had contradicted itself since #631
wrote the correct mechanism seven lines below without correcting this. Deleting
the lines and rebuilding gives a byte-identical sdist listing -- empty diff --
that installs with exit 0: ``setuptools/command/sdist.py:59-60`` ships the README
unconditionally and ``setuptools/dist.py:460``'s default ``license_files`` glob
ships ``LICENSE``. Of the original three ``include`` lines only ``CHANGELOG.md``
is load-bearing. The comment now says that, and the same correction is applied to
the #619 entry in ``docs/source/changelog/1.5.0.rst``, which carried the
identical claim.

Three changelog bullets gained the citation they were missing: ``(#619)`` for the
README entry, ``(#570)`` for the ``code=`` keyword and ``(#577)`` for the
``StreamEOFError`` gaps. Two of the five remain uncited because no number could
be recovered for them.

``CHANGELOG.md`` regenerated with ``util/changelog_md.py``; ``--check`` exits 0.

New ``tests/project/test_annotation_names.py`` resolves every string annotation
in the package -- following a nested forward reference such as
``'list["Nested"]'``, while treating ``Literal`` members and ``Annotated``
metadata as the values they are -- against the names its own module binds. It
reports the same three findings as mypy on the unfixed tree and none after.
127 -> 132 passed over ``tests/project`` and ``tests/utilities/test_logging.py``,
both exit 0, subtests unchanged at 487.
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…a false packaging claim (#642)

Three names appeared in string annotations that their module never imported, and
they were mypy's complete set of ``name-defined`` findings for the package:

* ``pcapkit/utilities/logging.py:350`` used ``Any``; added to the
  ``TYPE_CHECKING`` block beside ``IO``, ``Optional`` and ``Union``.
* ``pcapkit/protocols/schema/internet/ipv6_route.py:136`` used ``Protocol`` and
  ``:271`` used ``Optional``; added, ``Protocol`` as
  ``ProtocolBase as Protocol``. Twenty-one sibling schema modules already spell
  it that way, in the same ``payload:`` stub; this makes twenty-two.

mypy 2.3.1 over ``pcapkit``: 3 ``name-defined`` errors before, 0 after; 115 total
errors before, 112 after, so nothing else moved.

``MANIFEST.in:14-17`` asserted that ``include README.md`` was "the only thing
that puts it in an sdist" and that an sdist without it "cannot be installed at
all". Both halves are false, and the file had contradicted itself since #631
wrote the correct mechanism seven lines below without correcting this. Deleting
the lines and rebuilding gives a byte-identical sdist listing -- empty diff --
that installs with exit 0: ``setuptools/command/sdist.py:59-60`` ships the README
unconditionally and ``setuptools/dist.py:460``'s default ``license_files`` glob
ships ``LICENSE``. Of the original three ``include`` lines only ``CHANGELOG.md``
is load-bearing. The comment now says that.

New ``tests/project/test_annotation_names.py`` resolves every string annotation
in the package -- following a nested forward reference such as
``'list["Nested"]'``, while treating ``Literal`` members and ``Annotated``
metadata as the values they are -- against the names its own module binds. It
reports the same three findings as mypy on the unfixed tree and none after.

That module named ``ast.TypeAlias`` and ``ast.TypeVar`` directly, and both are
PEP 695 nodes added in Python 3.12, so *every* test in it raised
``AttributeError`` on the 3.10 and 3.11 matrix jobs -- ``bound_names`` walks every
node of every file, so the attribute is reached whatever a test does. Both are now
resolved once at module scope through ``getattr(ast, ..., ())``, leaving the
``isinstance`` branches otherwise untouched: ``isinstance(x, ())`` is always
False, so the branches stay live on 3.12+ and are simply unreachable below it.
Chosen over a ``sys.version_info`` comparison because it writes no version number
down at all -- a comparison states 3.12 next to the attribute it guards, and the
two can then drift -- and over a per-node ``getattr`` because a module-level
constant lifts the lookup out of a loop that runs on every node of every file.

``ast.TypeVar`` is the branch that earns its keep: it carries its name as a bare
``str`` and emits no ``ast.Name`` node, so forcing ``_TYPE_VAR`` to ``()`` on
3.14.7 turns ``T`` and ``U`` into false findings. ``ast.TypeAlias`` is defensive
by comparison -- its name *is* an ``ast.Name`` in ``Store`` context, which the
preceding branch already catches -- and is left as it stands rather than removed.
A new ``test_a_pep695_type_parameter_is_in_scope`` pins both the guards and the
behaviour, skipped below 3.12 because its fixture source cannot parse there.

Measured on real interpreters rather than simulated. 3.10.21 and 3.11.15:
5 failed, exit 1 -> 5 passed, 1 skipped, exit 0. 3.14.7: all 6 pass, exit 0.
133 passed over ``tests/project`` and ``tests/utilities/test_logging.py``, exit 0,
subtests unchanged at 487.

No changelog entry on this branch. Per the rule that no code branch touches
``CHANGELOG.md`` or anything under ``docs/source/changelog/``, this change's entry
-- and the wording correction the ``MANIFEST.in`` claim implies for the #619
entry, plus the missing ``(#570)`` and ``(#577)`` citations -- go to the shared
changelog pull request #657 instead.
JarryShaw added a commit that referenced this pull request Sep 22, 2026
The changelog changes #666 was carrying on its own branch, moved here so that #666
touches only `MANIFEST.in`, `pcapkit/protocols/schema/internet/ipv6_route.py`,
`pcapkit/utilities/logging.py` and `tests/project/test_annotation_names.py`. #666
was the last open branch still editing `CHANGELOG.md` itself; it no longer does.

Four pieces, not one, because #666 had amended existing entries as well as needing
a new one.

A new **Fixed** bullet for #642: three names used in string annotations that their
own module never imported -- `Any` in `pcapkit/utilities/logging.py`, and
`Protocol` and `Optional` in `pcapkit/protocols/schema/internet/ipv6_route.py`.
The bullet names the `typing.cast` case specifically, because that is the one no
running test can catch: `cast` never evaluates its first argument. It states
plainly that nothing resolves at runtime that did not before, since `TYPE_CHECKING`
is `False` when the interpreter runs, so that the entry is not read as a runtime
fix. mypy 2.3.1's before/after is quoted as the measurement -- three `name-defined`
errors to none, 115 total to 112 -- and the new
`tests/project/test_annotation_names.py` is described as what pins the invariant
without a type checker installed.

Three missing citations recovered: `(#570)` on the L2TPv3 worked-example line,
`(#577)` on the `register_extractor_engine` keyword line, and `(#619)` on the
README rename entry.

And the #619 entry's packaging claim corrected in place. It asserted that
`include README.md` in `MANIFEST.in` was "the only thing that puts the README in a
source distribution" and that an sdist without it "cannot be installed". Both
halves are false: setuptools' own `sdist` command ships the README before
`MANIFEST.in` is read at all, so dropping the line leaves the listing
byte-identical at 861 entries, and `setup.py` reads the file from wherever it is
executing, which for a `pip` install of an sdist is the unpacked sdist. #666
corrects the same claim in the `MANIFEST.in` comment, so the two stay in step.

No `:pep:` role, though the new bullet discusses PEP 695: `util/changelog_md.py`
converts only double-backtick literals and the `:rfc:` role, and raises
`ResidualMarkupError` on anything else, exactly as the #661 entry hit with `:obj:`.
Plain prose instead.

50 lines added to the two files, 11 reflowed. `CHANGELOG.md` regenerated with
`util/changelog_md.py`, not edited; `--check` exits 0 and `tests/project/` is green
at 96 passed, 469 subtests, exit 0 -- the same counts the previous commit on this
branch reported, so nothing else moved.

Committed from a detached HEAD and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree at a stale
f846523 and could not be taken here. Written against b2ec64b and rebased onto
d47ada0, the branch having taken #641's entry and the #652/#650 boundary
correction in the meantime. Both files conflicted, both at the append point rather
than in substance -- #641's bullet and this one land in the same place at the end
of **Fixed** -- so the resolution keeps both, #641's first. `CHANGELOG.md` was not
hand-resolved: it is generated, so it was regenerated from the resolved entry file
and `--check` re-run, which is the only resolution that cannot drift.

Refs #642
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…a false packaging claim (#642) (#666)

Three names appeared in string annotations that their module never imported, and
they were mypy's complete set of ``name-defined`` findings for the package:

* ``pcapkit/utilities/logging.py:350`` used ``Any``; added to the
  ``TYPE_CHECKING`` block beside ``IO``, ``Optional`` and ``Union``.
* ``pcapkit/protocols/schema/internet/ipv6_route.py:136`` used ``Protocol`` and
  ``:271`` used ``Optional``; added, ``Protocol`` as
  ``ProtocolBase as Protocol``. Twenty-one sibling schema modules already spell
  it that way, in the same ``payload:`` stub; this makes twenty-two.

mypy 2.3.1 over ``pcapkit``: 3 ``name-defined`` errors before, 0 after; 115 total
errors before, 112 after, so nothing else moved.

``MANIFEST.in:14-17`` asserted that ``include README.md`` was "the only thing
that puts it in an sdist" and that an sdist without it "cannot be installed at
all". Both halves are false, and the file had contradicted itself since #631
wrote the correct mechanism seven lines below without correcting this. Deleting
the lines and rebuilding gives a byte-identical sdist listing -- empty diff --
that installs with exit 0: ``setuptools/command/sdist.py:59-60`` ships the README
unconditionally and ``setuptools/dist.py:460``'s default ``license_files`` glob
ships ``LICENSE``. Of the original three ``include`` lines only ``CHANGELOG.md``
is load-bearing. The comment now says that.

New ``tests/project/test_annotation_names.py`` resolves every string annotation
in the package -- following a nested forward reference such as
``'list["Nested"]'``, while treating ``Literal`` members and ``Annotated``
metadata as the values they are -- against the names its own module binds. It
reports the same three findings as mypy on the unfixed tree and none after.

That module named ``ast.TypeAlias`` and ``ast.TypeVar`` directly, and both are
PEP 695 nodes added in Python 3.12, so *every* test in it raised
``AttributeError`` on the 3.10 and 3.11 matrix jobs -- ``bound_names`` walks every
node of every file, so the attribute is reached whatever a test does. Both are now
resolved once at module scope through ``getattr(ast, ..., ())``, leaving the
``isinstance`` branches otherwise untouched: ``isinstance(x, ())`` is always
False, so the branches stay live on 3.12+ and are simply unreachable below it.
Chosen over a ``sys.version_info`` comparison because it writes no version number
down at all -- a comparison states 3.12 next to the attribute it guards, and the
two can then drift -- and over a per-node ``getattr`` because a module-level
constant lifts the lookup out of a loop that runs on every node of every file.

``ast.TypeVar`` is the branch that earns its keep: it carries its name as a bare
``str`` and emits no ``ast.Name`` node, so forcing ``_TYPE_VAR`` to ``()`` on
3.14.7 turns ``T`` and ``U`` into false findings. ``ast.TypeAlias`` is defensive
by comparison -- its name *is* an ``ast.Name`` in ``Store`` context, which the
preceding branch already catches -- and is left as it stands rather than removed.
A new ``test_a_pep695_type_parameter_is_in_scope`` pins both the guards and the
behaviour, skipped below 3.12 because its fixture source cannot parse there.

Measured on real interpreters rather than simulated. 3.10.21 and 3.11.15:
5 failed, exit 1 -> 5 passed, 1 skipped, exit 0. 3.14.7: all 6 pass, exit 0.
133 passed over ``tests/project`` and ``tests/utilities/test_logging.py``, exit 0,
subtests unchanged at 487.

No changelog entry on this branch. Per the rule that no code branch touches
``CHANGELOG.md`` or anything under ``docs/source/changelog/``, this change's entry
-- and the wording correction the ``MANIFEST.in`` claim implies for the #619
entry, plus the missing ``(#570)`` and ``(#577)`` citations -- go to the shared
changelog pull request #657 instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test Pull requests that add or correct tests (test: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant