From 0b21f62553fe4a3c82b1e5c21f75fd142a8da246 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Thu, 22 Oct 2026 19:08:06 +0200 Subject: [PATCH 1/2] fix(check): SPDX license string; E501 off in tests (device-types pilot) The pilot's build warned that setuptools refuses a license table and License classifiers from 2027-02-18 (PEP 639); check now reports both and the template builds with setuptools>=77. Recorded device output in tests keeps its lines, so the template turns E501 off under tests/. --- CHANGELOG.md | 7 +++++++ STANDARDS.md | 7 +++++-- napalm_standard/check.py | 5 +++++ pyproject.toml | 2 +- template/pyproject.toml | 6 +++++- tests/conftest.py | 2 +- tests/test_check.py | 24 +++++++++++++++++++++--- 7 files changed, 45 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c9981..06bf013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/), versions fol ## [Unreleased] +### Changed +- `check` reports a license given as a TOML table or as a `License ::` classifier: + setuptools refuses both from 2027-02-18 (PEP 639). The template builds with + setuptools>=77. +- E501 is off under `tests/`: recorded device output keeps its lines (found in the + device-types pilot). + ## [0.1.0] - 2026-10-22 ### Added diff --git a/STANDARDS.md b/STANDARDS.md index 098bfc8..3685c5a 100644 --- a/STANDARDS.md +++ b/STANDARDS.md @@ -101,7 +101,9 @@ A pull request without tests for what it adds or changes is not merged. ## 5. Clean code -- **Format and lint:** `ruff format` and `ruff check`, line length 100. +- **Format and lint:** `ruff format` and `ruff check` with `E, F, I, UP, B`, line length + 100. Only under `tests/` may a line be longer (E501 off there): recorded device output + keeps its lines. - **Types:** every public function is annotated; `mypy --strict` (§6 for the old code). - **Small functions:** under 30 lines recommended, 50 at most; cyclomatic complexity 10 at most. One function does one thing. @@ -165,7 +167,8 @@ release tags: someone else gets a descriptive one instead. The `napalm.drivers` entry point names do not change with the package name. - **`pyproject.toml`** declares `name`, `version`, `description`, `readme`, - `license = "Apache-2.0"`, `requires-python = ">=3.10"`, the dependencies, a + `license = "Apache-2.0"` (an SPDX string, PEP 639; no table, no `License ::` + classifier, `setuptools>=77`), `requires-python = ">=3.10"`, the dependencies, a `dev` extra, `[project.urls]` with `Repository` (and `Issues`) on `https://git.netork.io/NAPALM/…`, and the `napalm.drivers` entry points. - **Dependencies are ranges with true floors.** A driver is a library: it never pins diff --git a/napalm_standard/check.py b/napalm_standard/check.py index a112284..3a8705d 100644 --- a/napalm_standard/check.py +++ b/napalm_standard/check.py @@ -77,6 +77,11 @@ def _metadata(repo: Repo) -> Iterator[Finding]: for field in REQUIRED_FIELDS: if not repo.project.get(field): yield Finding("metadata", f"[project] has no {field}") + # PEP 639: setuptools refuses the table form and license classifiers from 2027-02-18. + if isinstance(repo.project.get("license"), dict): + yield Finding("metadata", 'license must be an SPDX string: license = "Apache-2.0"') + if any(c.startswith("License ::") for c in repo.project.get("classifiers", [])): + yield Finding("metadata", "drop the License :: classifier; the SPDX license says it") def _python(repo: Repo) -> Iterator[Finding]: diff --git a/pyproject.toml b/pyproject.toml index 6043b7a..05ac39c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] diff --git a/template/pyproject.toml b/template/pyproject.toml index 018b52d..de35015 100644 --- a/template/pyproject.toml +++ b/template/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] @@ -43,6 +43,10 @@ target-version = "py310" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B"] +[tool.ruff.lint.per-file-ignores] +# Recorded device output keeps its lines. +"tests/**" = ["E501"] + [tool.mypy] python_version = "3.10" strict = true diff --git a/tests/conftest.py b/tests/conftest.py index e83cd3f..1ec1a19 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import pytest PYPROJECT = """\ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] diff --git a/tests/test_check.py b/tests/test_check.py index fc91a64..7383c62 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -51,10 +51,28 @@ def test_a_missing_project_field_is_reported(make_repo: RepoFactory, field: str) assert field in findings[0].message -def test_a_license_table_is_accepted(make_repo: RepoFactory) -> None: - pyproject = PYPROJECT.replace('license = "Apache-2.0"', 'license = { text = "Apache-2.0" }') +@pytest.mark.parametrize( + "license_line", + ['license = { text = "Apache-2.0" }', 'license = { file = "LICENSE" }'], +) +def test_a_license_table_is_reported(make_repo: RepoFactory, license_line: str) -> None: + """setuptools refuses the table form from 2027-02-18 (PEP 639).""" + pyproject = PYPROJECT.replace('license = "Apache-2.0"', license_line) - assert check_repo(make_repo({"pyproject.toml": pyproject})) == [] + findings = check_repo(make_repo({"pyproject.toml": pyproject})) + + assert rules(findings) == {"metadata"} + assert "SPDX" in findings[0].message + + +def test_a_license_classifier_is_reported(make_repo: RepoFactory) -> None: + pyproject = PYPROJECT.replace( + 'requires-python = ">=3.10"\n', + 'requires-python = ">=3.10"\n' + 'classifiers = ["License :: OSI Approved :: Apache Software License"]\n', + ) + + assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"metadata"} @pytest.mark.parametrize("spec", [">=3.8", ">=3.9", ""]) From ba133d9de33bed8b95a1dd745d93c9a98d4714c4 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Thu, 22 Oct 2026 19:14:55 +0200 Subject: [PATCH 2/2] fix(ci): audit the package's dependencies, not the tool venv The device-types pilot failed pip-audit on pip 25.0.1 of the venv that also held bandit and pip-audit. The package now goes into a venv of its own with a current pip; its frozen set (URL installs left out) is what pip-audit checks. --- .gitea/workflows/driver.yml | 25 ++++++++++++++++--------- CHANGELOG.md | 4 ++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/driver.yml b/.gitea/workflows/driver.yml index 6cbf1b5..470908d 100644 --- a/.gitea/workflows/driver.yml +++ b/.gitea/workflows/driver.yml @@ -137,18 +137,25 @@ jobs: with: python-version: "3.12" - name: Install + # Two venvs: the tools, and the package alone. Auditing the tools' venv + # reported the runner's own pip instead of anything the package brings. run: | - python -m venv .venv - echo "$PWD/.venv/bin" >> "$GITHUB_PATH" - .venv/bin/pip install -q "$STANDARD" bandit pip-audit - if [ -n "${{ inputs.pre-install }}" ]; then .venv/bin/pip install -q ${{ inputs.pre-install }}; fi - .venv/bin/pip install -q . + python -m venv .tools + .tools/bin/pip install -q --upgrade pip + .tools/bin/pip install -q "$STANDARD" bandit pip-audit + python -m venv .audit + .audit/bin/pip install -q --upgrade pip + if [ -n "${{ inputs.pre-install }}" ]; then .audit/bin/pip install -q ${{ inputs.pre-install }}; fi + .audit/bin/pip install -q . - name: bandit - run: bandit -q -r "$(napalm-standard package)" + run: .tools/bin/bandit -q -r "$(.tools/bin/napalm-standard package)" - name: pip-audit - # The installed set: what a user gets today from the package's ranges. - # A package not on PyPI yet (the driver itself, a git pre-install) is skipped with a note. - run: pip-audit --skip-editable --progress-spinner off + # What a user gets today from the package's ranges. Packages installed from + # a URL (the package itself, a git pre-install) are not on PyPI's index yet. + run: | + .audit/bin/pip freeze --all | grep -vE ' @ |^-e ' > audit-requirements.txt + cat audit-requirements.txt + .tools/bin/pip-audit -r audit-requirements.txt --no-deps --disable-pip --progress-spinner off build: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 06bf013..6d80a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/), versions fol - E501 is off under `tests/`: recorded device output keeps its lines (found in the device-types pilot). +### Fixed +- `security` audited the runner's own pip instead of the package: pip-audit now + checks a venv holding only the package and its dependencies. + ## [0.1.0] - 2026-10-22 ### Added