Skip to content

Commit

Permalink
feat: allow defining a python version list for GHA action (#609)
Browse files Browse the repository at this point in the history
  • Loading branch information
mayeut committed Jun 16, 2022
1 parent 6d5d9e0 commit d1bdf09
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 10 deletions.
74 changes: 74 additions & 0 deletions .github/action_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import sys


def filter_version(version: str) -> str:
"""return python 'major.minor'"""

# remove interpreter prefix
if version.startswith("pypy-"):
version_ = version[5:]
elif version.startswith("pypy"):
version_ = version[4:]
else:
version_ = version

# remove extra specifier e.g. "3.11-dev" => "3.11"
version_ = version_.split("-")[0]

version_parts = version_.split(".")
if len(version_parts) < 2:
raise ValueError(f"invalid version: {version}")
if not version_parts[0].isdigit():
raise ValueError(f"invalid major python version: {version}")
if not version_parts[1].isdigit():
raise ValueError(f"invalid minor python version: {version}")
return ".".join(version_parts[:2])


def setup_action(input_: str) -> None:
versions = [version.strip() for version in input_.split(",") if version.strip()]

pypy_versions = [version for version in versions if version.startswith("pypy")]
pypy_versions_filtered = [filter_version(version) for version in pypy_versions]
if len(pypy_versions) != len(set(pypy_versions_filtered)):
raise ValueError(
"multiple versions specified for the same 'major.minor' PyPy interpreter:"
f" {pypy_versions}"
)

cpython_versions = [version for version in versions if version not in pypy_versions]
cpython_versions_filtered = [
filter_version(version) for version in cpython_versions
]
if len(cpython_versions) != len(set(cpython_versions_filtered)):
raise ValueError(
"multiple versions specified for the same 'major.minor' CPython"
f" interpreter: {cpython_versions}"
)

# cpython shall be installed last because
# other interpreters also define pythonX.Y symlinks.
versions = pypy_versions + cpython_versions

# we want to install python 3.10 last to ease nox set-up
if "3.10" in cpython_versions_filtered:
index = cpython_versions_filtered.index("3.10")
index = versions.index(cpython_versions[index])
cpython_310 = versions.pop(index)
versions.append(cpython_310)
else:
# add this to install nox
versions.append("3.10")

if len(versions) > 20:
raise ValueError(f"too many interpreters to install: {len(versions)} > 20")

print(f"::set-output name=interpreter_count::{len(versions)}")
for i, version in enumerate(versions):
print(f"::set-output name=interpreter_{i}::{version}")


if __name__ == "__main__":
if len(sys.argv) != 2:
raise AssertionError(f"invalid arguments: {sys.argv}")
setup_action(sys.argv[1])
8 changes: 8 additions & 0 deletions .github/workflows/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ jobs:
- uses: actions/checkout@v3
- uses: ./
- run: nox --non-interactive --error-on-missing-interpreter --session github_actions_default_tests
action-all-tests:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: ./
with:
python-versions: "2.7.18, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11-dev, pypy-2.7, pypy-3.7, pypy-3.8, pypy-3.9-v7.3.9"
- run: nox --non-interactive --error-on-missing-interpreter --session github_actions_all_tests
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ repos:
hooks:
- id: isort
args: ["-a", "from __future__ import annotations"]
exclude: ^.github/action_helper.py$

- repo: https://github.com/asottile/pyupgrade
rev: v2.32.0
hooks:
- id: pyupgrade
args: [--py37-plus]
exclude: ^.github/action_helper.py$

- repo: https://github.com/tox-dev/pyproject-fmt
rev: "0.3.3"
Expand Down
86 changes: 77 additions & 9 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,102 @@
name: Setup Nox
description: 'Prepares all python versions for nox'
description: "Prepares all python versions for nox"
inputs:
python-versions:
description: "comma-separated list of python versions to install"
required: false
default: "3.7, 3.8, 3.9, 3.10, pypy-3.7, pypy-3.8, pypy-3.9"
branding:
icon: package
color: blue

runs:
using: composite
steps:
- name: "Validate input"
id: helper
run: ${{ runner.os == 'Windows' && 'python' || 'python3' }} '${{ github.action_path }}/.github/action_helper.py' '${{ inputs.python-versions }}'
shell: bash

- uses: actions/setup-python@v4
with:
python-version: "pypy-3.7"
python-version: "${{ steps.helper.outputs.interpreter_0 }}"
if: ${{ steps.helper.outputs.interpreter_count > 0 }}
- uses: actions/setup-python@v4
with:
python-version: "pypy-3.8"
python-version: "${{ steps.helper.outputs.interpreter_1 }}"
if: ${{ steps.helper.outputs.interpreter_count > 1 }}
- uses: actions/setup-python@v4
with:
python-version: "pypy-3.9"

python-version: "${{ steps.helper.outputs.interpreter_2 }}"
if: ${{ steps.helper.outputs.interpreter_count > 2 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_3 }}"
if: ${{ steps.helper.outputs.interpreter_count > 3 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_4 }}"
if: ${{ steps.helper.outputs.interpreter_count > 4 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_5 }}"
if: ${{ steps.helper.outputs.interpreter_count > 5 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_6 }}"
if: ${{ steps.helper.outputs.interpreter_count > 6 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_7 }}"
if: ${{ steps.helper.outputs.interpreter_count > 7 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_8 }}"
if: ${{ steps.helper.outputs.interpreter_count > 8 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_9 }}"
if: ${{ steps.helper.outputs.interpreter_count > 9 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_10 }}"
if: ${{ steps.helper.outputs.interpreter_count > 10 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_11 }}"
if: ${{ steps.helper.outputs.interpreter_count > 11 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_12 }}"
if: ${{ steps.helper.outputs.interpreter_count > 12 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_13 }}"
if: ${{ steps.helper.outputs.interpreter_count > 13 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_14 }}"
if: ${{ steps.helper.outputs.interpreter_count > 14 }}
- uses: actions/setup-python@v4
with:
python-version: "${{ steps.helper.outputs.interpreter_15 }}"
if: ${{ steps.helper.outputs.interpreter_count > 15 }}
- uses: actions/setup-python@v4
with:
python-version: "3.7"
python-version: "${{ steps.helper.outputs.interpreter_16 }}"
if: ${{ steps.helper.outputs.interpreter_count > 16 }}
- uses: actions/setup-python@v4
with:
python-version: "3.8"
python-version: "${{ steps.helper.outputs.interpreter_17 }}"
if: ${{ steps.helper.outputs.interpreter_count > 17 }}
- uses: actions/setup-python@v4
with:
python-version: "3.9"
python-version: "${{ steps.helper.outputs.interpreter_18 }}"
if: ${{ steps.helper.outputs.interpreter_count > 18 }}
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "${{ steps.helper.outputs.interpreter_19 }}"
if: ${{ steps.helper.outputs.interpreter_count > 19 }}

- name: "Install nox"
# --python "$(which python)" => always use the last setup-python version to install nox.
Expand Down
18 changes: 17 additions & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,23 @@ Either way, Nox is usually installed *globally*, similar to ``tox``, ``pip``, an

If you're interested in running ``nox`` within `docker`_, you can use the `thekevjames/nox images`_ on DockerHub which contain builds for all ``nox`` versions and all supported ``python`` versions. Nox is also supported via ``pipx run nox`` in the `manylinux images`_.

If you want to run ``nox`` within `GitHub Actions`_, you can use the ``wntrblm/nox`` action, which installs the latest ``nox`` and makes available all active CPython and PyPY versions provided by the GitHub Actions environment. You can safely combine this with with ``setup-python`` for past end-of-life or development versions of Python, as well.
If you want to run ``nox`` within `GitHub Actions`_, you can use the ``wntrblm/nox`` action, which installs the latest ``nox`` and makes available all active CPython and PyPY versions provided by the GitHub Actions environment:

.. code-block:: yaml
# setup nox with all active CPython and PyPY versions provided by
# the GitHub Actions environment i.e.
# python-versions: "3.7, 3.8, 3.9, 3.10, pypy-3.7, pypy-3.8, pypy-3.9"
- uses: wntrblm/nox
# setup nox only for a given list of python versions
# Limitations:
# - Version specifiers shall be supported by actions/setup-python
# - You can specify up-to 20 versions
# - There can only be one "major.minor" per interpreter i.e. "3.7.0, 3.7.1" is invalid
- uses: wntrblm/nox
with:
python-versions: "2.7, 3.5, 3.11-dev, pypy-3.9"
.. _pip: https://pip.readthedocs.org
.. _user site: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site
Expand Down
25 changes: 25 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import os
import platform
import shutil
import sys

import nox

Expand Down Expand Up @@ -132,4 +133,28 @@ def _check_python_version(session: nox.Session) -> None:
@nox.session(python=["3.7", "3.8", "3.9", "3.10", "pypy3.7", "pypy3.8", "pypy3.9"])
def github_actions_default_tests(session: nox.Session) -> None:
"""Check default versions installed by the nox GHA Action"""
assert sys.version_info[:2] == (3, 10)
_check_python_version(session)


# The following sessions are only to be run in CI to check the nox GHA action
@nox.session(
python=[
"2.7",
"3.4",
"3.5",
"3.6",
"3.7",
"3.8",
"3.9",
"3.10",
"3.11",
"pypy2.7",
"pypy3.7",
"pypy3.8",
"pypy3.9",
]
)
def github_actions_all_tests(session: nox.Session) -> None:
"""Check all versions installed by the nox GHA Action"""
_check_python_version(session)
117 changes: 117 additions & 0 deletions tests/test_action_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import annotations

import sys
from pathlib import Path

import pytest

GITHUB_FOLDER = Path(__file__).resolve().parent.parent / ".github"
sys.path.insert(0, str(GITHUB_FOLDER))
from action_helper import filter_version, setup_action # noqa: E402

VALID_VERSIONS = {
"2.7.18": "2.7",
"3.9-dev": "3.9",
"3.10": "3.10",
"3.11.0.beta1": "3.11",
"pypy-3.7": "3.7",
"pypy-3.8-v7.3.9": "3.8",
"pypy-3.9": "3.9",
"pypy3.10": "3.10",
}


@pytest.mark.parametrize("version", VALID_VERSIONS.keys())
def test_filter_version(version):
assert filter_version(version) == VALID_VERSIONS[version]


def test_filter_version_invalid():
with pytest.raises(ValueError, match=r"invalid version: 3"):
filter_version("3")


def test_filter_version_invalid_major():
with pytest.raises(ValueError, match=r"invalid major python version: x.0"):
filter_version("x.0")


def test_filter_version_invalid_minor():
with pytest.raises(ValueError, match=r"invalid minor python version: 3.x"):
filter_version("3.x")


VALID_VERSION_LISTS = {
"3.7, 3.8, 3.9, 3.10, pypy-3.7, pypy-3.8, pypy-3.9": [
"::set-output name=interpreter_count::7",
"::set-output name=interpreter_0::pypy-3.7",
"::set-output name=interpreter_1::pypy-3.8",
"::set-output name=interpreter_2::pypy-3.9",
"::set-output name=interpreter_3::3.7",
"::set-output name=interpreter_4::3.8",
"::set-output name=interpreter_5::3.9",
"::set-output name=interpreter_6::3.10",
],
"": [
"::set-output name=interpreter_count::1",
"::set-output name=interpreter_0::3.10",
],
"3.10.4": [
"::set-output name=interpreter_count::1",
"::set-output name=interpreter_0::3.10.4",
],
"3.9-dev,pypy3.9-nightly": [
"::set-output name=interpreter_count::3",
"::set-output name=interpreter_0::pypy3.9-nightly",
"::set-output name=interpreter_1::3.9-dev",
"::set-output name=interpreter_2::3.10",
],
"3.10, 3.9, 3.8": [
"::set-output name=interpreter_count::3",
"::set-output name=interpreter_0::3.9",
"::set-output name=interpreter_1::3.8",
"::set-output name=interpreter_2::3.10",
],
",".join(f"3.{minor}" for minor in range(20)): [
"::set-output name=interpreter_count::20"
]
+ [
f"::set-output name=interpreter_{i}::3.{minor}"
for i, minor in enumerate(minor_ for minor_ in range(20) if minor_ != 10)
]
+ ["::set-output name=interpreter_19::3.10"],
}


@pytest.mark.parametrize("version_list", VALID_VERSION_LISTS.keys())
def test_setup_action(capsys, version_list):
setup_action(version_list)
captured = capsys.readouterr()
lines = captured.out.splitlines()
assert lines == VALID_VERSION_LISTS[version_list]


def test_setup_action_multiple_pypy():
with pytest.raises(
ValueError,
match=(
r"multiple versions specified for the same 'major.minor' PyPy interpreter"
),
):
setup_action("pypy3.9, pypy-3.9-v7.3.9")


def test_setup_action_multiple_cpython():
with pytest.raises(
ValueError,
match=(
r"multiple versions specified for the same 'major.minor' CPython"
r" interpreter"
),
):
setup_action("3.10, 3.10.4")


def test_setup_action_too_many_interpreters():
with pytest.raises(ValueError, match=r"too many interpreters to install: 21 > 20"):
setup_action(",".join(f"3.{minor}" for minor in range(21)))

0 comments on commit d1bdf09

Please sign in to comment.