Skip to content

Commit fc355c8

Browse files
dynamically add supported Python versions to tox matrix, drawing from package metadata (requires-python) (#326)
* dynamically add supported Python versions to tox matrix, drawing from package metadata (`requires-python`) * Apply suggestion from @Cadair * Apply suggestion from @Cadair Co-authored-by: Stuart Mumford <stuart@cadair.com> * set `persist-credentials: false` in checkout * ignore line in zimzor * replace usage of `peppyproject` with simply reading `pyproject.toml` --------- Co-authored-by: Stuart Mumford <stuart@cadair.com>
1 parent 1714f3e commit fc355c8

5 files changed

Lines changed: 168 additions & 3 deletions

File tree

.github/workflows/test_tox.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ jobs:
6868
posargs: 'PyPy'
6969
pytest: false
7070

71+
test_supported_pythons:
72+
uses: ./.github/workflows/tox.yml
73+
with:
74+
envs: |
75+
- linux: pep8
76+
- linux: py3
77+
fill: true
78+
fill_platforms: linux,macos
79+
pytest: false
80+
7181
test_libraries:
7282
uses: ./.github/workflows/tox.yml
7383
with:

.github/workflows/tox.yml

Lines changed: 33 additions & 2 deletions
Large diffs are not rendered by default.

tools/supported_pythons.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# /// script
2+
# requires-python = ">=3.12"
3+
# dependencies = [
4+
# "click==8.2.1",
5+
# "packaging==25.0",
6+
# "requests==2.32.5",
7+
# "tomli==2.4.0",
8+
# ]
9+
# ///
10+
import os
11+
import warnings
12+
from pathlib import Path
13+
14+
import click
15+
import requests
16+
import tomli
17+
from packaging.specifiers import SpecifierSet
18+
from packaging.version import Version
19+
20+
21+
@click.command()
22+
@click.option("--package-source", default=None)
23+
@click.option("--factors", default=None)
24+
@click.option("--no-eoas", is_flag=True, default=False)
25+
@click.option("--platforms", default=None)
26+
def supported_python_envs_block(
27+
package_source: Path = None,
28+
factors: list[str] = None,
29+
no_eoas: bool = False,
30+
platforms: list[str] = None,
31+
):
32+
"""enumerate toxenvs for each Python version supported by package"""
33+
34+
if platforms is None:
35+
platforms = ["linux"]
36+
elif isinstance(platforms, str):
37+
platforms = platforms.split(",")
38+
39+
toxenvs = supported_python_toxenvs(package_source, factors, no_eoas)
40+
envs_block = "\\n".join(
41+
f"- {platform}: {toxenv}" for platform in platforms for toxenv in toxenvs
42+
)
43+
44+
print(envs_block)
45+
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
46+
f.write(f"envs={envs_block}\n")
47+
48+
49+
def supported_python_toxenvs(
50+
package_source: Path = None,
51+
factors: list[str] = None,
52+
no_eoas: bool = False,
53+
) -> list[str]:
54+
if isinstance(factors, str):
55+
factors = factors.split(",")
56+
57+
return [
58+
f"py{str(python_version).replace('.', '')}{'-' + '-'.join(factors) if factors is not None and len(factors) > 0 else ''}"
59+
for python_version in supported_pythons(package_source, no_eoas=no_eoas)
60+
]
61+
62+
63+
def supported_pythons(
64+
package_source: Path = None,
65+
no_eoas: bool = False,
66+
) -> list[Version]:
67+
current_python_versions = current_pythons(no_eoas=no_eoas)
68+
69+
if not package_source:
70+
supported_versions = current_python_versions
71+
else:
72+
try:
73+
pyproject_toml_filename = Path(package_source) / "pyproject.toml"
74+
if pyproject_toml_filename.exists():
75+
with open(pyproject_toml_filename, "rb") as pyproject_toml_file:
76+
pyproject_toml = tomli.load(pyproject_toml_file)
77+
if "project" in pyproject_toml:
78+
project_metadata = pyproject_toml["table"]
79+
if "requires_python" in project_metadata:
80+
python_version_requirements = SpecifierSet(
81+
project_metadata["requires-python"]
82+
)
83+
else:
84+
raise KeyError(
85+
"`project.requires_python` not found in `pyproject.toml`; ensure your package conforms to PEP621"
86+
)
87+
else:
88+
raise KeyError(
89+
"`project` not found in `pyproject.toml`; ensure your package conforms to PEP621"
90+
)
91+
else:
92+
raise FileNotFoundError(
93+
"could not find `pyproject.toml` in the provided package source; ensure your package conforms to PEP621"
94+
)
95+
96+
supported_versions = [
97+
python_version
98+
for python_version in current_python_versions
99+
if python_version in python_version_requirements
100+
]
101+
except (KeyError, TypeError, FileNotFoundError) as error:
102+
warnings.warn(str(error))
103+
warnings.warn("falling back to current Python versions...")
104+
supported_versions = current_python_versions
105+
106+
return supported_versions
107+
108+
109+
def current_pythons(no_eoas: bool = False) -> list[Version]:
110+
url = "https://endoflife.date/api/v1/products/python"
111+
response = requests.get("https://endoflife.date/api/v1/products/python")
112+
if response.status_code == 200:
113+
return [
114+
Version(python_version["name"])
115+
for python_version in response.json()["result"]["releases"]
116+
if not python_version["isEoas" if no_eoas else "isEol"]
117+
]
118+
else:
119+
raise ValueError(f"request to {url} returned status code {response.status_code}")
120+
121+
122+
if __name__ == "__main__":
123+
supported_python_envs_block()

tools/tox_matrix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def load_tox_targets(
6161
):
6262
"""Script to load tox targets for GitHub Actions workflow."""
6363
# Load envs config
64-
envs = yaml.load(envs, Loader=yaml.BaseLoader)
64+
envs = yaml.load(envs.replace("\\n", "\n"), Loader=yaml.BaseLoader)
6565
print(json.dumps(envs, indent=2))
6666

6767
# Load global libraries config

update_scripts_in_yml.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ def base64_encode_into(script, yml_file, env_var):
3434
base64_encode_into('set_env.py', 'tox.yml', 'SET_ENV_SCRIPT')
3535
base64_encode_into('set_env.py', 'publish.yml', 'SET_ENV_SCRIPT')
3636
base64_encode_into('set_env.py', 'publish_pure_python.yml', 'SET_ENV_SCRIPT')
37+
base64_encode_into('supported_pythons.py', 'tox.yml', 'SUPPORTED_PYTHONS_SCRIPT')

0 commit comments

Comments
 (0)