From 0542b73d5eb40ff77e50f00052fe4579171ab59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Mon, 26 Oct 2026 08:39:37 +0100 Subject: [PATCH] add dependabot config and dependency pin sync test --- .github/dependabot.yml | 30 +++++++++++++++++++ tests/test_dependency_pins.py | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 tests/test_dependency_pins.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..064d74c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +version: 2 + +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "05:00" + open-pull-requests-limit: 5 + groups: + python-minor-and-patch: + update-types: + - "minor" + - "patch" + # ruff minors can enable new default rules, so it is bumped on its own + exclude-patterns: + - "ruff" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "05:00" + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" diff --git a/tests/test_dependency_pins.py b/tests/test_dependency_pins.py new file mode 100644 index 0000000..649ca95 --- /dev/null +++ b/tests/test_dependency_pins.py @@ -0,0 +1,55 @@ +import re +import tomllib +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + +REQUIREMENT = re.compile(r"^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:==\s*(\S+))?$") + + +def parse_requirements(lines): + # Maps lower-cased name -> pinned version (None when unpinned). Blank + # lines, comments and -r includes are skipped. + parsed = {} + for line in lines: + line = line.split("#", 1)[0].strip() + if not line or line.startswith("-"): + continue + match = REQUIREMENT.match(line) + if match is None: + raise ValueError(f"unsupported requirement line: {line!r}") + name, version = match.groups() + parsed[name.lower()] = version + return parsed + + +def read_requirements(name): + return parse_requirements((ROOT / name).read_text().splitlines()) + + +def read_pyproject(): + with (ROOT / "pyproject.toml").open("rb") as handle: + return tomllib.load(handle)["project"] + + +class DependencyPinsTest(unittest.TestCase): + # Dependabot bumps requirements.txt and pyproject.toml independently, so a + # PR that touches only one of them fails here: update the other file in + # the same PR. + + def test_runtime_pins_match_pyproject(self): + requirements = read_requirements("requirements.txt") + declared = parse_requirements(read_pyproject()["dependencies"]) + + self.assertEqual(requirements, declared) + + def test_dev_requirements_listed_in_pyproject_extra(self): + dev = read_requirements("requirements-dev.txt") + extra = parse_requirements(read_pyproject()["optional-dependencies"]["dev"]) + + self.assertEqual(set(dev), set(extra)) + + +if __name__ == "__main__": + unittest.main()