-
Notifications
You must be signed in to change notification settings - Fork 24
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 1 commit
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
Next
Next commit
Add core Python/Matplotlib version contract checks
Introduce a shared tools/ci/version_support.py helper that derives the supported Python versions, supported Matplotlib versions, and the core CI test matrix directly from pyproject.toml. This removes the duplicated inline parser from the main workflow and gives the project a single source of truth for the version contract that matters most to UltraPlot. Add ultraplot/tests/test_core_versions.py to assert that Python classifiers stay aligned with requires-python, that the matrix workflow uses the shared helper, that the test-map workflow stays pinned to the oldest supported Python/Matplotlib pair, and that the publish workflow builds with a supported Python version. Also expand the PR change filter so workflow, tool, and version-policy changes still trigger the relevant checks.
- Loading branch information
commit 53560f29003c2f61de53d49a198a2c3f77720ebd
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,128 @@ | ||
| #!/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: | ||
| with path.open("rb") as fh: | ||
| return tomllib.load(fh) | ||
|
|
||
|
|
||
| def _expand_half_open_minor_range(spec: str) -> list[str]: | ||
| 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]: | ||
| 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]: | ||
| 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]: | ||
| 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]]: | ||
| 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: | ||
| 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: | ||
| 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: | ||
| 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,54 @@ | ||
| 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(): | ||
| 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(): | ||
| 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(): | ||
| 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(): | ||
| 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(): | ||
| 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.