Skip to content

Commit 4ed259e

Browse files
Merge branch 'actions:main' into main
2 parents cd1de83 + 19e4675 commit 4ed259e

File tree

14 files changed

+513
-93
lines changed

14 files changed

+513
-93
lines changed

.github/workflows/test-python.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,39 @@ jobs:
245245
- name: Run simple code
246246
run: python -c 'import math; print(math.factorial(5))'
247247

248+
setup-versions-from-tool-versions-file:
249+
name: Setup ${{ matrix.python }} ${{ matrix.os }} .tool-versions file
250+
runs-on: ${{ matrix.os }}
251+
strategy:
252+
fail-fast: false
253+
matrix:
254+
os:
255+
[
256+
macos-latest,
257+
windows-latest,
258+
ubuntu-20.04,
259+
ubuntu-22.04,
260+
macos-13,
261+
ubuntu-latest
262+
]
263+
python: [3.13.0, 3.14-dev, pypy3.11-7.3.18, graalpy-24.1.2]
264+
exclude:
265+
- os: windows-latest
266+
python: graalpy-24.1.2
267+
steps:
268+
- name: Checkout
269+
uses: actions/checkout@v4
270+
271+
- name: build-tool-versions-file ${{ matrix.python }}
272+
run: |
273+
echo "python ${{ matrix.python }}" > .tool-versions
274+
275+
- name: setup-python using .tool-versions ${{ matrix.python }}
276+
id: setup-python-tool-versions
277+
uses: ./
278+
with:
279+
python-version-file: .tool-versions
280+
248281
setup-pre-release-version-from-manifest:
249282
name: Setup 3.14.0-alpha.1 ${{ matrix.os }}
250283
runs-on: ${{ matrix.os }}
Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ steps:
4545
- run: python my_script.py
4646
```
4747
48+
**Free threaded Python**
49+
```yaml
50+
steps:
51+
- uses: actions/checkout@v4
52+
- uses: actions/setup-python@v5
53+
with:
54+
python-version: '3.13t'
55+
- run: python my_script.py
56+
```
57+
4858
The `python-version` input is optional. If not supplied, the action will try to resolve the version from the default `.python-version` file. If the `.python-version` file doesn't exist Python or PyPy version from the PATH will be used. The default version of Python or PyPy in PATH varies between runners and can be changed unexpectedly so we recommend always setting Python version explicitly using the `python-version` or `python-version-file` inputs.
4959

5060
The action will first check the local [tool cache](docs/advanced-usage.md#hosted-tool-cache) for a [semver](https://github.com/npm/node-semver#versions) match. If unable to find a specific version in the tool cache, the action will attempt to download a version of Python from [GitHub Releases](https://github.com/actions/python-versions/releases) and for PyPy from the official [PyPy's dist](https://downloads.python.org/pypy/).

__tests__/find-python.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {desugarVersion, pythonVersionToSemantic} from '../src/find-python';
2+
3+
describe('desugarVersion', () => {
4+
it.each([
5+
['3.13', {version: '3.13', freethreaded: false}],
6+
['3.13t', {version: '3.13', freethreaded: true}],
7+
['3.13.1', {version: '3.13.1', freethreaded: false}],
8+
['3.13.1t', {version: '3.13.1', freethreaded: true}],
9+
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
10+
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}]
11+
])('%s -> %s', (input, expected) => {
12+
expect(desugarVersion(input)).toEqual(expected);
13+
});
14+
});
15+
16+
// Test the combined desugarVersion and pythonVersionToSemantic functions
17+
describe('pythonVersions', () => {
18+
it.each([
19+
['3.13', {version: '3.13', freethreaded: false}],
20+
['3.13t', {version: '3.13', freethreaded: true}],
21+
['3.13.1', {version: '3.13.1', freethreaded: false}],
22+
['3.13.1t', {version: '3.13.1', freethreaded: true}],
23+
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
24+
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}]
25+
])('%s -> %s', (input, expected) => {
26+
const {version, freethreaded} = desugarVersion(input);
27+
const semanticVersionSpec = pythonVersionToSemantic(version, false);
28+
expect({version: semanticVersionSpec, freethreaded}).toEqual(expected);
29+
});
30+
31+
it.each([
32+
['3.13', {version: '~3.13.0-0', freethreaded: false}],
33+
['3.13t', {version: '~3.13.0-0', freethreaded: true}],
34+
['3.13.1', {version: '3.13.1', freethreaded: false}],
35+
['3.13.1t', {version: '3.13.1', freethreaded: true}],
36+
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
37+
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}]
38+
])('%s (allowPreReleases=true) -> %s', (input, expected) => {
39+
const {version, freethreaded} = desugarVersion(input);
40+
const semanticVersionSpec = pythonVersionToSemantic(version, true);
41+
expect({version: semanticVersionSpec, freethreaded}).toEqual(expected);
42+
});
43+
});

__tests__/finder.test.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('Finder tests', () => {
5656
await io.mkdirP(pythonDir);
5757
fs.writeFileSync(`${pythonDir}.complete`, 'hello');
5858
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
59-
await finder.useCpythonVersion('3.x', 'x64', true, false, false);
59+
await finder.useCpythonVersion('3.x', 'x64', true, false, false, false);
6060
expect(spyCoreAddPath).toHaveBeenCalled();
6161
expect(spyCoreExportVariable).toHaveBeenCalledWith(
6262
'pythonLocation',
@@ -73,7 +73,7 @@ describe('Finder tests', () => {
7373
await io.mkdirP(pythonDir);
7474
fs.writeFileSync(`${pythonDir}.complete`, 'hello');
7575
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
76-
await finder.useCpythonVersion('3.x', 'x64', false, false, false);
76+
await finder.useCpythonVersion('3.x', 'x64', false, false, false, false);
7777
expect(spyCoreAddPath).not.toHaveBeenCalled();
7878
expect(spyCoreExportVariable).not.toHaveBeenCalled();
7979
});
@@ -96,7 +96,7 @@ describe('Finder tests', () => {
9696
});
9797
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
9898
await expect(
99-
finder.useCpythonVersion('1.2.3', 'x64', true, false, false)
99+
finder.useCpythonVersion('1.2.3', 'x64', true, false, false, false)
100100
).resolves.toEqual({
101101
impl: 'CPython',
102102
version: '1.2.3'
@@ -135,7 +135,14 @@ describe('Finder tests', () => {
135135
});
136136
// This will throw if it doesn't find it in the manifest (because no such version exists)
137137
await expect(
138-
finder.useCpythonVersion('1.2.4-beta.2', 'x64', false, false, false)
138+
finder.useCpythonVersion(
139+
'1.2.4-beta.2',
140+
'x64',
141+
false,
142+
false,
143+
false,
144+
false
145+
)
139146
).resolves.toEqual({
140147
impl: 'CPython',
141148
version: '1.2.4-beta.2'
@@ -186,7 +193,7 @@ describe('Finder tests', () => {
186193

187194
fs.writeFileSync(`${pythonDir}.complete`, 'hello');
188195
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
189-
await finder.useCpythonVersion('1.2', 'x64', true, true, false);
196+
await finder.useCpythonVersion('1.2', 'x64', true, true, false, false);
190197

191198
expect(infoSpy).toHaveBeenCalledWith("Resolved as '1.2.3'");
192199
expect(infoSpy).toHaveBeenCalledWith(
@@ -197,7 +204,14 @@ describe('Finder tests', () => {
197204
);
198205
expect(installSpy).toHaveBeenCalled();
199206
expect(addPathSpy).toHaveBeenCalledWith(expPath);
200-
await finder.useCpythonVersion('1.2.4-beta.2', 'x64', false, true, false);
207+
await finder.useCpythonVersion(
208+
'1.2.4-beta.2',
209+
'x64',
210+
false,
211+
true,
212+
false,
213+
false
214+
);
201215
expect(spyCoreAddPath).toHaveBeenCalled();
202216
expect(spyCoreExportVariable).toHaveBeenCalledWith(
203217
'pythonLocation',
@@ -224,7 +238,7 @@ describe('Finder tests', () => {
224238
});
225239
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
226240
await expect(
227-
finder.useCpythonVersion('1.2', 'x64', false, false, false)
241+
finder.useCpythonVersion('1.2', 'x64', false, false, false, false)
228242
).resolves.toEqual({
229243
impl: 'CPython',
230244
version: '1.2.3'
@@ -251,25 +265,32 @@ describe('Finder tests', () => {
251265
});
252266
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
253267
await expect(
254-
finder.useCpythonVersion('1.1', 'x64', false, false, false)
268+
finder.useCpythonVersion('1.1', 'x64', false, false, false, false)
255269
).rejects.toThrow();
256270
await expect(
257-
finder.useCpythonVersion('1.1', 'x64', false, false, true)
271+
finder.useCpythonVersion('1.1', 'x64', false, false, true, false)
258272
).resolves.toEqual({
259273
impl: 'CPython',
260274
version: '1.1.0-beta.2'
261275
});
262276
// Check 1.1.0 version specifier does not fallback to '1.1.0-beta.2'
263277
await expect(
264-
finder.useCpythonVersion('1.1.0', 'x64', false, false, true)
278+
finder.useCpythonVersion('1.1.0', 'x64', false, false, true, false)
265279
).rejects.toThrow();
266280
});
267281

268282
it('Errors if Python is not installed', async () => {
269283
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
270284
let thrown = false;
271285
try {
272-
await finder.useCpythonVersion('3.300000', 'x64', true, false, false);
286+
await finder.useCpythonVersion(
287+
'3.300000',
288+
'x64',
289+
true,
290+
false,
291+
false,
292+
false
293+
);
273294
} catch {
274295
thrown = true;
275296
}

__tests__/utils.test.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
getNextPageUrl,
1616
isGhes,
1717
IS_WINDOWS,
18-
getDownloadFileName
18+
getDownloadFileName,
19+
getVersionInputFromToolVersions
1920
} from '../src/utils';
2021

2122
jest.mock('@actions/cache');
@@ -139,6 +140,82 @@ describe('Version from file test', () => {
139140
expect(_fn(pythonVersionFilePath)).toEqual([]);
140141
}
141142
);
143+
it.each([getVersionInputFromToolVersions])(
144+
'Version from .tool-versions',
145+
async _fn => {
146+
const toolVersionFileName = '.tool-versions';
147+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
148+
const toolVersionContent = 'python 3.9.10\nnodejs 16';
149+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
150+
expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']);
151+
}
152+
);
153+
154+
it.each([getVersionInputFromToolVersions])(
155+
'Version from .tool-versions with comment',
156+
async _fn => {
157+
const toolVersionFileName = '.tool-versions';
158+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
159+
const toolVersionContent = '# python 3.8\npython 3.9';
160+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
161+
expect(_fn(toolVersionFilePath)).toEqual(['3.9']);
162+
}
163+
);
164+
165+
it.each([getVersionInputFromToolVersions])(
166+
'Version from .tool-versions with whitespace',
167+
async _fn => {
168+
const toolVersionFileName = '.tool-versions';
169+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
170+
const toolVersionContent = ' python 3.10 ';
171+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
172+
expect(_fn(toolVersionFilePath)).toEqual(['3.10']);
173+
}
174+
);
175+
176+
it.each([getVersionInputFromToolVersions])(
177+
'Version from .tool-versions with v prefix',
178+
async _fn => {
179+
const toolVersionFileName = '.tool-versions';
180+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
181+
const toolVersionContent = 'python v3.9.10';
182+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
183+
expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']);
184+
}
185+
);
186+
187+
it.each([getVersionInputFromToolVersions])(
188+
'Version from .tool-versions with pypy version',
189+
async _fn => {
190+
const toolVersionFileName = '.tool-versions';
191+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
192+
const toolVersionContent = 'python pypy3.10-7.3.14';
193+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
194+
expect(_fn(toolVersionFilePath)).toEqual(['pypy3.10-7.3.14']);
195+
}
196+
);
197+
198+
it.each([getVersionInputFromToolVersions])(
199+
'Version from .tool-versions with alpha Releases',
200+
async _fn => {
201+
const toolVersionFileName = '.tool-versions';
202+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
203+
const toolVersionContent = 'python 3.14.0a5t';
204+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
205+
expect(_fn(toolVersionFilePath)).toEqual(['3.14.0a5t']);
206+
}
207+
);
208+
209+
it.each([getVersionInputFromToolVersions])(
210+
'Version from .tool-versions with dev suffix',
211+
async _fn => {
212+
const toolVersionFileName = '.tool-versions';
213+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
214+
const toolVersionContent = 'python 3.14t-dev';
215+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
216+
expect(_fn(toolVersionFilePath)).toEqual(['3.14t-dev']);
217+
}
218+
);
142219
});
143220

144221
describe('getNextPageUrl', () => {

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ inputs:
2626
allow-prereleases:
2727
description: "When 'true', a version range passed to 'python-version' input will match prerelease versions if no GA versions are found. Only 'x.y' version range is supported for CPython."
2828
default: false
29+
freethreaded:
30+
description: "When 'true', use the freethreaded version of Python."
31+
default: false
2932
outputs:
3033
python-version:
3134
description: "The installed Python or PyPy version. Useful when given a version range as input."

0 commit comments

Comments
 (0)