Skip to content

fix: import three names used only in string annotations, and correct a false packaging claim (#642) - #666

Merged
JarryShaw merged 1 commit into
mainfrom
fix/642-string-annotation-imports
Sep 22, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/642-string-annotation-imports

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Fixes #642

1. Three unimported names in string annotations

mypy 2.3.1 over pcapkit, before and after — these three were the complete name-defined set, and still are:

before: 3 name-defined,  Found 115 errors in 39 files (checked 496 source files)
after:  0 name-defined,  Found 112 errors in 38 files (checked 496 source files)
  • pcapkit/utilities/logging.py:350 used Any; added to the TYPE_CHECKING block beside IO, Optional, Union.
  • pcapkit/protocols/schema/internet/ipv6_route.py:136 used Protocol and :271 used Optional; added, Protocol spelled ProtocolBase as Protocol to match the twenty-one sibling schema modules that already import it that way, in the same payload: stub shape. This makes twenty-two. (The PR originally said "nine"; that came from a truncated grep and the cross-review caught it. Re-counted: 21 on main, 22 with this change.)

The issue's claim that logging.py:350 is a live failure is half wrong, and the half that is wrong is the fix

Measured, on the worktree tree (pcapkit.__file__ printed and asserted, __editable__* finders stripped from sys.meta_path):

MEASURED TREE: .../agent-a2a27bfd0e34cdb81/pcapkit/__init__.py
--- get_type_hints(logging._writes_to) ---
  NameError: name 'Any' is not defined

Refinement from the cross-review, and it cuts against this section: under plain get_type_hints the fix changes nothing, as below. But this project's own Sphinx machinery — docs/source/conf.py's bind_type_checking_names, which executes every module's TYPE_CHECKING block via sphinx_autodoc_typehints._resolver.resolve_type_guarded_imports precisely so autodoc's get_type_hints calls succeed — does evaluate those names. Under that mechanism the fix is load-bearing: on the branch _writes_to's hints resolve to {'candidate': Handler, 'stream': typing.Any, 'return': bool}, while on main the same mechanism still raises NameError: name 'Any' is not defined for _writes_to alone (the other four resolve, since Optional and Union are in the block). It never surfaces in a real docs build only because _writes_to is private and no autofunction/automodule directive in docs/source/pcapkit/utilities/logging.rst documents it. So the "type-checker-visible only" conclusion holds for this package as it stands, but partly by accident — document or rename that function and it becomes a live docs-build failure.

The NameError is real. But adding Any to the TYPE_CHECKING block does not fix it, and cannot. After the fix, that call raises identically — because TYPE_CHECKING is False at runtime, so a TYPE_CHECKING-only import is absent from the module namespace either way. The decisive evidence is the siblings:

get_type_hints over every function in pcapkit.utilities.logging -> failures:
  get_logger:     NameError: name 'Optional' is not defined
  ensure_output:  NameError: name 'Union' is not defined
  reset:          NameError: name 'Optional' is not defined
  configure:      NameError: name 'Optional' is not defined
  _writes_to:     NameError: name 'Any' is not defined

Four of those five fail on Optional and Union, which are imported in the block. Identical output on an unmodified main tree. So _writes_to is not qualitatively different from its neighbours: unresolvable-at-runtime annotations are a property of the TYPE_CHECKING idiom this package uses in all 496 modules, not a defect of line 350.

What line 350 did have, and what is fixed here, is a genuine type-checker-visible defect: it used a name imported nowhere at all, not even for type checking. That is why mypy flagged it and not the other four.

Not done here, and offered as a decision rather than taken unilaterally: making get_type_hints actually work would mean importing the typing names unconditionally. That fixes 5 functions in 1 of 496 modules, leaves 495 unchanged, and makes one module inconsistent with the package's universal idiom. It is a repository-wide style decision, not a missing import. Say the word and it is a one-line follow-up.

All three names are therefore type-checker-visible only. Nothing here is described as a live bug.

The unreachable at ipv6_route.py:264 is not related

It is adjacent by two lines and independent. addresses: 'bytes' = ListField(...) at :211 declares the attribute as bytes, so mypy narrows buffer = self.addresses and reads if not isinstance(buffer, bytes): at :228 as statically false — making the whole block, ending return self at :269, dead to mypy. At runtime the branch is live and load-bearing: the NOTE at :229 records that self.addresses really is a list[bytes] when the schema was built via make, and #556 is the issue where treating it as bytes raised.

So it is a false positive of the declared-type convention that the file's own # mypy: disable-error-code=assignment header exists to accommodate. Fixing it properly means widening the declared type to 'bytes | list[bytes]', which changes the schema's public type surface and ripples into the __init__ stub — a real change deserving its own review, deliberately not folded in here. It survives the fix unchanged, now reported at :265 after the one-line insertion:

pcapkit/protocols/schema/internet/ipv6_route.py:265:13: error: Statement is unreachable  [unreachable]

2. MANIFEST.in — re-measured from scratch, claim confirmed false

Re-measured independently rather than taken on trust, on setuptools 84.0.0, by git archive-ing to a clean scratch tree and building both ways:

entries installs
baseline 862 listing lines (861 entries + the root dir record)
lines 14-19 deleted 862, diff empty pip install --no-depsexit 0
only include CHANGELOG.md deleted 861 — exactly CHANGELOG.md gone, nothing else

README.md, LICENSE, CHANGELOG.md and CITATION.cff are all still in the sdist built without those lines. What ships them:

  • setuptools/command/sdist.py:59-60README_EXTENSIONS = ['', '.rst', '.txt', '.md'] feeding READMES, appended unconditionally by setuptools/_distutils/command/sdist.py:277-296 before MANIFEST.in is read. The .md entry is setuptools' own addition over the distutils default, which is exactly what made the Markdown rename stop mattering.
  • setuptools/dist.py:460 — default license_files glob ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'], used because pyproject.toml declares no license-files. It emits adding license file 'LICENSE' in a build with the line deleted.

One correction to the issue's own reasoning. It says setup.py's unguarded read happens "from the working tree during the build, not from an unpacked sdist". That is wrong: during pip install of an sdist, setup.py executes inside the unpacked sdist, so the failure mode is reachable in principle. It just cannot be triggered by deleting the line, because the README is in the sdist regardless. The correction says this accurately rather than repeating the issue's version.

Only CHANGELOG.md is load-bearing, confirmed by the third build. The two include lines are kept, now documented as an explicit statement of intent that does not depend on a setuptools default — a weaker reason than the one the comment used to give, which is the point of rewriting rather than deleting it.

MANIFEST.in:47 is corrected too: it justified the recursive-include docs/source/changelog line as "the same shape as include README.md, and for the same reason" — the same false premise, one propagation further. It now names include CHANGELOG.md.

The rewrite is comment-only: rebuilt with the edited file, the sdist listing diffs empty against the baseline, 862 lines.

3. Changelog: the self-contradiction, and the citations

docs/source/changelog/1.5.0.rst:961-965 carried the identical false claim, contradicted by the #631 entry ~250 lines below. Corrected in place.

Matching the precedent. 375e9d411 (#638) corrected the #630 entry by doing both: editing the wrong prose in place and adding a new * **Fixed** bullet describing the correction. This PR does only the in-place half, deliberately — a new bullet for this correction belongs on #657, the long-lived shared changelog branch, and will go there as its own commit.

Citations. The base rate has drifted since the issue was filed: re-measured on "does the bullet contain any (#nnn) anywhere", the file now has 80 top-level bullets, 75 cited, 5 uncited — not the issue's 76/71/5, because four cited bullets have landed since. The five uncited are exactly the ones the issue lists. Three are fixed:

bullet citation evidence
:944-979 README landing page (#619) 2221c2d8f docs(readme): trim to a landing page and convert to Markdown (#619)
:181-214 code= class keyword (#570) 1c5833e00 feat(protocols): opt-in code= registration with enum-type inference for Protocol/ProtocolBase (#514) (#570)
:343-350 StreamEOFError/register_extractor_engine (#577) 9c240a60e test(utilities,foundation): pin the quiet StreamEOFError convention and register_extractor_engine's keyword (#577)

Two are deliberately left uncited, because no number could be recovered and a wrong number is worse than an admitted gap:

  • :136-139 (Probe/CipherSuite/IntegritySuite off NamedTuple) — the exact-content commit is 26d72fa14, which carries no PR number. #406 is inferable from its position between (#405) and (#407) and from pr/406 being the one missing ref in that range, but that is circumstantial and a guess does not belong in a published changelog.
  • :336-338 (format='text'dictdumper.Text) — exact-content commit 892c2100c, no PR number, and its neighbours cf06ff971/73bf00308 have none either, consistent with a rebase-merge that never got a squash number. Nothing found.

#619 appears once elsewhere, at :1228, as a back-reference from the #631 entry ("#619 did not address it") — a reference, not a citation, and left alone.

CHANGELOG.md regenerated with util/changelog_md.py, never hand-edited. --check exits 0. Its diff is exactly three lines, one per edited bullet, so no pre-existing drift was reintroduced.

Evidence

Exit codes read from files; .venv/bin/python 3.14.7.

The new test fails on main and passes here. tests/project/test_annotation_names.py resolves every string annotation in the package — including typing.cast's first argument, which is where ipv6_route.py:271 hid, since cast never evaluates it — against the names its own module binds anywhere, TYPE_CHECKING blocks included. Copied onto an unmodified main tree:

main:    EXIT=1   AssertionError: Lists differ ... First list contains 3 additional elements
                    ipv6_route.py:136: Protocol  (in 'Protocol | Schema | bytes')
                    ipv6_route.py:271: Optional  (in 'Optional[IPv6Address]')
                    logging.py:350:   Any        (in 'Any')
branch:  EXIT=0   2 passed

Name for name and line for line the same three mypy reports, with no mypy dependency and in 2s. It carries its own self-check: a module with a planted unresolvable name is written to a scratch directory and must be reported, because a source-analysis guard that silently found nothing would look exactly like success.

tests/project + tests/utilities/test_logging.py: 127 passed before, 129 passed after (the +2 are the new test's two methods), both EXIT=0, subtests unchanged at 487. tests/utilities/test_logging.py tests/protocols/schema tests/protocols/internet/test_ipv6_extension_unit.py: 121 passed, EXIT=0.

Coverage. Both changed lines are inside if TYPE_CHECKING: blocks, which CPython never executes and coverage excludes, so no statement count moves. Measured over the selection above: ipv6_route.py 100% (78 stmts, 0 miss, 12 branches, 0 partial); logging.py 95% (78 stmts, 2 miss) with both misses at :314 and :321, pre-existing and nowhere near line 46.

Conflict note

docs/source/changelog/1.5.0.rst is also touched by #657 (docs/changelog-1.5.0), the long-lived shared changelog PR, so this may conflict. #657 merges last. These changes are corrections to existing prose and citations on existing bullets, not new bullets, which is why they are here; the new bullet describing these two fixes goes to #657.

Corrections after cross-review

A cross-review on a different model (Sonnet) re-derived every measurement here independently. It confirmed the mypy counts (3 → 0 name-defined, 115 → 112 total), the get_type_hints reasoning, the ProtocolBase as Protocol choice over typing.Protocol, the absence of any circular-import hazard (traced through bind_type_checking_names' own per-module walk: 462 modules, 0 failures), the unreachable analysis, all three setuptools citations to the line, the three sdist builds, the changelog bullet counts (80/75/5 before, 80/78/2 after, independently derived), and that the new test fails on main with exactly the three findings. Four things changed as a result:

  1. "nine sibling schema modules" was wrong — it is twenty-one. Corrected above and in the commit message.
  2. The get_type_hints section understated the fix. The Sphinx-build refinement above is new, and it partly vindicates the issue's "live failure" framing in the one context that matters for this repo.
  3. The new test followed no nested forward reference. 'list["Nested"]' parses the inner name as an ast.Constant, invisible to a walk over ast.Name — so an unresolvable name one level of quoting down went unreported. Now followed, with Literal members and Annotated metadata deliberately not followed, because those are values and following them false-positives on the twenty-odd Literal["big", "little"] annotations this package already has. Three new tests pin all of it.
  4. A stricter scope rule was tried and reverted, and that is worth recording. The reviewer's other finding was that bound_names is scope-blind, so a local variable named Optional anywhere in a file masks a genuinely missing Optional import in that file. Real, and I implemented the narrowing — module and class scope only, which is what get_type_hints actually consults. It then false-positived on real code: pcapkit/toolkit/scapy.py:208 casts to 'IPv6ExtHdrFragment', imported six lines above inside the same function, which is correct because a cast string resolves lexically — and mypy accordingly does not flag it. Telling the two apart means tracking a binding set per scope and knowing which construct resolves where, i.e. reimplementing the type checker. So the trade is made the other way on purpose: no false positives, one documented false negative. The docstring now records the whole episode, and test_a_cast_to_a_function_local_import_is_not_a_finding pins it so that reintroducing the narrowing fails a test rather than a real module.

The reviewer also found positive evidence that declining to cite #406 on the NamedTuple bullet was right rather than merely cautious: issue #406 exists and is about a scapy-engine bug, unrelated to that bullet. Citing it would have been actively wrong.

One number it could not reconcile: it measured 863 total sdist entries where this PR says 862. Both of the conclusions that matter — empty diff between baseline and modified, exactly one entry lost when include CHANGELOG.md goes — reproduced exactly. Recorded rather than papered over.

Re-verified after these changes: tests/project/test_annotation_names.py EXIT=0, 5 passed (was 2); on unmodified main, EXIT=1 with the same three findings. tests/project + tests/utilities/test_logging.py: 127 → 132 passed, both EXIT=0, subtests unchanged at 487.

@JarryShaw
JarryShaw force-pushed the fix/642-string-annotation-imports branch 2 times, most recently from 8f737e1 to fb6a0d0 Compare September 22, 2026 18:13
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES → addressed, pushed as fb6a0d0e3

Cross-review by an independent agent on a different model (Sonnet), briefed to falsify rather than confirm, per the house rule that an agent-raised PR gets a cross-review on a model other than the authoring one (this PR was authored by Opus). It ran read-only, built its own scratch trees, and re-derived every measurement here from scratch.

Its verdict was NEEDS CHANGES on exactly one item, plus two findings it was asked to hunt for and found. All are now fixed.

1. The required change: "nine sibling schema modules" was wrong — it is twenty-one

It grepped pcapkit/protocols/schema/ on both trees and found 21 modules already spelling from pcapkit.protocols.protocol import ProtocolBase as Protocol, not nine, and confirmed each uses Protocol in the same payload: __init__-stub shape — so this is one idiom 21 times, not 21 incidental hits. Re-counted independently: 21 on main, 22 with this change. My "nine" came from a grep I had truncated with head -10 and then read as complete — a careless error, and the count makes this PR's own consistency argument stronger than I claimed. Corrected in the PR body and in the commit message.

2. It found a real hole in the new test, and it is now fixed

Asked to break the check, it did, twice.

Nested forward references were invisible. 'list["NeverImportedEither"]' parses the inner name as an ast.Constant, so a walk over ast.Name never sees it — an unresolvable name one level of quoting down went unreported. Reproduced, then fixed: the descent now follows nested strings recursively.

That fix needed care the reviewer did not have to supply, and getting it wrong would have been worse than the hole. Following every nested string reports Literal["big", "little"] as a reference to a type named big — and this package writes twenty-odd such annotations, so the naive fix false-positives on real code. The descent is therefore explicit rather than an ast.walk, and skips a Literal slice entirely and everything after the first argument of Annotated. Both directions are pinned: test_a_nested_forward_reference_is_followed and test_a_literal_member_is_not_mistaken_for_a_type.

3. Its second finding: I implemented the fix, then reverted it, and that is the interesting part

It observed that bound_names is scope-blind — a local variable named Optional anywhere in a file masks a genuinely missing Optional import in that file — and demonstrated it. Real, and worth fixing: get_type_hints resolves a parameter or return annotation against module and class scope, never a function's locals, so narrowing to those scopes is strictly more correct for annotations.

So I implemented the narrowing. It immediately false-positived on real code:

pcapkit/toolkit/scapy.py:208: IPv6ExtHdrFragment  (in 'IPv6ExtHdrFragment')

scapy.py:202 imports IPv6ExtHdrFragment inside the function, and :208 casts to it six lines later in that same function. A typing.cast string is resolved lexically, in the enclosing function — so that code is correct, and mypy accordingly does not flag it. Annotations and cast strings resolve in different scopes, and telling them apart means tracking a binding set per scope and knowing which construct resolves where: reimplementing the type checker this is deliberately not.

The trade is therefore made the other way on purpose — no false positives, at the cost of one documented false negative — and matching mypy's finding set exactly is what makes the check usable as a cheap stand-in for it. The whole episode is in the bound_names docstring rather than lost, and test_a_cast_to_a_function_local_import_is_not_a_finding pins the scapy.py shape so that reintroducing the narrowing fails a test rather than a real module.

4. It also strengthened the logging.py section against me

The PR argued that all three names are type-checker-visible only. The reviewer went further than the brief and reproduced this project's own Sphinx machinery — docs/source/conf.py's bind_type_checking_names, which executes each module's TYPE_CHECKING block via sphinx_autodoc_typehints._resolver.resolve_type_guarded_imports precisely so autodoc's get_type_hints calls succeed. Under that mechanism the fix is load-bearing: on this branch _writes_to's hints resolve to {'candidate': Handler, 'stream': typing.Any, 'return': bool}, while on main the same mechanism raises NameError: name 'Any' is not defined for _writes_to alone.

It never surfaces in a real docs build only because _writes_to is private and no directive in docs/source/pcapkit/utilities/logging.rst documents it. So the conclusion stands for the package as it is, but partly by accident — document or rename that function and it becomes a live docs-build failure. That is a genuine partial vindication of the issue's "live failure" framing, in the one context that matters for this repo, and it is now in the PR body.

What it confirmed, independently derived

  • mypy counts exactly. main: exit 1, Found 115 errors in 39 files, name-defined = 3, the same three lines. Branch: Found 112 errors in 38 files, name-defined = 0.
  • ProtocolBase as Protocol is semantically right, not typing.Protocolpayload: 'Protocol | Schema | bytes' means a decoded protocol object, not PEP 544 structural typing.
  • No circular-import hazard. It traced pcapkit/protocols/protocol.py's unconditional module-level import of pcapkit.protocols.schema, then reproduced bind_type_checking_names' own pkgutil.walk_packages descent over both trees: 462 modules, 0 failures, and route_mod.__dict__['Protocol'] resolving to ProtocolBase on the branch versus None on main — which independently confirms the bug was real.
  • The unreachable is not dead code. It traced _make_data_type_rpl (pcapkit/protocols/internet/ipv6_route.py:732-828) and confirmed every path builds a list, never bytes, then ran coverage --branch over the schema and IPv6-extension tests: 78 stmts, 0 miss, 12 branches, 0 partial, 100% — the isinstance(buffer, bytes) branch genuinely executes. Matches this PR's coverage figures exactly.
  • All three setuptools citations correct to the line, and all three sdist builds: empty diff between baseline and modified, pip install --no-deps exit 0, exactly one entry lost when include CHANGELOG.md alone goes.
  • The changelog counts, independently derived with its own regex careful about en-dash ranges like #362--#364: main 80 total / 75 cited / 5 uncited at exactly the five lines named; branch 80 / 78 / 2 at exactly the two left uncited.
  • No surviving contradiction anywhere — grep for the false claim across MANIFEST.in, CHANGELOG.md and 1.5.0.rst returns zero hits.
  • Scope: one commit, Jarry Shaw <jarryshaw@icloud.com>, six files, none of the off-limits paths, no .github/, no new .md doc, no GH-nnn, no from inspect import.

And one thing it turned from caution into evidence

Declining to cite #406 on the NamedTuple bullet was recorded here as circumstantial caution. It checked GitHub directly: issue #406 exists and is about a scapy-engine bug, unrelated to that bullet. Citing it would have been actively wrong, not merely unproven. It independently failed to find a better candidate for the format='text' bullet either.

One number neither of us could reconcile

It measured 863 total sdist entries where this PR says 862 — a one-entry difference it could not trace, likely a build/setuptools micro-version or environment difference. Both conclusions that matter reproduced exactly. Recorded rather than papered over.

Re-verification after all of the above

  • tests/project/test_annotation_names.py: EXIT=0, 5 passed (was 2 — three new tests). Copied onto unmodified main: EXIT=1, still exactly the same three findings, name for name and line for line.
  • tests/project + tests/utilities/test_logging.py: 127 → 132 passed, both EXIT=0, subtests unchanged at 487.
  • Coverage over tests/utilities/test_logging.py tests/protocols/schema tests/protocols/internet/test_ipv6_extension_unit.py, measured on both trees and byte-identical: ipv6_route.py 78 stmts / 0 miss / 12 branches / 0 partial / 100%; logging.py 78 stmts / 2 miss / 95%, same two missing lines (:314, :321) either side. It cannot go backwards.

Final state: fb6a0d0e3, one commit, six files, unmerged and awaiting your review.

@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) docs Pull requests that change documentation only (docs: subject prefix) labels 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
JarryShaw force-pushed the fix/642-string-annotation-imports branch from fb6a0d0 to 9f23972 Compare September 22, 2026 19:15
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 JarryShaw changed the title fix: import three names used only in string annotations, and correct two false packaging claims (#642) fix: import three names used only in string annotations, and correct a false packaging claim (#642) Sep 22, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO

Cross-review of 9f2397213 by an independent agent on a different model (Sonnet; the revision itself was authored on Opus), briefed to falsify rather than confirm. It ran read-only and did not amend the PR.

What it checked and how

Claim Verdict Evidence it obtained itself
No third version-gated defect in the file Confirmed Enumerated every ast.* name the file touches in executable code — AnnAssign, arg, AST, AsyncFunctionDef, Attribute, Call, ClassDef, Constant, expr, FunctionDef, Import, ImportFrom, iter_child_nodes, Name, parse, Store, Subscript, Tuple, TypeAlias, TypeVar, walk. Only TypeAlias/TypeVar are 3.12+. ast.parse is called with mode=/filename= only — no type_comments, feature_version or optimize. No itertools.batched, Path.walk, typing.override, Self, tomllib, StrEnum, datetime.UTC, contextlib.chdir, hashlib.file_digest, str.removeprefix, match, except* or PEP 701 f-strings. It also grepped all 496 files of pcapkit/ for PEP 695 syntax, finding none — so ast.parse over the real package cannot raise SyntaxError on 3.10/3.11 either.
Passes on 3.10 and 3.11 Confirmed by running it Built a clean tree with git archive 9f2397213 | tar -x and ran the module on real 3.10.21 and 3.11.15 interpreters: exit 0, 5 passed, 1 skipped on both, exit codes read from a file rather than a pipe. It also independently reproduced the pre-fix failure on fb6a0d0e3: exit 1, 5 failed, AttributeError: module 'ast' has no attribute 'TypeAlias' at :138.
The new test is not vacuous on 3.12+ Confirmed, both halves By mutation on 3.14.7, bypassing the test's own assertIs so only behaviour was measured: baseline []; with _TYPE_VAR = () the findings became T and U — so that branch is load-bearing; with _TYPE_ALIAS = () alone the findings stayed [] — so that branch is genuinely redundant, as the code comment and docstring already say. It judged leaving the redundant branch in place acceptable.
The 3.12 skip boundary is right, and not over-broad Confirmed Fed the test's embedded fixture source to ast.parse on 3.10 and 3.11 directly: both raise SyntaxError, so the skip is grammar-forced rather than stylistic. Verbose output shows exactly one test skipped and the other five executing and passing on 3.10/3.11 — the skip does not mask the original defect.
Commit message accuracy Confirmed bar one figure mypy 2.3.1 on 0c7f2b7c9: Found 115 errors in 39 files, 3 name-defined. On 9f2397213: Found 112 errors in 38 files, 0 name-defined — matching 115→112 and 3→0 exactly. tests/project + tests/utilities/test_logging.py on 3.14.7: 133 passed, 487 subtests passed, exit 0 — exact match.
Scope Confirmed git diff 0c7f2b7c9...9f2397213 --name-only is exactly four files; git log --oneline 0c7f2b7c9..9f2397213 is exactly one commit. CHANGELOG.md and docs/source/changelog/1.5.0.rst are absent from the PR diff. tests/project/test_changelog_md.py passes on the revision — 47 passed, 37 subtests, exit 0 — so removing them caused no drift. The MANIFEST.in diff is comment-only; the include directives themselves are untouched.

What it disputed, recorded rather than folded away

The MANIFEST.in comment says the sdist listing is "861 entries either way". Rebuilding the sdist with and without the include lines, under setuptools 84.0.0 and via both python -m build --sdist and setup.py sdist, it measured 863 both times.

The comparative claim the number supports — identical listing with and without the line, empty diff — reproduced cleanly and is the load-bearing half. The absolute count is inherently a function of how many files the tree contains, and the tree has gained files since that figure was first measured, which is the likely explanation. Not corrected here: MANIFEST.in is part of this PR's already-reviewed diff and touching it costs another review round. Worth making version-independent ("identical either way") rather than re-pinning to a number that goes stale on every added file.

Not verified, stated rather than implied

  • No actual pip install of the built sdist into a fresh venv, skipped to conserve disk (host at 88%). "Installs with exit 0" is therefore unverified by the reviewer, though it confirmed README.md and LICENSE are both present in the sdist listing without their include lines.
  • The "twenty-one sibling schema modules" count in the commit body was not re-derived.

Note on this revision's scope change

The changelog changes this branch was carrying have been moved to the shared changelog pull request #657 (29aa75247), so no code branch edits CHANGELOG.md any more. util/changelog_md.py --check exits 0 there and tests/project is green at 96 passed, 469 subtests.

The docs label may now be stale — with the changelog files gone, this PR changes no file under docs/. Left as-is deliberately rather than changed.

@JarryShaw
JarryShaw merged commit 3d03055 into main Sep 22, 2026
26 checks passed
@JarryShaw
JarryShaw deleted the fix/642-string-annotation-imports branch September 22, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

1 participant