From e80e32ca1120c2e14f9b3528b4dec31eaa3730c4 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 22 Oct 2026 19:29:12 -0400 Subject: [PATCH] build: remove setup.py now that the floor is Python 3.8 (#2061) After #2053 setup.py was a Python 2 guard and a bare setup(); pyproject.toml declares everything, and the wheel builds the same files without it. - Delete setup.py and tests/project/test_setup.py. - Add tests/project/test_no_setup_py_2061.py: fails if setup.py returns, or if a COPY in examples/benchmark/Dockerfile names an untracked path. - examples/benchmark/Dockerfile: stop copying setup.py. - pyproject.toml, test_actions_pinned_1968: the static readme is now explained by Dependabot fetching pyproject.toml in setup.py's place (fetch_project_file) and passing it through unstubbed, not by its stub setup.py. - util/classify_changes.py, tests/_tiers.py: examples no longer name the removed files. - Changelog entry in 1.5.0.rst; CHANGELOG.md regenerated. --- CHANGELOG.md | 1 + docs/source/changelog/1.5.0.rst | 10 ++ examples/benchmark/Dockerfile | 2 +- pyproject.toml | 8 +- setup.py | 21 ---- tests/_tiers.py | 2 +- tests/project/test_actions_pinned_1968.py | 15 +-- tests/project/test_no_setup_py_2061.py | 116 ++++++++++++++++++++++ tests/project/test_setup.py | 57 ----------- util/classify_changes.py | 2 +- 10 files changed, 143 insertions(+), 91 deletions(-) delete mode 100644 setup.py create mode 100644 tests/project/test_no_setup_py_2061.py delete mode 100644 tests/project/test_setup.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 99bbd6b552..8b1cff78e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -399,6 +399,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r #### Changed - **a breaking change to** `requires-python`: it is `>=3.8`, so pip refuses an older interpreter at install, as "requires a different Python". The sources use 3.8 syntax, and the `bpc-*` backport tools that converted it for 3.6 and 3.7 are no longer maintained, so the conversion goes: the `bpc-*` requirements, `refactor()` and the `setup.py` build commands that ran it. The 3.7 KTLO (*keep the lights on*) legs go with it, and `coverage-comment.yml` lists no install failure as by design any more, which closes [#2040](https://github.com/JarryShaw/PyPCAPKit/issues/2040) ([#2051](https://github.com/JarryShaw/PyPCAPKit/issues/2051), [#2053](https://github.com/JarryShaw/PyPCAPKit/pull/2053)). +- `setup.py` is gone, and `pyproject.toml` is the only build configuration. With the 3.8 floor it was a Python 2 guard and a bare `setup()`: the wheel's files are identical without it, `pip install -e .` goes through PEP 660, pip still refuses 3.7 as "requires a different Python", and on 2.7 it fails on the `setuptools` build requirement before it would reach `setup.py`. `examples/benchmark/Dockerfile` stops copying it, and a test fails should `setup.py` return or a `COPY` there name a file the repository does not track. Dependabot now fetches `pyproject.toml` in `setup.py`'s place and passes it through unstubbed; the static `readme` stays, as it does not depend on what a stub would supply ([#2061](https://github.com/JarryShaw/PyPCAPKit/issues/2061), [#2066](https://github.com/JarryShaw/PyPCAPKit/pull/2066)). - renames with no compatibility alias left behind: `examples/sample` and `examples/samples` -- one letter apart, holding different things -- are now `examples/captures` and `examples/generators`. - the README is a landing page now, and Markdown rather than reStructuredText. `README.rst` became a much shorter `README.md`, keeping what a reader arriving from PyPI or a search result needs (what the library is, why it exists rather than Scapy or DPKT, how to install it, a worked example, where the documentation lives). **Module Structure**, **Engine Comparison**, **Engine support by Python version**, **Test Environment**, **Test Results** and **Installation Notes** were already duplicated in fuller form in `docs/source/index.rst`, so they are linked rather than restated; the blocks that existed nowhere else moved: **Testing** is now `docs/source/contributing/testing.rst`, and the `pipenv` and `make setup` block joined the Installation section of `docs/source/index.rst`. Requested by the project owner, and a deliberate exception to the reStructuredText convention, for the README only, since its renderers are GitHub and PyPI rather than Sphinx. `setup.py` reads `README.md` and declares `text/markdown`, and `MANIFEST.in` gains `include README.md`, which is belt-and-braces rather than load-bearing: setuptools' `sdist` ships a `README.md` regardless. Of the `include` lines only `CHANGELOG.md` is load-bearing, measured in the [#631](https://github.com/JarryShaw/PyPCAPKit/pull/631) entry below. `examples/benchmark/Dockerfile` copies `README.md`, while the `README.md` that `examples/benchmark/benchmark.py` names is the benchmark suite's own ([#619](https://github.com/JarryShaw/PyPCAPKit/pull/619)). - `CODE_OF_CONDUCT.md` moves from Contributor Covenant 1.4 to 3.0, at the owner's request. The text is the canonical 3.0 Markdown fetched from https://www.contributor-covenant.org/version/3/0/code_of_conduct/code_of_conduct.md rather than transcribed. Some things needed deciding. 3.0's `[NOTE` placeholders are filled or removed: the reporting channel names `jarryshaw@icloud.com` (the contact 1.4 carried and `SECURITY.md` uses as email fallback) plus GitHub's report-abuse form for a report about the maintainer, which a single-maintainer project cannot otherwise cover; the enforcement placeholder is an instruction to the adopter and is removed. 3.0's plural "Community Moderators" become the singular maintainer, this repository having no moderators. The enforcement ladder (Warning, Temporarily Limited Activities, Temporary Suspension, Permanent Ban) is a suggestion and is **kept**, each rung mapping to a lever one person holds on GitHub. 3.0 is CC BY-SA 4.0 where 1.4 carried no licence notice, so the attribution names version 3.0, links the permanent `version/3/0/` URL, carries the CC BY-SA 4.0 notice, indicates changes were made as BY requires, and says the share-alike term covers this document only (the code remains BSD-3-Clause, `LICENSE` untouched). The ladder renders as nested list items, checked against GitHub's Markdown API; [#613](https://github.com/JarryShaw/PyPCAPKit/pull/613) had to repair the 1.4 file when a stray list marker collapsed it into one nested item ([#624](https://github.com/JarryShaw/PyPCAPKit/pull/624)). diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index fe7cd486a8..bafcc87a2d 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -2963,6 +2963,16 @@ Changed the lights on*) legs go with it, and ``coverage-comment.yml`` lists no install failure as by design any more, which closes :issue:`2040` (:issue:`2051`, :pr:`2053`). +* ``setup.py`` is gone, and ``pyproject.toml`` is the only build configuration. With + the 3.8 floor it was a Python 2 guard and a bare ``setup()``: the wheel's files are + identical without it, ``pip install -e .`` goes through PEP 660, pip still refuses + 3.7 as "requires a different Python", and on 2.7 it fails on the ``setuptools`` + build requirement before it would reach ``setup.py``. + ``examples/benchmark/Dockerfile`` stops copying it, and a test fails should + ``setup.py`` return or a ``COPY`` there name a file the repository does not track. + Dependabot now fetches ``pyproject.toml`` in ``setup.py``'s place and passes it + through unstubbed; the static ``readme`` stays, as it does not depend on what a stub + would supply (:issue:`2061`, :pr:`2066`). * renames with no compatibility alias left behind: ``examples/sample`` and ``examples/samples`` -- one letter apart, holding different things -- are now ``examples/captures`` and ``examples/generators``. diff --git a/examples/benchmark/Dockerfile b/examples/benchmark/Dockerfile index 0b788ec9c3..789d336ba6 100644 --- a/examples/benchmark/Dockerfile +++ b/examples/benchmark/Dockerfile @@ -153,7 +153,7 @@ RUN set -eux; \ rm -f /tmp/pypcap-install.log # `pcapkit` last, because it is the thing under test and changes on every commit. -COPY pyproject.toml setup.py MANIFEST.in README.md ./ +COPY pyproject.toml MANIFEST.in README.md ./ COPY pcapkit ./pcapkit # --no-deps so the pinned requirements above stay authoritative: `pcapkit` diff --git a/pyproject.toml b/pyproject.toml index 710440e390..33e902b4de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,11 @@ build-backend = "setuptools.build_meta" [project] name = "pypcapkit" -# ``readme`` is static, not supplied by ``setup.py`` (#1968): Dependabot replaces -# ``setup.py`` with a stub that passes no long description, so a dynamic one fails -# to build the Pipfile's editable ``pypcapkit`` and every pip update job with it. +# ``readme`` is static (#1968). There is no ``setup.py`` (#2061), so +# ``fetch_project_file`` in Dependabot's Python file fetcher fetches +# ``pyproject.toml`` in its place, and its Pipfile updater writes that file as is: +# only a ``setup.py`` is stubbed. A static ``readme`` keeps the Pipfile's editable +# ``pypcapkit`` from depending on what a stub would supply. dynamic = [ "version" ] authors = [ { name="Jarry Shaw", email="jarryshaw@icloud.com" }, diff --git a/setup.py b/setup.py deleted file mode 100644 index 312b17d611..0000000000 --- a/setup.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import sys - -# Python 2 only, not ``requires-python``'s 3.8 (#2051). pip builds the metadata -# with this file before it compares ``requires-python``, so a 3.8 guard here would -# replace pip's "requires a different Python" with a traceback. A Python 2 that -# runs this file directly would otherwise reach ``setup()`` with a setuptools too -# old to read ``pyproject.toml``. -if sys.version_info[0] <= 2: - raise OSError("PyPCAPKit does not support Python 2!") - -try: - from setuptools import setup -except ImportError: - raise ImportError("setuptools is required to install PyPCAPKit!") - -# Everything is declared in ``pyproject.toml``. The long description is its -# static ``readme`` (#1968). -setup() diff --git a/tests/_tiers.py b/tests/_tiers.py index 4dca218957..a166207468 100644 --- a/tests/_tiers.py +++ b/tests/_tiers.py @@ -402,7 +402,7 @@ def is_unit_tier(path: 'pathlib.Path | str') -> 'bool': _IMPORT_TEXT = re.compile(rf'(?:\b(?:import|from)|-m)\s+{PROJECT_PIN_PACKAGE}(?:\.\w+)*(?![\w/.])') #: A string that is wholly a relative path, e.g. ``util/ci_timing.py`` or ``tests/cli``. _PATH_TEXT = re.compile(r'[\w.-]+(?:/[\w.-]+)*/?\Z') -#: A string that is wholly a dotted module name, e.g. ``tests.project.test_setup``. +#: A string that is wholly a dotted module name, e.g. ``tests.project.test_no_setup_py_2061``. _DOTTED_NAME = re.compile(r'[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+\Z') #: What a module asks of the running interpreter when it branches on it: diff --git a/tests/project/test_actions_pinned_1968.py b/tests/project/test_actions_pinned_1968.py index 0c8cf957da..04992cec97 100644 --- a/tests/project/test_actions_pinned_1968.py +++ b/tests/project/test_actions_pinned_1968.py @@ -22,8 +22,8 @@ either; and the root job's ``exclude-paths`` skips :file:`conda/requirements.txt`, which it would otherwise read. * :class:`TestDependabotCanBuildTheEditable` checks :file:`pyproject.toml` - declares its readme statically, as Dependabot's stub :file:`setup.py` supplies - none. + declares its readme statically, as Dependabot now fetches that file in place + of :file:`setup.py` and passes it through unstubbed. * :class:`TestPyUpOpensNoUpdates` checks :file:`.pyup.yml` turns PyUp's update pull requests off, as Dependabot opens them. @@ -203,12 +203,13 @@ def test_every_entry_is_weekly_and_names_one_kind_of_directory(self) -> None: class TestDependabotCanBuildTheEditable(unittest.TestCase): - """The Pipfile's editable ``pypcapkit`` builds under Dependabot's stub :file:`setup.py`. + """The Pipfile's editable ``pypcapkit`` reads its readme from :file:`pyproject.toml`. - Dependabot's ``SetupFileSanitizer`` replaces :file:`setup.py` with - ``setup(name=..., version="0.0.1", ...)``, which passes no long - description. A ``readme`` left in ``dynamic`` then fails the build, and every - root pip update with it. + There is no :file:`setup.py` (#2061), so ``fetch_project_file`` in + Dependabot's Python file fetcher fetches :file:`pyproject.toml` in its place, + and the Pipfile updater writes it unstubbed: ``SetupFileSanitizer`` runs only + on a :file:`setup.py`. A static ``readme`` does not depend on what a stub + would supply. """ def setUp(self) -> None: diff --git a/tests/project/test_no_setup_py_2061.py b/tests/project/test_no_setup_py_2061.py new file mode 100644 index 0000000000..aaffcc0cbc --- /dev/null +++ b/tests/project/test_no_setup_py_2061.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +"""#2061: there is no :file:`setup.py`, and the benchmark image copies only files that exist. + +``pyproject.toml`` declares everything, and the 3.8 floor (#2051) left +:file:`setup.py` a Python 2 guard and a bare ``setup()``, so it went. + +* :class:`TestNoSetupPy` fails if one comes back at the repository root. +* :class:`TestBenchmarkDockerfileCopies` checks every source a ``COPY`` in + :file:`examples/benchmark/Dockerfile` names is in the build context, the + repository root that ``run.sh`` passes. Its ``COPY`` of the packaging files + named :file:`setup.py` too, and a missing source fails only at ``docker + build``, which no CI job runs. + +""" + +from __future__ import annotations + +import pathlib +import shlex +import shutil +import subprocess # nosec: B404 +import unittest + +#: Test tier (#1958): reads the repository's files and asks git which it tracks. +__tier__ = 'LATEST' + +ROOT = pathlib.Path(__file__).resolve().parents[2] +DOCKERFILE = ROOT / 'examples' / 'benchmark' / 'Dockerfile' + + +def instructions(text: 'str') -> 'list[str]': + """The Dockerfile's instructions, each with its continuation lines joined.""" + result = [] # type: list[str] + pending = '' + for line in text.splitlines(): + stripped = line.strip() + if not pending and (not stripped or stripped.startswith('#')): + continue + if pending and stripped.startswith('#'): # a comment inside a continuation + continue + if stripped.endswith('\\'): + pending += stripped[:-1] + ' ' + continue + result.append(pending + stripped) + pending = '' + if pending: + result.append(pending) + return result + + +def copy_sources(text: 'str') -> 'list[str]': + """Every source path a build-context ``COPY`` names, in order. + + A ``COPY --from=...`` reads another stage or image, not the context, so it + is left out; so are flags and the destination. + + """ + sources = [] # type: list[str] + for instruction in instructions(text): + keyword, _, rest = instruction.partition(' ') + if keyword.upper() != 'COPY': + continue + words = shlex.split(rest) + if any(word.startswith('--from=') for word in words): + continue + operands = [word for word in words if not word.startswith('--')] + sources.extend(operands[:-1]) + return sources + + +def tracked() -> 'set[str] | None': + """Every path git tracks, or :data:`None` when this is not a checkout.""" + if shutil.which('git') is None or not (ROOT / '.git').exists(): + return None + result = subprocess.run(['git', 'ls-files', '-z'], cwd=ROOT, # nosec + capture_output=True, check=True) + return {name for name in result.stdout.decode('utf-8').split('\0') if name} + + +class TestNoSetupPy(unittest.TestCase): + """``pyproject.toml`` is the only build configuration.""" + + def test_there_is_no_setup_py(self) -> None: + self.assertFalse((ROOT / 'setup.py').exists(), + 'setup.py is back; pyproject.toml declares everything (#2061)') + + +class TestBenchmarkDockerfileCopies(unittest.TestCase): + """Every ``COPY`` source in the benchmark image is in the build context.""" + + def setUp(self) -> None: + if not DOCKERFILE.is_file(): # pragma: no cover - an sdist does not ship it + self.skipTest('examples/benchmark/Dockerfile is not in this tree') + self.sources = copy_sources(DOCKERFILE.read_text(encoding='utf-8')) + + def test_the_parser_finds_the_packaging_copy(self) -> None: + """A parser that found nothing would pass the check below vacuously.""" + for name in ('pyproject.toml', 'MANIFEST.in', 'README.md', 'pcapkit'): + with self.subTest(name=name): + self.assertIn(name, self.sources) + + def test_every_source_exists(self) -> None: + """Tracked, on a checkout: ``docker build`` runs from a fresh clone too.""" + files = tracked() + for source in self.sources: + with self.subTest(source=source): + if files is None: + self.assertTrue((ROOT / source).exists()) + else: + prefix = source.rstrip('/') + '/' + self.assertTrue(source in files or any(name.startswith(prefix) for name in files), + f'{source} is not tracked, so a COPY of it fails the build') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/project/test_setup.py b/tests/project/test_setup.py deleted file mode 100644 index f0e28b76ea..0000000000 --- a/tests/project/test_setup.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import annotations - -import importlib.util -import pathlib -import sys -import types -import unittest -from unittest import mock - -#: Test tier (#1958): loads setup.py under the running interpreter, as every leg's -#: install does. -__tier__ = 'ALL' - -ROOT = pathlib.Path(__file__).resolve().parents[2] - - -class SetupModuleTests(unittest.TestCase): - def _load_setup_module(self): - fake_setuptools = types.ModuleType('setuptools') - captured = {} - - def fake_setup(*args, **kwargs): - captured['args'] = args - captured['kwargs'] = kwargs - - fake_setuptools.setup = fake_setup - with mock.patch.dict(sys.modules, {'setuptools': fake_setuptools}): - spec = importlib.util.spec_from_file_location('project_setup', ROOT / 'setup.py') - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module, captured - - def test_setup_passes_no_long_description(self) -> None: - """``pyproject.toml``'s static ``readme`` is the long description (#1968). - - Dependabot replaces this file with a stub that passes none, so a long - description supplied here leaves ``readme`` dynamic and the stub's build - failing. - """ - _, captured = self._load_setup_module() - self.assertNotIn('long_description', captured['kwargs']) - self.assertNotIn('long_description_content_type', captured['kwargs']) - - def test_setup_overrides_no_build_command(self) -> None: - """No ``cmdclass``: the commands only ever ran the BPC conversion (#2051). - - Nothing converts the 3.8 syntax any more, so a build command overridden - here has nothing left to add to setuptools' own. - """ - module, captured = self._load_setup_module() - self.assertEqual(captured, {'args': (), 'kwargs': {}}) - self.assertFalse(hasattr(module, 'refactor')) - - -if __name__ == '__main__': - unittest.main() diff --git a/util/classify_changes.py b/util/classify_changes.py index a5247e66be..d986a22591 100644 --- a/util/classify_changes.py +++ b/util/classify_changes.py @@ -11,7 +11,7 @@ A file is docs iff :func:`is_docs` holds: it is under ``docs/`` or ends in ``.md``, and is not under one of :data:`CODE_DIRS` -- so a Markdown file in one of those (a PR template, a test fixture) is code, and so is every other file -(``pyproject.toml``, ``setup.py``, ...). The diff is ``HEAD^1..HEAD``, a pull +(``pyproject.toml``, ``Makefile``, ...). The diff is ``HEAD^1..HEAD``, a pull request's merge commit against its first parent, which is exactly what merging would add; ``--no-renames`` lists both sides of a rename, so moving a code file under ``docs/`` is still a code change.