From 2c22fbdf3bea7f17ec343469a8c1841c7754e37b Mon Sep 17 00:00:00 2001 From: melbinjp Date: Tue, 18 Aug 2026 22:23:06 +0530 Subject: [PATCH] version: one source of truth, because the tool was misreporting its own pyproject carried version = "0.1.2" while src/docproof/__init__.py carried "0.1.0". Three releases had shipped and the README pinned v0.1.2, so anyone who followed the README ran a tool whose every report header read docproof 0.1.0 - their-repo, N document(s) and whose --version agreed. The packaging metadata was right; the number the tool says about itself was two releases stale. That is a documented claim contradicted by the repository, which is the exact thing this tool exists to find, inside this tool. Found while checking whether docproof was fit to offer into another project's CI, after a gsd-core maintainer had been pointed at it. He would have seen 0.1.0. Correcting the number would have left the second copy in place, so the second copy is gone instead: hatchling reads the package attribute and there is one literal. Bumped to 0.1.3, which is also the release that carries the rename-chain fix in #8. tests/test_version_has_one_source.py asserts the SHAPE rather than the value. A test that the two numbers are equal passes today and rots the moment somebody bumps one of them, which is how this arose. It asserts there is no static version under [project] at all, so the drift cannot be reintroduced by remembering to keep two things in sync - the class of rule this repository exists because people forget. Verified: full suite 167 passing, and `python -m build --wheel` produces docproof-0.1.3-py3-none-any.whl, so the build really does read the attribute. --- pyproject.toml | 11 +++- src/docproof/__init__.py | 2 +- tests/test_version_has_one_source.py | 83 ++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 tests/test_version_has_one_source.py diff --git a/pyproject.toml b/pyproject.toml index 3128a3c..1ed0921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,13 @@ build-backend = "hatchling.build" [project] name = "docproof" -version = "0.1.2" +# DYNAMIC, and that is the fix rather than a preference. This read `version = "0.1.2"` +# while `src/docproof/__init__.py` said "0.1.0", so `docproof --version` and every +# report header printed 0.1.0 on an install pinned to v0.1.2. Two sources of truth for +# one number, disagreeing - which is the exact defect this tool exists to find, in this +# tool. Hatchling reads the package attribute now, so there is one literal and the +# build follows it. Correcting the number would have left the second copy in place. +dynamic = ["version"] description = "Prove your documentation against your code. Deterministically, with receipts." readme = "README.md" requires-python = ">=3.10" @@ -34,6 +40,9 @@ dev = [ [project.scripts] docproof = "docproof.cli:main" +[tool.hatch.version] +path = "src/docproof/__init__.py" + [tool.hatch.build.targets.wheel] packages = ["src/docproof"] diff --git a/src/docproof/__init__.py b/src/docproof/__init__.py index a346b63..35da8e3 100644 --- a/src/docproof/__init__.py +++ b/src/docproof/__init__.py @@ -1,3 +1,3 @@ """docproof — prove your documentation against your code.""" -__version__ = "0.1.0" +__version__ = "0.1.3" diff --git a/tests/test_version_has_one_source.py b/tests/test_version_has_one_source.py new file mode 100644 index 0000000..6da38ad --- /dev/null +++ b/tests/test_version_has_one_source.py @@ -0,0 +1,83 @@ +"""The version is declared once, and the build reads it from there. + +THE DEFECT, MEASURED 2026-08-18 +------------------------------- +`pyproject.toml` carried `version = "0.1.2"` while `src/docproof/__init__.py` carried +`__version__ = "0.1.0"`. Three releases had shipped and the README pinned v0.1.2, so a user +who followed the README got a tool whose every report header read: + + docproof 0.1.0 - their-repo, N document(s) + +`--version` said the same. The packaging metadata was right and the number the tool SAYS +ABOUT ITSELF was two releases stale, which is a documented claim contradicted by the +repository - the exact thing docproof is for, inside docproof. + +Found while checking whether the tool was fit to offer into another project's CI, after a +gsd-core maintainer had been pointed at it. + +WHY THIS IS A STRUCTURAL TEST AND NOT A VALUE TEST +-------------------------------------------------- +Asserting the two numbers are EQUAL would pass today and rot the moment someone bumps one +of them, which is precisely how this arose. The fix was to delete the second copy: hatchling +reads the package attribute, so there is nothing left to disagree. This test asserts THAT, +because a rule that says "keep these in sync" is a rule somebody has to remember, and the +whole point of this repository is that those are the rules that fail. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PYPROJECT = ROOT / "pyproject.toml" +INIT = ROOT / "src" / "docproof" / "__init__.py" + + +def _pyproject() -> str: + return PYPROJECT.read_text(encoding="utf-8") + + +def test_pyproject_declares_no_version_of_its_own() -> None: + """A static `version = "..."` under [project] is the second copy that caused this.""" + project_block = _pyproject().split("[project]", 1)[1].split("\n[", 1)[0] + static = re.findall(r"^version\s*=", project_block, re.M) + + assert static == [], ( + "pyproject declares a static version again. That is the second source of truth " + "that printed 0.1.0 on a v0.1.2 install. Use the package attribute." + ) + + +def test_pyproject_marks_version_dynamic_and_points_at_the_package() -> None: + text = _pyproject() + + assert 'dynamic = ["version"]' in text + assert "[tool.hatch.version]" in text + assert 'path = "src/docproof/__init__.py"' in text + + +def test_the_package_carries_exactly_one_version_literal() -> None: + literals = re.findall(r'__version__\s*=\s*"([^"]+)"', INIT.read_text(encoding="utf-8")) + + assert len(literals) == 1, f"expected one version literal, found {literals}" + + +def test_the_reported_version_is_the_declared_one() -> None: + """What `--version` and every report header print comes from that literal. + + The header is the user-visible surface that was wrong, so it is asserted directly + rather than trusting that importing the name is enough. + """ + from docproof import __version__ + + declared = re.search(r'__version__\s*=\s*"([^"]+)"', INIT.read_text(encoding="utf-8")) + assert declared is not None + assert __version__ == declared.group(1) + + +def test_the_version_is_a_plain_release_number() -> None: + """Guards the bump itself: a stray suffix or an empty string would ship silently.""" + from docproof import __version__ + + assert re.fullmatch(r"\d+\.\d+\.\d+", __version__), __version__