From 0ee030bb9155ee82eea0c5f3278e0b64b4a5faf4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 5 Oct 2026 14:10:29 -0400 Subject: [PATCH] fix: derive readiness from declared project metadata --- pyproject.toml | 1 + scripts/verify_release_ready.py | 47 ++++++++++++++++++++------------- tests/test_release_readiness.py | 22 +++++++++++++++ 3 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 tests/test_release_readiness.py diff --git a/pyproject.toml b/pyproject.toml index 954d060..360da38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "pytest>=7.0", + "tomli>=2.0.1; python_version < '3.11'", "pytest-cov>=4.0", "ruff>=0.1.0", "black>=23.0", diff --git a/scripts/verify_release_ready.py b/scripts/verify_release_ready.py index 0b04e9b..baa01d6 100755 --- a/scripts/verify_release_ready.py +++ b/scripts/verify_release_ready.py @@ -9,12 +9,18 @@ - Dependencies are available """ -import os import re import subprocess import sys from pathlib import Path +from packaging.utils import InvalidName, canonicalize_name + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + def check_version(): """Check that version is valid and follows semantic versioning.""" @@ -48,24 +54,27 @@ def check_pyproject_toml(): print(" ❌ pyproject.toml not found") return False - content = pyproject_file.read_text() - required_fields = { - 'name': r'name\s*=\s*["\']openhcs["\']', - 'version': r'version\s*=', - 'description': r'description\s*=', - 'authors': r'authors\s*=', - 'build-backend': r'build-backend\s*=\s*["\']setuptools\.build_meta["\']', - } - - all_found = True - for field, pattern in required_fields.items(): - if not re.search(pattern, content): - print(f" ❌ Missing or invalid field: {field}") - all_found = False - - if all_found: - print(" ✅ All required fields present") - return all_found + with pyproject_file.open("rb") as project_file: + declaration = tomllib.load(project_file) + project = declaration.get("project", {}) + name = project.get("name") + if not isinstance(name, str): + print(" ❌ Missing or invalid project.name") + return False + try: + canonicalize_name(name, validate=True) + except InvalidName: + print(" ❌ Invalid project.name") + return False + for field in ("version", "description", "authors"): + if not project.get(field): + print(f" ❌ Missing or invalid project.{field}") + return False + if not declaration.get("build-system", {}).get("build-backend"): + print(" ❌ Missing build-system.build-backend") + return False + print(f" ✅ Declared project metadata present: {name}") + return True def check_readme(): diff --git a/tests/test_release_readiness.py b/tests/test_release_readiness.py new file mode 100644 index 0000000..b2fc9aa --- /dev/null +++ b/tests/test_release_readiness.py @@ -0,0 +1,22 @@ +"""Readiness checks consume the repository's declared project metadata.""" + +import runpy +from pathlib import Path + +import pytest + + +CHECKS = runpy.run_path(str(Path(__file__).parents[1] / "scripts/verify_release_ready.py")) +PROJECT = (Path(__file__).parents[1] / "pyproject.toml").read_text() + + +def test_actual_project_declaration_is_release_ready(monkeypatch): + monkeypatch.chdir(Path(__file__).parents[1]) + assert CHECKS["check_pyproject_toml"]() + + +@pytest.mark.parametrize("name", ('name = "invalid name"', "")) +def test_missing_or_invalid_project_name_is_rejected(tmp_path, monkeypatch, name): + (tmp_path / "pyproject.toml").write_text(PROJECT.replace('name = "python-introspect"', name)) + monkeypatch.chdir(tmp_path) + assert not CHECKS["check_pyproject_toml"]()