Skip to content

Commit 5cef39b

Browse files
feat(setup_python): Add support for multiple versions and version file input (#71)
1 parent 0c0ec2c commit 5cef39b

File tree

25 files changed

+793
-25
lines changed

25 files changed

+793
-25
lines changed

.github/workflows/ci-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
tests:
2525
runs-on: ${{ matrix.runner }}
2626
env:
27-
INPUT_PYTHON_VERSION: '3.12'
27+
DEFAULT_PYTHON_VERSION: '3.12'
2828
strategy:
2929
fail-fast: false
3030
matrix:
@@ -39,7 +39,7 @@ jobs:
3939
- name: Set up Python
4040
uses: actions/setup-python@v6
4141
with:
42-
python-version: ${{ env.INPUT_PYTHON_VERSION }}
42+
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
4343

4444
- name: Set up Node
4545
uses: actions/setup-node@v6

actions/setup_python/README.md

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ including older versions like Python 2.7 that are no longer available in the sta
99

1010
See [action.yml](action.yml)
1111

12-
**Python**
12+
**Single Python Version**
1313
```yaml
1414
steps:
1515
- uses: LizardByte/actions/actions/setup_python@master
@@ -18,6 +18,27 @@ steps:
1818
- run: python my_script.py
1919
```
2020
21+
**Multiple Python Versions**
22+
```yaml
23+
steps:
24+
- uses: LizardByte/actions/actions/setup_python@master
25+
with:
26+
python-version: |
27+
3.11
28+
3.12
29+
3.13
30+
- run: python my_script.py
31+
```
32+
33+
**Python Version from File**
34+
```yaml
35+
steps:
36+
- uses: LizardByte/actions/actions/setup_python@master
37+
with:
38+
python-version-file: '.python-version'
39+
- run: python my_script.py
40+
```
41+
2142
**Python 2.7**
2243
```yaml
2344
steps:
@@ -38,16 +59,68 @@ steps:
3859
3960
## 📥 Inputs
4061
41-
| Name | Description | Default | Required |
42-
|----------------|----------------------------------|---------|----------|
43-
| python-version | The version of Python to set up. | | `true` |
62+
| Name | Description | Default | Required |
63+
|---------------------|--------------------------------------------------------------------------------------------------------------------------|---------|----------|
64+
| python-version | The version(s) of Python to set up. Can be a single version or multiple versions separated by newlines or spaces. | | `false` |
65+
| python-version-file | File containing the Python version to set up. Supports `.python-version`, `pyproject.toml`, `.tool-versions`, `Pipfile`. | | `false` |
66+
67+
> [!NOTE]
68+
> Either `python-version` or `python-version-file` must be specified.
4469

4570
## 📤 Outputs
4671

47-
This action does not produce outputs.
72+
| Name | Description |
73+
|----------------|----------------------------------------|
74+
| python-version | The version of Python that was set up. |
75+
| python-path | The path to the Python executable. |
4876

4977
## 📝 Notes
5078

5179
> [!NOTE]
5280
> The python version must be an available option from pyenv. The versions available depend on the operating system and
5381
> architecture. The available versions will be listed in the output of the action.
82+
83+
> [!TIP]
84+
> When installing multiple Python versions:
85+
> - All specified versions will be installed via pyenv
86+
> - Only the last version in the list will be set as the global/default Python version
87+
> - Other installed versions can still be accessed using `pyenv shell <version>` or `pyenv local <version>`
88+
89+
## 📂 Supported File Formats
90+
91+
When using `python-version-file`, the following file formats are supported:
92+
93+
### `.python-version`
94+
```
95+
3.12.0
96+
```
97+
98+
Or for multiple versions:
99+
```
100+
3.11.0
101+
3.12.0
102+
3.13.0
103+
```
104+
105+
### `pyproject.toml`
106+
```toml
107+
[project]
108+
requires-python = ">=3.8"
109+
```
110+
111+
Or:
112+
```toml
113+
[tool.poetry.dependencies]
114+
python = "^3.8"
115+
```
116+
117+
### `.tool-versions`
118+
```
119+
python 3.12.0
120+
```
121+
122+
### `Pipfile`
123+
```toml
124+
[requires]
125+
python_version = "3.12"
126+
```

actions/setup_python/action.yml

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,23 @@ branding:
99

1010
inputs:
1111
python-version:
12-
description: "The version of Python to set up."
13-
required: true
12+
description: >
13+
The version(s) of Python to set up.
14+
Can be a single version or multiple versions separated by newlines or spaces.
15+
required: false
16+
python-version-file:
17+
description: >
18+
File containing the Python version to set up.
19+
Supports .python-version, pyproject.toml, .tool-versions, and Pipfile.
20+
required: false
21+
22+
outputs:
23+
python-version:
24+
description: "The version of Python that was set up."
25+
value: ${{ steps.outputs.outputs.python-version }}
26+
python-path:
27+
description: "The path to the Python executable."
28+
value: ${{ steps.outputs.outputs.python-path }}
1429

1530
runs:
1631
using: "composite"
@@ -49,21 +64,51 @@ runs:
4964
shell: bash
5065
run: pyenv update
5166

67+
- name: Determine Python Version(s)
68+
id: determine-version
69+
shell: bash
70+
env:
71+
INPUT_PYTHON_VERSION: ${{ inputs.python-version }}
72+
INPUT_PYTHON_VERSION_FILE: ${{ inputs.python-version-file }}
73+
run: |
74+
# Source the version determination script
75+
source "${{ github.action_path }}/determine_version.sh"
76+
77+
# Determine versions using the shared function
78+
determine_python_version "${INPUT_PYTHON_VERSION}" "${INPUT_PYTHON_VERSION_FILE}"
79+
80+
echo "Python versions to install: ${PYTHON_VERSIONS}"
81+
echo "Default Python version: ${DEFAULT_PYTHON_VERSION}"
82+
echo "versions=${PYTHON_VERSIONS}" >> $GITHUB_OUTPUT
83+
echo "default-version=${DEFAULT_PYTHON_VERSION}" >> $GITHUB_OUTPUT
84+
5285
- name: Available Python versions
5386
shell: bash
5487
run: pyenv install --list
5588

5689
- name: Install Python
5790
env:
58-
INPUT_PYTHON_VERSION: ${{ inputs.python-version }}
91+
PYTHON_VERSIONS: ${{ steps.determine-version.outputs.versions }}
92+
DEFAULT_VERSION: ${{ steps.determine-version.outputs.default-version }}
5993
shell: bash
6094
run: |
61-
pyenv install ${INPUT_PYTHON_VERSION}
62-
pyenv global ${INPUT_PYTHON_VERSION}
95+
# Convert space-separated versions to array
96+
IFS=' ' read -ra version_array <<< "${PYTHON_VERSIONS}"
97+
98+
# Install each version
99+
for version in "${version_array[@]}"; do
100+
echo "Installing Python ${version}..."
101+
pyenv install ${version}
102+
done
103+
104+
# Set only the default version as global
105+
pyenv global ${DEFAULT_VERSION}
106+
107+
echo "Default Python version set to: ${DEFAULT_VERSION}"
63108
64109
- name: Setup Python Environment
65110
env:
66-
INPUT_PYTHON_VERSION: ${{ inputs.python-version }}
111+
DEFAULT_VERSION: ${{ steps.determine-version.outputs.default-version }}
67112
shell: bash
68113
run: |
69114
echo "Current system Python Version:"
@@ -97,7 +142,7 @@ runs:
97142
fi
98143
99144
# Extract major.minor version (e.g., "2.7.18" -> "2.7", "2.7-win32" -> "2.7", "3.10.5" -> "3.10")
100-
python_version_two_digit=$(echo "${INPUT_PYTHON_VERSION}" | sed -E 's/^([0-9]+\.[0-9]+).*$/\1/')
145+
python_version_two_digit=$(echo "${DEFAULT_VERSION}" | sed -E 's/^([0-9]+\.[0-9]+).*$/\1/')
101146
102147
# Convert version to comparable format (e.g., "2.7" -> 27, "3.10" -> 310)
103148
version_major=$(echo "${python_version_two_digit}" | cut -d. -f1)
@@ -139,3 +184,19 @@ runs:
139184
# show python version
140185
echo "Python venv version:"
141186
python --version
187+
188+
- name: Set outputs
189+
id: outputs
190+
shell: bash
191+
run: |
192+
python_path=$(which python)
193+
194+
# Convert to Windows path format on Windows runners
195+
if [[ "${{ runner.os }}" == "Windows" ]]; then
196+
python_path=$(cygpath -w "${python_path}")
197+
fi
198+
199+
{
200+
echo "python-version=$(pyenv version-name)"
201+
echo "python-path=${python_path}"
202+
} >> "${GITHUB_OUTPUT}"

actions/setup_python/ci-matrix.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,29 @@
130130
"with": {
131131
"python-version": "3.12.11"
132132
}
133+
},
134+
{
135+
"runs-on": "ubuntu-latest",
136+
"with": {
137+
"python-version-file": "tests/setup_python/version_files/python-version/single-version/.python-version"
138+
}
139+
},
140+
{
141+
"runs-on": "ubuntu-latest",
142+
"with": {
143+
"python-version-file": "tests/setup_python/version_files/pyproject-toml/requires-python-gte/pyproject.toml"
144+
}
145+
},
146+
{
147+
"runs-on": "ubuntu-latest",
148+
"with": {
149+
"python-version-file": "tests/setup_python/version_files/tool-versions/single-version/.tool-versions"
150+
}
151+
},
152+
{
153+
"runs-on": "ubuntu-latest",
154+
"with": {
155+
"python-version-file": "tests/setup_python/version_files/pipfile/simple/Pipfile"
156+
}
133157
}
134158
]
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/bin/bash
2+
# Reusable script to determine Python version(s) from inputs
3+
# This script can be sourced by both action.yml and post-ci.sh
4+
5+
# Function to read version from file
6+
read_version_from_file() {
7+
local file="$1"
8+
9+
if [[ ! -f "${file}" ]]; then
10+
echo "Error: File '${file}' not found" >&2
11+
return 1
12+
fi
13+
14+
# Get the basename of the file for matching
15+
local filename
16+
filename=$(basename "${file}")
17+
18+
case "${filename}" in
19+
*python-version)
20+
# .python-version format: one version per line or space-separated
21+
tr '\n' ' ' < "${file}"
22+
;;
23+
pyproject.toml)
24+
# pyproject.toml format: requires-python = ">=3.8" or python_version = "3.8"
25+
# We'll extract the version from requires-python or python_version
26+
if grep -q "requires-python" "${file}"; then
27+
# Extract version like ">=3.8", "^3.8", "~=3.8.0", "==3.8.1"
28+
version=$(grep "requires-python" "${file}" | \
29+
sed -E 's/.*requires-python.*=.*["><=^~]*([0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/')
30+
echo "${version}"
31+
elif grep -q "python_version" "${file}"; then
32+
version=$(grep "python_version" "${file}" | \
33+
sed -E 's/.*python_version.*=.*"([0-9]+\.[0-9]+(\.[0-9]+)?)".*/\1/')
34+
echo "${version}"
35+
else
36+
echo "Error: Could not find requires-python or python_version in pyproject.toml" >&2
37+
return 1
38+
fi
39+
;;
40+
.tool-versions)
41+
# .tool-versions format: python 3.8.10
42+
grep "^python " "${file}" | awk '{print $2}'
43+
;;
44+
Pipfile)
45+
# Pipfile format: python_version = "3.8"
46+
grep "python_version" "${file}" | \
47+
sed -E 's/.*python_version.*=.*"([0-9]+\.[0-9]+(\.[0-9]+)?)".*/\1/'
48+
;;
49+
*)
50+
echo "Error: Unsupported file type '${file}'" >&2
51+
return 1
52+
;;
53+
esac
54+
}
55+
56+
# Function to determine Python versions and default version
57+
# Sets two variables: PYTHON_VERSIONS and DEFAULT_PYTHON_VERSION
58+
determine_python_version() {
59+
local python_version="$1"
60+
local python_version_file="$2"
61+
local versions=""
62+
63+
# Determine which input to use
64+
if [[ -n "${python_version}" ]]; then
65+
# Use python-version input
66+
# Support both newline and space separated versions
67+
versions="${python_version}"
68+
elif [[ -n "${python_version_file}" ]]; then
69+
# Read from file
70+
if ! versions=$(read_version_from_file "${python_version_file}"); then
71+
return 1
72+
fi
73+
else
74+
echo "Error: Either python-version or python-version-file must be specified" >&2
75+
return 1
76+
fi
77+
78+
# Normalize versions: convert newlines to spaces, collapse multiple spaces
79+
versions=$(echo "${versions}" | tr '\n' ' ' | tr -s ' ' | xargs)
80+
81+
# Convert to array and get the last version as default
82+
IFS=' ' read -ra version_array <<< "${versions}"
83+
# Use bash 3.x compatible way to get last element
84+
local array_length=${#version_array[@]}
85+
local default_version="${version_array[$((array_length - 1))]}"
86+
87+
# Export the results
88+
PYTHON_VERSIONS="${versions}"
89+
DEFAULT_PYTHON_VERSION="${default_version}"
90+
export PYTHON_VERSIONS DEFAULT_PYTHON_VERSION
91+
92+
return 0
93+
}

actions/setup_python/post-ci.sh

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
#!/bin/bash
22

3-
# Extract python-version from WITH_PARAMS environment variable
3+
# Get the directory where this script is located
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
6+
# Source the version determination script
7+
# shellcheck source=actions/setup_python/determine_version.sh
8+
source "${SCRIPT_DIR}/determine_version.sh"
9+
10+
# Extract python-version and python-version-file from WITH_PARAMS environment variable
411
if [[ -n "$WITH_PARAMS" ]]; then
5-
PYTHON_VERSION=$(echo "$WITH_PARAMS" | jq -r '.["python-version"]')
6-
echo "Extracted Python Version: ${PYTHON_VERSION}"
7-
export INPUT_PYTHON_VERSION="${PYTHON_VERSION}"
12+
PYTHON_VERSION=$(echo "$WITH_PARAMS" | jq -r '.["python-version"] // empty')
13+
PYTHON_VERSION_FILE=$(echo "$WITH_PARAMS" | jq -r '.["python-version-file"] // empty')
14+
15+
echo "Input python-version: ${PYTHON_VERSION:-<not set>}"
16+
echo "Input python-version-file: ${PYTHON_VERSION_FILE:-<not set>}"
17+
18+
# Determine the Python version(s) using the shared function
19+
if ! determine_python_version "${PYTHON_VERSION}" "${PYTHON_VERSION_FILE}"; then
20+
echo "Error: Failed to determine Python version" >&2
21+
exit 1
22+
fi
23+
24+
echo "All Python versions: ${PYTHON_VERSIONS:-<not set>}"
25+
echo "Default Python version: ${DEFAULT_PYTHON_VERSION:-<not set>}"
826

927
# setup python
1028
python -m pip install --upgrade pip setuptools wheel

0 commit comments

Comments
 (0)