From 0304f986943604fb6ac224a863669fb4fb0387cf Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Mon, 2 Nov 2026 13:48:32 +0100 Subject: [PATCH] fix: declare plain-parser 0.2.0 in the wheel pyproject.toml carries the dependencies an installed client gets, and it still named plain-parser 0.1.4. That release has no project module, so codeplain 0.4.0 stopped on an ImportError before it read anything. requirements.txt was right, which is why every test and every CI job passed: they all build from it, never from the wheel. A test now compares the pins of the two files. --- pyproject.toml | 2 +- tests/test_dependency_pins.py | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/test_dependency_pins.py diff --git a/pyproject.toml b/pyproject.toml index ecafff2f..e61a01f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Topic :: Software Development :: Code Generators", ] dependencies = [ - "plain-parser==0.1.4", + "plain-parser==0.2.0", # Both transitive via plain-parser; pinned so an automatic mistletoe upgrade doesn't change spec hashes. "mistletoe==1.6.0", diff --git a/tests/test_dependency_pins.py b/tests/test_dependency_pins.py new file mode 100644 index 00000000..c3b4714b --- /dev/null +++ b/tests/test_dependency_pins.py @@ -0,0 +1,52 @@ +"""The pins of requirements.txt and pyproject.toml must say the same thing. + +requirements.txt builds the development environment and the CI jobs. pyproject.toml is what the +wheel carries, so it is what an installed client gets. A pin changed in one file and not the other +passes every test here and still ships a client that cannot start. +""" + +import os +import re +import tomllib + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def _name(requirement: str) -> str: + return re.split(r"[=<>@\[]", requirement, maxsplit=1)[0].strip().lower() + + +def _requirements() -> dict[str, str]: + with open(os.path.join(REPO, "requirements.txt"), encoding="utf-8") as f: + lines = [line.strip() for line in f] + return {_name(line): line for line in lines if line and not line.startswith("#")} + + +def _pyproject() -> tuple[dict[str, str], dict[str, str]]: + with open(os.path.join(REPO, "pyproject.toml"), "rb") as f: + pyproject = tomllib.load(f)["project"] + runtime = {_name(item): item for item in pyproject["dependencies"]} + dev = {_name(item): item for item in pyproject["optional-dependencies"]["dev"]} + return runtime, dev + + +def test_every_pin_of_the_wheel_matches_requirements(): + requirements = _requirements() + runtime, dev = _pyproject() + + mismatched = { + name: (requirement, requirements.get(name)) + for name, requirement in {**runtime, **dev}.items() + if requirements.get(name) != requirement + } + + assert mismatched == {}, f"pyproject.toml and requirements.txt disagree: {mismatched}" + + +def test_requirements_names_nothing_the_wheel_leaves_out(): + requirements = _requirements() + runtime, dev = _pyproject() + + missing = sorted(set(requirements) - set(runtime) - set(dev)) + + assert missing == [], f"requirements.txt pins what pyproject.toml does not declare: {missing}"