-
Notifications
You must be signed in to change notification settings - Fork 25
Add core Python/Matplotlib version contract checks #613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cvanelteren
wants to merge
7
commits into
main
Choose a base branch
from
ci/check-core-version-contract
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+443
−131
Open
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
53560f2
Add core Python/Matplotlib version contract checks
cvanelteren 0d2503a
Document core version contract helpers
cvanelteren 2467ae9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] ea06e53
Update ultraplot/tests/test_core_versions.py
cvanelteren d769ee1
Make core version support explicit
cvanelteren 2ee34b7
Handle cross-major version filtering
cvanelteren f42d2d5
Add pip Dependabot updates
cvanelteren File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Shared helpers for UltraPlot's supported Python/Matplotlib version contract. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
| try: | ||
| import tomllib | ||
| except ModuleNotFoundError: # pragma: no cover | ||
| import tomli as tomllib | ||
|
|
||
|
|
||
| ROOT = Path(__file__).resolve().parents[2] | ||
| PYPROJECT = ROOT / "pyproject.toml" | ||
|
|
||
|
|
||
| def load_pyproject(path: Path = PYPROJECT) -> dict: | ||
| """ | ||
| Load the project metadata used to define the supported version contract. | ||
| """ | ||
| with path.open("rb") as fh: | ||
| return tomllib.load(fh) | ||
|
|
||
|
|
||
| def _expand_half_open_minor_range(spec: str) -> list[str]: | ||
| """ | ||
| Expand constraints like ``>=3.10,<3.15`` into minor-version strings. | ||
| """ | ||
| min_match = re.search(r">=\s*(\d+\.\d+)", spec) | ||
| max_match = re.search(r"<\s*(\d+\.\d+)", spec) | ||
| if min_match is None or max_match is None: | ||
| return [] | ||
| major_min, minor_min = map(int, min_match.group(1).split(".")) | ||
| major_max, minor_max = map(int, max_match.group(1).split(".")) | ||
| versions = [] | ||
| major, minor = major_min, minor_min | ||
| while (major, minor) < (major_max, minor_max): | ||
| versions.append(f"{major}.{minor}") | ||
| minor += 1 | ||
| return versions | ||
|
|
||
|
|
||
| def supported_python_versions(pyproject: dict | None = None) -> list[str]: | ||
| """ | ||
| Return the supported Python minors derived from ``requires-python``. | ||
| """ | ||
| pyproject = pyproject or load_pyproject() | ||
| return _expand_half_open_minor_range(pyproject["project"]["requires-python"]) | ||
|
|
||
|
|
||
| def supported_matplotlib_versions(pyproject: dict | None = None) -> list[str]: | ||
| """ | ||
| Return the supported Matplotlib minors derived from dependencies. | ||
| """ | ||
| pyproject = pyproject or load_pyproject() | ||
| for dep in pyproject["project"]["dependencies"]: | ||
| if dep.startswith("matplotlib"): | ||
| return _expand_half_open_minor_range(dep) | ||
| raise AssertionError("matplotlib dependency not found in pyproject.toml") | ||
|
|
||
|
|
||
| def supported_python_classifiers(pyproject: dict | None = None) -> list[str]: | ||
| """ | ||
| Extract the explicit Python version classifiers from ``pyproject.toml``. | ||
| """ | ||
| pyproject = pyproject or load_pyproject() | ||
| prefix = "Programming Language :: Python :: " | ||
| versions = [] | ||
| for classifier in pyproject["project"]["classifiers"]: | ||
| if classifier.startswith(prefix): | ||
| tail = classifier.removeprefix(prefix) | ||
| if re.fullmatch(r"\d+\.\d+", tail): | ||
| versions.append(tail) | ||
| return versions | ||
|
|
||
|
|
||
| def build_core_test_matrix( | ||
| python_versions: list[str], matplotlib_versions: list[str] | ||
| ) -> list[dict[str, str]]: | ||
| """ | ||
| Build the representative CI matrix from the supported version bounds. | ||
|
|
||
| We intentionally sample the oldest, midpoint, and newest supported | ||
| Python/Matplotlib combinations instead of exhaustively testing every pair. | ||
| """ | ||
| midpoint_python = python_versions[len(python_versions) // 2] | ||
| midpoint_mpl = matplotlib_versions[len(matplotlib_versions) // 2] | ||
| candidates = [ | ||
| (python_versions[0], matplotlib_versions[0]), | ||
| (midpoint_python, midpoint_mpl), | ||
| (python_versions[-1], matplotlib_versions[-1]), | ||
| ] | ||
| matrix = [] | ||
| seen = set() | ||
| for py_ver, mpl_ver in candidates: | ||
| key = (py_ver, mpl_ver) | ||
| if key in seen: | ||
| continue | ||
| seen.add(key) | ||
| matrix.append({"python-version": py_ver, "matplotlib-version": mpl_ver}) | ||
| return matrix | ||
|
|
||
|
|
||
| def build_version_payload(pyproject: dict | None = None) -> dict: | ||
| """ | ||
| Bundle the version contract into the shape expected by CI and tests. | ||
| """ | ||
| pyproject = pyproject or load_pyproject() | ||
| python_versions = supported_python_versions(pyproject) | ||
| matplotlib_versions = supported_matplotlib_versions(pyproject) | ||
| return { | ||
| "python_versions": python_versions, | ||
| "matplotlib_versions": matplotlib_versions, | ||
| "test_matrix": build_core_test_matrix(python_versions, matplotlib_versions), | ||
| } | ||
|
|
||
|
|
||
| def _emit_github_output(payload: dict) -> str: | ||
| """ | ||
| Format the derived version payload for ``$GITHUB_OUTPUT`` consumption. | ||
| """ | ||
| return "\n".join( | ||
| ( | ||
| f"python-versions={json.dumps(payload['python_versions'], separators=(',', ':'))}", | ||
| f"matplotlib-versions={json.dumps(payload['matplotlib_versions'], separators=(',', ':'))}", | ||
| f"test-matrix={json.dumps(payload['test_matrix'], separators=(',', ':'))}", | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def main() -> int: | ||
| """ | ||
| CLI entry point used by GitHub Actions and local verification. | ||
| """ | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument( | ||
| "--format", | ||
| choices=("json", "github-output"), | ||
| default="json", | ||
| ) | ||
| args = parser.parse_args() | ||
|
|
||
| payload = build_version_payload() | ||
| if args.format == "github-output": | ||
| print(_emit_github_output(payload)) | ||
| else: | ||
| print(json.dumps(payload)) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": # pragma: no cover | ||
| raise SystemExit(main()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import importlib.util | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[2] | ||
| PYPROJECT = ROOT / "pyproject.toml" | ||
| MAIN_WORKFLOW = ROOT / ".github" / "workflows" / "main.yml" | ||
| TEST_MAP_WORKFLOW = ROOT / ".github" / "workflows" / "test-map.yml" | ||
| PUBLISH_WORKFLOW = ROOT / ".github" / "workflows" / "publish-pypi.yml" | ||
| VERSION_SUPPORT = ROOT / "tools" / "ci" / "version_support.py" | ||
|
|
||
|
|
||
| def _load_version_support(): | ||
| """ | ||
| Import the shared version helper directly from the repo checkout. | ||
| """ | ||
| spec = importlib.util.spec_from_file_location("version_support", VERSION_SUPPORT) | ||
| module = importlib.util.module_from_spec(spec) | ||
| assert spec is not None and spec.loader is not None | ||
|
cvanelteren marked this conversation as resolved.
Outdated
|
||
| spec.loader.exec_module(module) | ||
| return module | ||
|
|
||
|
|
||
| def test_python_classifiers_match_requires_python(): | ||
| """ | ||
| Supported Python classifiers should mirror the declared version range. | ||
| """ | ||
| version_support = _load_version_support() | ||
| pyproject = version_support.load_pyproject(PYPROJECT) | ||
| assert version_support.supported_python_classifiers(pyproject) == ( | ||
| version_support.supported_python_versions(pyproject) | ||
| ) | ||
|
|
||
|
|
||
| def test_main_workflow_uses_shared_version_support_script(): | ||
| """ | ||
| The matrix workflow should consume the shared version helper, not reparse inline. | ||
| """ | ||
| text = MAIN_WORKFLOW.read_text(encoding="utf-8") | ||
| assert "python tools/ci/version_support.py --format github-output" in text | ||
|
|
||
|
|
||
| def test_test_map_workflow_pins_oldest_supported_python_and_matplotlib(): | ||
| """ | ||
| The cache-building workflow should exercise the lowest supported core pair. | ||
| """ | ||
| version_support = _load_version_support() | ||
| pyproject = version_support.load_pyproject(PYPROJECT) | ||
| expected_python = version_support.supported_python_versions(pyproject)[0] | ||
| expected_mpl = version_support.supported_matplotlib_versions(pyproject)[0] | ||
| text = TEST_MAP_WORKFLOW.read_text(encoding="utf-8") | ||
| assert f"python={expected_python}" in text | ||
| assert f"matplotlib={expected_mpl}" in text | ||
|
|
||
|
|
||
| def test_publish_workflow_python_is_supported(): | ||
| """ | ||
| Package builds should run on a Python version that UltraPlot declares support for. | ||
| """ | ||
| version_support = _load_version_support() | ||
| pyproject = version_support.load_pyproject(PYPROJECT) | ||
| supported = set(version_support.supported_python_versions(pyproject)) | ||
| text = PUBLISH_WORKFLOW.read_text(encoding="utf-8") | ||
| match = re.search(r'python-version:\s*"(\d+\.\d+)"', text) | ||
| assert match is not None | ||
| assert match.group(1) in supported | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.