diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 59017ac8e2..8789364877 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,8 @@ # Left out (#1968): conda/requirements.txt, which Conda Update's `Refresh the # platform pins` and `Setup Conda Distribution` steps write, and the benchmark's # image digests, which its Dockerfile takes from examples/benchmark/python-images.txt. +# The pip entry's `exclude-paths` keeps conda/requirements.txt out: the `/` job +# otherwise reads requirements files one directory down, conda/ included. version: 2 updates: @@ -15,6 +17,8 @@ updates: - "/" - "/docs" # Pins Sphinx and sphinx-autodoc-typehints deliberately, bumps included - "/examples/benchmark" # Nothing else updates the benchmark's pins + exclude-paths: # Matched against the path from the repository root + - "conda/requirements.txt" schedule: interval: "weekly" diff --git a/MANIFEST.in b/MANIFEST.in index f9ccf204d7..78bba2e6fb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -40,9 +40,9 @@ prune typings # ``adding license file 'LICENSE'`` in the build log, twice, in a build with this # line deleted. # -# ``setup.py`` does read ``README.md`` unguarded for the ``long_description`` -# (``setup.py:31-36``), and it reads it from wherever ``setup.py`` is executing -- -# which during ``pip install`` of an sdist is the unpacked sdist, not a checkout. +# ``pyproject.toml``'s ``readme`` does read ``README.md`` unguarded for the long +# description, and it reads it from wherever the build is running -- which +# during ``pip install`` of an sdist is the unpacked sdist, not a checkout. # So the failure mode is real in principle; it just cannot be reached by removing # this line, because the README is in the sdist regardless. # diff --git a/pyproject.toml b/pyproject.toml index 3182cecd9e..2276a6c895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,10 @@ build-backend = "setuptools.build_meta" [project] name = "pypcapkit" -dynamic = [ "version", "readme" ] +# ``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. +dynamic = [ "version" ] authors = [ { name="Jarry Shaw", email="jarryshaw@icloud.com" }, ] @@ -58,6 +61,7 @@ license = { text="BSD 3-Clause License" } # besides. The ``PCAP_CT`` extra needs 3.10 regardless. requires-python = ">=3.6, <4" description = "PyPCAPKit: comprehensive network packet analysis library" +readme = { file = "README.md", content-type = "text/markdown" } keywords = [ "network", "pcap", "packet" ] classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/setup.py b/setup.py index 34f96857e5..02dade1c03 100644 --- a/setup.py +++ b/setup.py @@ -28,14 +28,6 @@ logger.addHandler(handler) -def get_long_description() -> "str": - """Extract description from README.md, for PyPI's usage.""" - readme = os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.md") - with open(readme, encoding="utf-8") as file: - long_description = file.read() - return long_description - - def refactor(path: "str") -> "None": """Refactor code.""" import subprocess # nosec: B404 @@ -160,8 +152,5 @@ def run(self) -> "None": # above, so the tree it archives is already converted before the wheel is # written, and ``dist_dir`` holds only the finished wheels (#1773). -setup( - cmdclass=cmdclass, - long_description=get_long_description(), - long_description_content_type="text/markdown", -) +# The long description is ``pyproject.toml``'s static ``readme`` (#1968). +setup(cmdclass=cmdclass) diff --git a/tests/project/test_actions_pinned_1968.py b/tests/project/test_actions_pinned_1968.py index 55735711c5..0c8cf957da 100644 --- a/tests/project/test_actions_pinned_1968.py +++ b/tests/project/test_actions_pinned_1968.py @@ -19,7 +19,11 @@ * :class:`TestDependabotCoversEveryManifest` checks the owner's ruling on #1968: ``pip`` over the root, :file:`docs` and :file:`examples/benchmark`, and neither :file:`conda` nor a ``docker`` entry, as Dependabot cannot update - either. + 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. * :class:`TestPyUpOpensNoUpdates` checks :file:`.pyup.yml` turns PyUp's update pull requests off, as Dependabot opens them. @@ -31,6 +35,11 @@ import unittest from typing import TYPE_CHECKING +try: + import tomllib +except ImportError: # pragma: no cover + tomllib = None # type: ignore[assignment] + from tests import _workflows from tests._tiers import ROOT @@ -168,6 +177,16 @@ def test_conda_is_not_watched(self) -> None: with self.subTest(ecosystem=ecosystem): self.assertNotIn('/conda', directories(entry)) + def test_the_root_job_skips_the_conda_requirements(self) -> None: + """The ``/`` job reads requirements files one directory down, :file:`conda` included. + + dependabot-core's ``SharedFileFetcher.req_files_for_dir`` fetches them, and + ``FileFetchers::Base.filter_excluded`` drops an entry whose path from the + repository root matches ``exclude-paths``; without it the job checked + :file:`conda/requirements.txt`'s cryptography pin. + """ + self.assertIn('conda/requirements.txt', updates()['pip'].get('exclude-paths') or []) + def test_there_is_no_docker_entry(self) -> None: """Dependabot reads a ``FROM`` that names its image; the benchmark's is an ``ARG``. @@ -183,6 +202,27 @@ def test_every_entry_is_weekly_and_names_one_kind_of_directory(self) -> None: self.assertEqual(len({'directory', 'directories'} & set(entry)), 1) +class TestDependabotCanBuildTheEditable(unittest.TestCase): + """The Pipfile's editable ``pypcapkit`` builds under Dependabot's stub :file:`setup.py`. + + 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. + """ + + def setUp(self) -> None: + if tomllib is None: # pragma: no cover + self.skipTest('tomllib needs Python 3.11') + self.project = tomllib.loads((ROOT / 'pyproject.toml').read_text(encoding='utf-8'))['project'] + + def test_the_readme_is_not_dynamic(self) -> None: + self.assertNotIn('readme', self.project.get('dynamic', [])) + + def test_the_readme_is_the_markdown_file(self) -> None: + self.assertEqual(self.project['readme'], {'file': 'README.md', 'content-type': 'text/markdown'}) + + class TestPyUpOpensNoUpdates(unittest.TestCase): """PyUp's update pull requests are off, and nothing in its file turns one back on.""" diff --git a/tests/project/test_setup.py b/tests/project/test_setup.py index de5372122b..59dbad72d2 100644 --- a/tests/project/test_setup.py +++ b/tests/project/test_setup.py @@ -47,15 +47,19 @@ def fake_setup(*args, **kwargs): spec.loader.exec_module(module) return module, captured, loaded - def test_get_long_description_reads_readme(self) -> None: - module, _, loaded = self._load_setup_module() - try: - description = module.get_long_description() - finally: - for name in loaded + ['project_setup']: - sys.modules.pop(name, None) - - self.assertIn('PyPCAPKit', description) + 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, loaded = self._load_setup_module() + for name in loaded + ['project_setup']: + sys.modules.pop(name, None) + + self.assertNotIn('long_description', captured['kwargs']) + self.assertNotIn('long_description_content_type', captured['kwargs']) def test_refactor_runs_no_backports_on_modern_python(self) -> None: module, _, loaded = self._load_setup_module()