From 24f9dd7a5c55bf24c52a78a40f8ab69648a56d4c Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 22 Oct 2026 19:17:57 -0400 Subject: [PATCH] build: raise the setuptools floor to 69.0.0 so wheels ship py.typed (#2062) A wheel built at the old floor, 62.3.0, without build isolation left out pcapkit/py.typed: nothing declares it, and setuptools packages it unasked only from 69.0.0 (pypa/setuptools#3136). Per the owner's ruling, bump the floor rather than add a package-data entry. - pyproject.toml: [build-system] requires "setuptools>=69.0.0"; the comment gives the reason and keeps #1814's "**" glob rationale. - conda/pypcapkit/meta.yaml: build and host pins match. - tests/project/test_setuptools_floor_2062.py pins the floor at or above 69.0.0 and the conda pins to it; fails on main. - 1.5.0 changelog entry; CHANGELOG.md regenerated. Wheels measured on 3.8 and 3.14 at 69.0.0 ship py.typed, file list identical to 84.2.1's. Related project tests pass, run one by one. --- CHANGELOG.md | 1 + conda/pypcapkit/meta.yaml | 4 +- docs/source/changelog/1.5.0.rst | 5 ++ pyproject.toml | 14 ++-- tests/project/test_setuptools_floor_2062.py | 76 +++++++++++++++++++++ 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 tests/project/test_setuptools_floor_2062.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 99bbd6b552..8f22ab30cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -437,6 +437,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - the API reference renders what the code declares. `Extractor._vfunc` keeps its annotation ([#1839](https://github.com/JarryShaw/PyPCAPKit/issues/1839), [#1867](https://github.com/JarryShaw/PyPCAPKit/pull/1867)), and the HTTP/2 frame schemas' and the engine classes' annotations resolve ([#1857](https://github.com/JarryShaw/PyPCAPKit/issues/1857), [#1870](https://github.com/JarryShaw/PyPCAPKit/pull/1870)). Under the pinned sphinx-autodoc-typehints 3.15.0 ([#1856](https://github.com/JarryShaw/PyPCAPKit/issues/1856), [#1877](https://github.com/JarryShaw/PyPCAPKit/pull/1877)), `conf.py` renders an `aenum` base as such, keeps a default as written (`byteorder=sys.byteorder` rather than the build host's value), gives a class the parameters of the `__init__` its `__new__` hides and an enumeration `(*values)`, keeps table ids unique, and stops the source index linking a missing page ([#1858](https://github.com/JarryShaw/PyPCAPKit/issues/1858) to [#1863](https://github.com/JarryShaw/PyPCAPKit/issues/1863), [#1881](https://github.com/JarryShaw/PyPCAPKit/pull/1881)). The generated const tables carry no version directives; each note is on the module's hand-written page instead ([#1884](https://github.com/JarryShaw/PyPCAPKit/issues/1884), [#1895](https://github.com/JarryShaw/PyPCAPKit/pull/1895)). - `pcapkit-cli`'s help strings say what each option does: `-o` describes the output rather than the input, an extension is appended only under `--auto-extension`, and each format flag names the flags that take precedence over it ([#1796](https://github.com/JarryShaw/PyPCAPKit/issues/1796), [#1810](https://github.com/JarryShaw/PyPCAPKit/pull/1810)). - the Conda package: its vendored dependencies are the package's `dependencies` and its `all` extra, with their requirements, shipped with their data files rather than as `.py` files only ([#1814](https://github.com/JarryShaw/PyPCAPKit/issues/1814), [#1821](https://github.com/JarryShaw/PyPCAPKit/pull/1821)); the `osx-64` package is built on an Intel runner, where converting the arm64 build shipped arm64 binaries ([#1822](https://github.com/JarryShaw/PyPCAPKit/issues/1822), [#1825](https://github.com/JarryShaw/PyPCAPKit/pull/1825)); and on Intel macOS it pins `cryptography` 48.0.1, which has a wheel for that platform where the newer pin has none ([#1827](https://github.com/JarryShaw/PyPCAPKit/issues/1827), [#1828](https://github.com/JarryShaw/PyPCAPKit/pull/1828)). `util/conda_vendor.py --check` reports a package pinned differently under one marker ([#1852](https://github.com/JarryShaw/PyPCAPKit/issues/1852), [#1865](https://github.com/JarryShaw/PyPCAPKit/pull/1865)); the platform pins move to `conda/platform-pins.toml`, each with a review-by date, and Conda Update moves them up ([#1851](https://github.com/JarryShaw/PyPCAPKit/issues/1851), [#1885](https://github.com/JarryShaw/PyPCAPKit/pull/1885)); and the release's conda leg installs none of pcapkit's extras and smoke-tests the package before uploading it ([#1854](https://github.com/JarryShaw/PyPCAPKit/issues/1854), [#1855](https://github.com/JarryShaw/PyPCAPKit/issues/1855), [#1868](https://github.com/JarryShaw/PyPCAPKit/pull/1868)). +- a wheel built with the oldest setuptools `[build-system] requires` allowed, without build isolation, left out `pcapkit/py.typed`, so type checkers read the installed package as untyped. Nothing declares the file, and setuptools ships it unasked only from 69.0.0, so that is the floor now, in `pyproject.toml` and the Conda recipe alike ([#2062](https://github.com/JarryShaw/PyPCAPKit/issues/2062), [#2064](https://github.com/JarryShaw/PyPCAPKit/pull/2064)). --- diff --git a/conda/pypcapkit/meta.yaml b/conda/pypcapkit/meta.yaml index 81befad125..85e8ce7e33 100644 --- a/conda/pypcapkit/meta.yaml +++ b/conda/pypcapkit/meta.yaml @@ -24,7 +24,7 @@ requirements: - git - python - pip - - setuptools>=62.3.0 + - setuptools>=69.0.0 # - dictdumper~=0.8.0 # for formatted output # - chardet # for bytes decode @@ -43,7 +43,7 @@ requirements: host: - python - pip - - setuptools>=62.3.0 + - setuptools>=69.0.0 # - dictdumper~=0.8.0 # for formatted output # - chardet # for bytes decode diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index fe7cd486a8..c522b15dcf 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -3371,3 +3371,8 @@ Fixed them up (:issue:`1851`, :pr:`1885`); and the release's conda leg installs none of pcapkit's extras and smoke-tests the package before uploading it (:issue:`1854`, :issue:`1855`, :pr:`1868`). +* a wheel built with the oldest setuptools ``[build-system] requires`` allowed, + without build isolation, left out ``pcapkit/py.typed``, so type checkers read the + installed package as untyped. Nothing declares the file, and setuptools ships it + unasked only from 69.0.0, so that is the floor now, in ``pyproject.toml`` and the + Conda recipe alike (:issue:`2062`, :pr:`2064`). diff --git a/pyproject.toml b/pyproject.toml index 710440e390..8dc84b477c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,14 @@ [build-system] requires = [ - # 62.3.0 is the first release whose ``package-data`` globs read ``**`` as any - # depth, which the ``pcapkit._extern`` entry below needs (#1814). Measured: - # 61.0.0 and 62.2.0 read it as ``*``, and ship only the files one directory - # down from it. - "setuptools>=62.3.0", + # 69.0.0 is the first release that ships ``pcapkit/py.typed`` unasked + # (pypa/setuptools#3136, in its NEWS.rst): nothing here declares the file, so + # a wheel built with 68.2.2 or older leaves it out and type checkers read the + # package as untyped (#2062). Measured on both sides of the bump. It also + # keeps what the earlier floor of 62.3.0 was for: ``package-data`` globs that + # read ``**`` as any depth, which the ``pcapkit._extern`` entry below needs + # (#1814) -- 61.0.0 and 62.2.0 read it as ``*``, measured. 69.0.0 still + # supports Python 3.8, the ``requires-python`` floor. + "setuptools>=69.0.0", ] build-backend = "setuptools.build_meta" diff --git a/tests/project/test_setuptools_floor_2062.py b/tests/project/test_setuptools_floor_2062.py new file mode 100644 index 0000000000..89f19d1ef8 --- /dev/null +++ b/tests/project/test_setuptools_floor_2062.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""#2062: the setuptools floor is one that ships ``pcapkit/py.typed``. + +Nothing declares ``py.typed``: :file:`MANIFEST.in` includes ``*.py`` alone, and +:file:`pyproject.toml` has no ``package-data`` entry for ``pcapkit``. setuptools +69.0.0 is the first release that packages it regardless (pypa/setuptools#3136, +in that release's ``NEWS.rst``), measured: a wheel built with 68.2.2 leaves it +out, and one built with 69.0.0 has it. Pinned here: + +* ``[build-system] requires`` floors setuptools at 69.0.0 or later; +* every setuptools pin in :file:`conda/pypcapkit/meta.yaml` matches that floor. + +These read the declarations, not a built wheel: they catch the floor being +lowered again, not a later setuptools release dropping the behaviour. + +""" + +from __future__ import annotations + +import pathlib +import re +import unittest + +try: + import tomllib +except ImportError: # pragma: no cover + tomllib = None # type: ignore[assignment] + +#: Test tier (#1958): reads pyproject.toml and the conda recipe, the same on every leg. +__tier__ = 'LATEST' + +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The first setuptools that packages ``py.typed`` without being told to (#2062). +FIRST_WITH_PY_TYPED = (69, 0, 0) + +#: A ``setuptools>=X.Y.Z`` requirement, as either file spells it. +FLOOR = re.compile(r'^\s*(?:-\s*)?setuptools\s*>=\s*(\d+)\.(\d+)\.(\d+)\s*$') + + +def floors(lines: 'list[str]') -> 'list[tuple[int, int, int]]': + """Every ``setuptools>=`` floor in ``lines``.""" + return [tuple(int(part) for part in match.groups()) # type: ignore[misc] + for match in map(FLOOR.match, lines) if match] + + +class SetuptoolsFloorTests(unittest.TestCase): + """The build floor packages ``py.typed``, and conda agrees with it.""" + + def setUp(self) -> None: + if tomllib is None: # pragma: no cover + self.skipTest('tomllib needs Python 3.11') + pyproject = tomllib.loads((ROOT / 'pyproject.toml').read_text(encoding='utf-8')) + requires = pyproject['build-system']['requires'] + self.assertEqual([text for text in requires if text.startswith('setuptools')], + [text for text in requires if FLOOR.match(text)], + 'every setuptools requirement should be a plain ">=" floor') + self.floors = floors(requires) + + def test_build_floor_ships_py_typed(self) -> None: + self.assertEqual(len(self.floors), 1, self.floors) + self.assertGreaterEqual(self.floors[0], FIRST_WITH_PY_TYPED, + 'setuptools older than 69.0.0 builds wheels without pcapkit/py.typed') + + def test_conda_pins_match_the_build_floor(self) -> None: + lines = (ROOT / 'conda' / 'pypcapkit' / 'meta.yaml').read_text(encoding='utf-8').splitlines() + pins = floors(lines) + self.assertTrue(pins, 'the conda recipe pins no setuptools floor') + self.assertEqual(set(pins), set(self.floors)) + + def test_py_typed_exists(self) -> None: + self.assertTrue((ROOT / 'pcapkit' / 'py.typed').is_file()) + + +if __name__ == '__main__': + unittest.main()