Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.

Commit 40cc54b

Browse files
colesburyandfoy
authored andcommitted
Add freethreaded input and fix handling of prerelease versions
1 parent 6aeccc6 commit 40cc54b

7 files changed

Lines changed: 163 additions & 55 deletions

File tree

__tests__/find-python.test.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,54 @@
1-
import {desugarVersion} from '../src/find-python';
1+
import {desugarVersion, pythonVersionToSemantic} from '../src/find-python';
22

33
describe('desugarVersion', () => {
44
it.each([
5-
['3.13', ['3.13', '']],
6-
['3.13t', ['3.13', '-freethreaded']],
7-
['3.13.1', ['3.13.1', '']],
8-
['3.13.1t', ['3.13.1', '-freethreaded']],
9-
['3.14-dev', ['~3.14.0-0', '']],
10-
['3.14t-dev', ['~3.14.0-0', '-freethreaded']],
11-
['3.14.0a4', ['3.14.0a4', '']],
12-
['3.14.0ta4', ['3.14.0a4', '-freethreaded']],
13-
['3.14.0rc1', ['3.14.0rc1', '']],
14-
['3.14.0trc1', ['3.14.0rc1', '-freethreaded']]
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+
['3.14.0a4', {version: '3.14.0a4', freethreaded: false}],
12+
['3.14.0rc1', {version: '3.14.0rc1', freethreaded: false}],
13+
['3.14.0rc1t', {version: '3.14.0rc1', freethreaded: true}]
1514
])('%s -> %s', (input, expected) => {
1615
expect(desugarVersion(input)).toEqual(expected);
1716
});
1817
});
18+
19+
// Test the combined desugarVersion and pythonVersionToSemantic functions
20+
describe('pythonVersions', () => {
21+
it.each([
22+
['3.13', {version: '3.13', freethreaded: false}],
23+
['3.13t', {version: '3.13', freethreaded: true}],
24+
['3.13.1', {version: '3.13.1', freethreaded: false}],
25+
['3.13.1t', {version: '3.13.1', freethreaded: true}],
26+
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
27+
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}],
28+
['3.14.0a4', {version: '3.14.0-alpha.4', freethreaded: false}],
29+
['3.14.0a4t', {version: '3.14.0-alpha.4', freethreaded: true}],
30+
['3.14.0rc1', {version: '3.14.0-rc.1', freethreaded: false}],
31+
['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}]
32+
])('%s -> %s', (input, expected) => {
33+
const {version, freethreaded} = desugarVersion(input);
34+
let semanticVersionSpec = pythonVersionToSemantic(version, false);
35+
expect({version: semanticVersionSpec, freethreaded}).toEqual(expected);
36+
});
37+
38+
it.each([
39+
['3.13', {version: '~3.13.0-0', freethreaded: false}],
40+
['3.13t', {version: '~3.13.0-0', freethreaded: true}],
41+
['3.13.1', {version: '3.13.1', freethreaded: false}],
42+
['3.13.1t', {version: '3.13.1', freethreaded: true}],
43+
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
44+
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}],
45+
['3.14.0a4', {version: '3.14.0-alpha.4', freethreaded: false}],
46+
['3.14.0a4t', {version: '3.14.0-alpha.4', freethreaded: true}],
47+
['3.14.0rc1', {version: '3.14.0-rc.1', freethreaded: false}],
48+
['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}]
49+
])('%s (allowPreReleases=true) -> %s', (input, expected) => {
50+
const {version, freethreaded} = desugarVersion(input);
51+
let semanticVersionSpec = pythonVersionToSemantic(version, true);
52+
expect({version: semanticVersionSpec, freethreaded}).toEqual(expected);
53+
});
54+
});

__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
}

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."

dist/setup/index.js

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99542,17 +99542,21 @@ function binDir(installDir) {
9954299542
return path.join(installDir, 'bin');
9954399543
}
9954499544
}
99545-
function useCpythonVersion(version, architecture, updateEnvironment, checkLatest, allowPreReleases) {
99545+
function useCpythonVersion(version, architecture, updateEnvironment, checkLatest, allowPreReleases, freethreaded) {
9954699546
return __awaiter(this, void 0, void 0, function* () {
9954799547
var _a;
9954899548
let manifest = null;
99549-
const [desugaredVersionSpec, freethreaded] = desugarVersion(version);
99549+
const { version: desugaredVersionSpec, freethreaded: versionFreethreaded } = desugarVersion(version);
9955099550
let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec, allowPreReleases);
99551+
if (versionFreethreaded) {
99552+
// Use the freethreaded version if it was specified in the input, e.g., 3.13t
99553+
freethreaded = true;
99554+
}
9955199555
core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`);
9955299556
if (freethreaded) {
9955399557
// Free threaded versions use an architecture suffix like `x64-freethreaded`
9955499558
core.debug(`Using freethreaded version of ${semanticVersionSpec}`);
99555-
architecture += freethreaded;
99559+
architecture += '-freethreaded';
9955699560
}
9955799561
if (checkLatest) {
9955899562
manifest = yield installer.getManifest();
@@ -99630,27 +99634,33 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest
9963099634
exports.useCpythonVersion = useCpythonVersion;
9963199635
/* Desugar free threaded and dev versions */
9963299636
function desugarVersion(versionSpec) {
99633-
const [desugaredVersionSpec, freethreaded] = desugarFreeThreadedVersion(versionSpec);
99634-
const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec);
99635-
return [desugaredVersionSpec2, freethreaded];
99637+
const { version, freethreaded } = desugarFreeThreadedVersion(versionSpec);
99638+
return { version: desugarDevVersion(version), freethreaded };
9963699639
}
9963799640
exports.desugarVersion = desugarVersion;
9963899641
/* Identify freethreaded versions like, 3.13t, 3.13.1t, 3.13t-dev, 3.14.0a1t.
9963999642
* Returns the version without the `t` and the architectures suffix, if freethreaded */
9964099643
function desugarFreeThreadedVersion(versionSpec) {
99641-
const prereleaseVersion = /(\d+\.\d+\.\d+)(t)((?:a|b|rc)\d*)/g;
99644+
// e.g., 3.14.0a1t -> 3.14.0a1
99645+
const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)(t)/g;
9964299646
if (prereleaseVersion.test(versionSpec)) {
99643-
return [versionSpec.replace(prereleaseVersion, '$1$3'), '-freethreaded'];
99647+
return {
99648+
version: versionSpec.replace(prereleaseVersion, '$1$2'),
99649+
freethreaded: true
99650+
};
9964499651
}
9964599652
const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/;
9964699653
if (majorMinor.test(versionSpec)) {
99647-
return [versionSpec.replace(majorMinor, '$1'), '-freethreaded'];
99654+
return { version: versionSpec.replace(majorMinor, '$1'), freethreaded: true };
9964899655
}
9964999656
const devVersion = /^(\d+\.\d+)(t)(-dev)$/;
9965099657
if (devVersion.test(versionSpec)) {
99651-
return [versionSpec.replace(devVersion, '$1$3'), '-freethreaded'];
99658+
return {
99659+
version: versionSpec.replace(devVersion, '$1$3'),
99660+
freethreaded: true
99661+
};
9965299662
}
99653-
return [versionSpec, ''];
99663+
return { version: versionSpec, freethreaded: false };
9965499664
}
9965599665
/** Convert versions like `3.8-dev` to a version like `~3.8.0-0`. */
9965699666
function desugarDevVersion(versionSpec) {
@@ -99665,15 +99675,22 @@ function versionFromPath(installDir) {
9966599675
}
9966699676
/**
9966799677
* Python's prelease versions look like `3.7.0b2`.
99668-
* This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`.
99678+
* This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-beta.2`.
9966999679
* If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent.
9967099680
*
9967199681
* For easier use of the action, we also map 'x.y' to allow pre-release before 'x.y.0' release if allowPreReleases is true
9967299682
*/
9967399683
function pythonVersionToSemantic(versionSpec, allowPreReleases) {
99674-
const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g;
99684+
const preleaseMap = {
99685+
a: 'alpha',
99686+
b: 'beta',
99687+
rc: 'rc'
99688+
};
99689+
const prereleaseVersion = /(\d+\.\d+\.\d+)(a|b|rc)(\d+)/g;
99690+
let result = versionSpec.replace(prereleaseVersion, (_, p1, p2, p3) => {
99691+
return `${p1}-${preleaseMap[p2]}.${p3}`;
99692+
});
9967599693
const majorMinor = /^(\d+)\.(\d+)$/;
99676-
let result = versionSpec.replace(prereleaseVersion, '$1-$2');
9967799694
if (allowPreReleases) {
9967899695
result = result.replace(majorMinor, '~$1.$2.0-0');
9967999696
}
@@ -100389,6 +100406,7 @@ function run() {
100389100406
const versions = resolveVersionInput();
100390100407
const checkLatest = core.getBooleanInput('check-latest');
100391100408
const allowPreReleases = core.getBooleanInput('allow-prereleases');
100409+
const freethreaded = core.getBooleanInput('freethreaded');
100392100410
if (versions.length) {
100393100411
let pythonVersion = '';
100394100412
const arch = core.getInput('architecture') || os.arch();
@@ -100409,7 +100427,7 @@ function run() {
100409100427
if (version.startsWith('2')) {
100410100428
core.warning('The support for python 2.7 was removed on June 19, 2023. Related issue: https://github.com/actions/setup-python/issues/672');
100411100429
}
100412-
const installed = yield finder.useCpythonVersion(version, arch, updateEnvironment, checkLatest, allowPreReleases);
100430+
const installed = yield finder.useCpythonVersion(version, arch, updateEnvironment, checkLatest, allowPreReleases, freethreaded);
100413100431
pythonVersion = installed.version;
100414100432
core.info(`Successfully set up ${installed.impl} (${pythonVersion})`);
100415100433
}

docs/advanced-usage.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ steps:
7777
- run: python my_script.py
7878
```
7979
80-
Use the **t** suffix to select the [free threading](https://docs.python.org/3/howto/free-threading-python.html) version of Python.
80+
You can specify the [free threading](https://docs.python.org/3/howto/free-threading-python.html) version of Python by setting the `freethreaded` input to `true` or by using the special **t** suffix in some cases. Pre-release free threading versions can be specified like `3.14.0a3t` or `3.14t-dev`.
8181
Free threaded Python is only available starting with the 3.13 release.
8282

8383
```yaml
@@ -89,7 +89,17 @@ steps:
8989
- run: python my_script.py
9090
```
9191

92-
Pre-release free threading versions should be specified like `3.14.0ta3` or `3.14t-dev`.
92+
Note that the **t** suffix is not `semver` syntax. If you wish to specify a range, you must use the `freethreaded` input instead of the `t` suffix.
93+
94+
```yaml
95+
steps:
96+
- uses: actions/checkout@v4
97+
- uses: actions/setup-python@v5
98+
with:
99+
python-version: '>=3.13'
100+
freethreaded: true
101+
- run: python my_script.py
102+
```
93103

94104
You can also use several types of ranges that are specified in [semver](https://github.com/npm/node-semver#ranges), for instance:
95105

0 commit comments

Comments
 (0)