Skip to content

fix(packaging): ship CITATION.cff in the source distribution - #631

Merged
JarryShaw merged 1 commit into
mainfrom
fix/manifest-include-citation-cff
Sep 22, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/manifest-include-citation-cff

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 22, 2026

Copy link
Copy Markdown
Owner

MANIFEST.in carries include README.md (line 18), include LICENSE (19) and include CHANGELOG.md (37), but had no line for CITATION.cff. Its two global-include patterns are *.rst and *.py, and neither can match a .cff, so CITATION.cff was in the repository and in no source distribution at all. This adds include CITATION.cff.

Requested by the repository owner; no issue is open for it, so there is no Fixes line.

Why this matters now

#625 has just taught util/bump_version.py to keep CITATION.cff's version and date-released in step with the version bump, and the owner intends a beta release specifically to exercise that path. Releasing with this gap would publish an sdist that omits the very artefact the release is meant to be testing — the bump would update a file that then does not ship.

The omission was flagged when the file landed: #615 added CITATION.cff and noted it ("CITATION.cff is not in the sdist. … Left alone because MANIFEST.in is owned by a concurrent branch this wave"), and #619 touched MANIFEST.in only for README.md, so it did not address the gap.

It also matters independently of the release. GitHub renders the "Cite this repository" button from the repository, so the button works either way and the gap is invisible from the web UI. Citation managers, Zenodo and dependency inventories read the published artifact, which is exactly the surface where the file was missing — so credit stopped travelling with the code at precisely the point where nobody can see the repository.

There was no default to fall back on

Worth establishing rather than assuming, because two of the three neighbouring include lines turn out to be redundant. Removing include README.md, include LICENSE and include CHANGELOG.md all at once from a clean checkout at ead73b204 and rebuilding gives an sdist whose root is:

pypcapkit-1.5.0b4/LICENSE
pypcapkit-1.5.0b4/MANIFEST.in
pypcapkit-1.5.0b4/PKG-INFO
pypcapkit-1.5.0b4/pyproject.toml
pypcapkit-1.5.0b4/README.md
pypcapkit-1.5.0b4/setup.cfg
pypcapkit-1.5.0b4/setup.py

README.md and LICENSE still ship — setuptools adds the licence from its license_files default, logging adding license file 'LICENSE' and recording License-File: LICENSE in PKG-INFO. CHANGELOG.md is gone. So of the three, only include CHANGELOG.md is genuinely load-bearing, and a .cff is in the same position as it: nothing outside MANIFEST.in knows a citation file exists. That is what the new comment says, and it is why it anchors on CHANGELOG.md rather than on its immediate neighbour.

Proof, both directions

Same command before and after the one-line change:

$ python -m build --sdist --outdir /tmp/cffsdist .
$ tar tzf /tmp/cffsdist/pypcapkit-*.tar.gz | grep -i citation

Before — no output, exit 1. The root of the sdist held CHANGELOG.md, LICENSE, MANIFEST.in, PKG-INFO, pyproject.toml, README.md, setup.cfg, setup.py.

After — exit 0:

pypcapkit-1.5.0b4/CITATION.cff

Nothing else moved. Diffing the two full archive listings (sorted under LC_ALL=C) gives exactly one added line and nothing removed — 860 entries before, 861 after:

$ comm -13 b.list f.list
pypcapkit-1.5.0b4/CITATION.cff
$ comm -23 b.list f.list        # removed: none

The shipped copy is the repository's copy, not a rewritten one — sha256 of both is 9d231a0bb7ab5ceece390497b2f9c42818cd6dbc47b88bb6d52d276cef488a25.

twine check still passes

$ python -m twine check --strict /tmp/cffsdist/*
Checking /tmp/cffsdist/pypcapkit-1.5.0b4.tar.gz: PASSED

PASSED, exit 0, on both the before and the after archive. Worth stating explicitly, since #497 records a beta that had to be reshipped as 1.5.0b2 after PyPI rejected its wheel over a twine check failure.

The comment

The two commented include blocks in this file each explain why the line is load-bearing rather than belt-and-braces, so this one does too, in the same voice: which global-include fails to reach a .cff, that the absence was measured by building one each way rather than assumed, that no setuptools default covers it, and why the published artifact rather than the repository is the surface that matters.

Changelog

A **Fixed** bullet in docs/source/changelog/1.5.0.rst, with CHANGELOG.md regenerated by util/changelog_md.py and --check confirmed to exit 0. CHANGELOG.md was not hand-edited.

No tests were run: this is one line of packaging configuration plus a changelog entry, and touches nothing under pcapkit/. python -m build was the relevant verification and it is quoted above.

Notes for whoever merges

  • This PR and fix(license): start the copyright term at 2017, and drop the end year #630 both insert a bullet at the same point in docs/source/changelog/1.5.0.rst, so they conflict with each other (and with the other changelog-touching PRs in flight). Whichever lands second needs a rebase that keeps every bullet, then a util/changelog_md.py regeneration rather than a hand merge of CHANGELOG.md.
  • Found but deliberately not fixed here, to keep this PR to one line: the pre-existing comment at MANIFEST.in:14-17 (from docs(readme): trim to a landing page and convert to Markdown #619) claims include README.md is "the only thing that puts it in an sdist" and that "an sdist without it cannot be installed at all". The build above shows README.md shipping with that line removed, so the claim is wrong. Left alone as out of scope — happy to send it as its own PR if wanted.

@JarryShaw

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts on this branch.

Requested by the owner; no issue is open for it.

* `MANIFEST.in` had `include` lines for `README.md`, `LICENSE` and
  `CHANGELOG.md` but none for `CITATION.cff`, and its two `global-include`
  patterns are `*.rst` and `*.py`, neither of which matches a `.cff`. The file
  was therefore in the repository and in no source distribution.
* Nor was there a default to fall back on. Removing all three `include` lines
  and rebuilding shows `README.md` and `LICENSE` shipping regardless --
  setuptools adds the latter from `license_files`, logging `adding license file
  'LICENSE'` and recording `License-File: LICENSE` in `PKG-INFO` -- while
  `CHANGELOG.md` disappears. Of the three only `CHANGELOG.md` is load-bearing,
  and a citation file, which no packaging default covers at all, is in the same
  position.
* The gap is invisible from the web UI, since GitHub renders the "Cite this
  repository" button from the repository. Citation managers, Zenodo and
  dependency inventories read the published artifact, which is the surface that
  was missing it.
* It matters now because #625 has just taught `util/bump_version.py` to keep
  that file's `version` and `date-released` in step with the bump, so a release
  exercising that path would publish an sdist omitting the artefact under test.
  #615 flagged the omission when it added the file; #619 did not address it.
* It also lets `RepositoryCitationTests` in `tests/project/test_bump_version.py`
  -- the gate #625 added for the hand-authored bumps that never run the script --
  execute against an unpacked sdist, where today it skips itself with
  "CITATION.cff is not shipped in the source distribution".
* Changelog bullet added; `CHANGELOG.md` regenerated with
  `util/changelog_md.py`, and `--check` exits 0.

Rebased onto `cfb81d3f6` after #630 merged; the changelog conflict was resolved
keeping both bullets and regenerating `CHANGELOG.md` rather than merging it.

Measured both ways with `python -m build --sdist` against that base: `tar tzf |
grep -i citation` found nothing before and `pypcapkit-1.5.0b4/CITATION.cff`
after; the listings differ by that one entry and nothing else, 860 against 861;
the shipped copy is byte-identical to the repository's; and `twine check
--strict` reports PASSED on both. No tests were run -- one line of packaging
configuration, nothing under `pcapkit/`.
@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review: GOOD TO GO

Independent cross-review by a subagent on a different model (Claude Haiku), run read-only on its own clones in /tmp, briefed to falsify rather than to bless. It took three passes; the first two returned NEEDS CHANGES and both found real defects, which is the point of doing it.

Verdict on this PR: GOOD TO GO.

No false clause remains. The MANIFEST.in comment now reads "setuptools ships LICENSE from its license_files whether or not this file mentions it, but no packaging default covers a citation file" — accurate on both halves, and the include CHANGELOG.md anchor is the genuinely load-bearing one.

What it caught, and what changed as a result

Pass 1 — the comment's central analogy was inverted. The first version of the new comment opened "Load-bearing for the same reason as include LICENSE above". Both halves of that were wrong: include LICENSE carries no comment of its own (the block above it is about README.md), and more importantly it is not load-bearing. Verified independently by removing all three include lines from a clean checkout and rebuilding — README.md and LICENSE still ship, CHANGELOG.md disappears. The build log says adding license file 'LICENSE' and PKG-INFO carries License-File: LICENSE. The comment was rewritten to anchor on include CHANGELOG.md, the one genuinely load-bearing neighbour, and that measurement is now stated in the changelog entry too.

Pass 2 — "nothing outside this file knows a citation file exists" was false. git grep -nl CITATION returns util/bump_version.py and tests/project/test_bump_version.py. Narrowed to "no packaging default covers a citation file".

Pass 2 also surfaced a second, independent motivation for this PR, now recorded in the entry: RepositoryCitationTests in tests/project/test_bump_version.py — the gate #625 added for the hand-authored bumps that never run the script — currently skips itself with 'CITATION.cff is not shipped in the source distribution'. This PR is what makes that gate live in the sdist context. The reviewer then checked its own suggestion rather than assuming it was safe, confirming that the test file ships (entry 753 of 861) and that its ROOT resolves to the sdist root, so the skip genuinely fires today and genuinely stops.

Two PR-body claims were also overstated and were corrected: that the surrounding include lines "each carry a comment" (two do not), and a conflation of #497 with #498#497 is the twine check rejection, #498 was a network timeout, and only #497 is cited now.

Independently re-measured on the final head, against the rebased base cfb81d3f6

  • 860 archive entries before, 861 after; diff output is exactly 2a3 / > pypcapkit-1.5.0b4/CITATION.cff; nothing removed
  • grep -i citation exit 1 before, exit 0 after
  • twine check --strictPASSED, exit 0, on both archives
  • shipped CITATION.cff sha256 9d231a0bb7ab5ceece390497b2f9c42818cd6dbc47b88bb6d52d276cef488a25, identical to the repository copy
  • util/changelog_md.py --check exit 0; both changelog bullets present in order with this PR's last, before the closing paragraph
  • one commit on cfb81d3f6, no merges, author and committer Jarry Shaw <jarryshaw@icloud.com>, and only MANIFEST.in plus the two changelog files touched

Advisory, non-blocking

Caveats the reviewer stated rather than glossed

Every measurement resolved setuptools==84.0.0 in the isolated build env, so behaviour at the declared floor of setuptools>=61.0.0 is not verified. No tests were run at all (deliberately — a coverage run -m pytest in this repo reached 41.4 GB RSS today and had to be killed), so the RepositoryCitationTests conclusion is read from source at lines 482-497 rather than from a run.

@JarryShaw
JarryShaw merged commit d3a80df into main Sep 22, 2026
25 checks passed
@JarryShaw JarryShaw added the fix Pull requests that fix a defect (fix: subject prefix) label Sep 22, 2026
JarryShaw added a commit that referenced this pull request Sep 22, 2026
`util/changelog_md.py --check` has failed on `main` since #638 (375e9d4),
which inserted three lines into the generated `CHANGELOG.md` directly instead
of running the generator. That left the #630 bullet duplicated -- the stale
pre-correction text alongside the corrected one -- and the #631 bullet in the
wrong position relative to its source entry.

Regenerated from `docs/source/changelog/1.5.0.rst`, which was always correct:
the phrase appeared once there and twice in the generated file.

`Changelog drift` had failed on four consecutive commits (375e9d4,
a62aed1, a05f461, da381f2), and because
`tests/project/test_changelog_md.py::RepositoryStateTests` asserts the same
consistency, all twelve matrix jobs failed with it -- 13 of 14 on run
35748204467. Every open pull request inherited the failure.

--check now exits 0.
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
…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.
@JarryShaw
JarryShaw deleted the fix/manifest-include-citation-cff branch September 23, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant