Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4eb7dbc
Bump braces from 3.0.2 to 3.0.3 (#893)
dependabot[bot] Jun 26, 2024
a0d74c0
fix(ci): update all failing workflows (#863)
mayeut Jun 27, 2024
39cd149
Documentation update for cache (#873)
gowridurgad Jul 10, 2024
cb68456
Updated @iarna/toml version to 3.0.0 (#912)
priya-kinthali Jul 22, 2024
04c1311
Fix display of emojis in contributors doc (#899)
sciencewhiz Jul 23, 2024
036a523
Fix: Add `.zip` extension to Windows package downloads for `Expand-Ar…
priyagupta108 Aug 5, 2024
80b49d3
fix: add arch to cache key (#896)
Zxilly Aug 7, 2024
2bd53f9
Documentation update for caching poetry dependencies (#908)
gowridurgad Aug 8, 2024
f677139
Bump pyinstaller from 3.6 to 5.13.1 in /__tests__/data (#923)
aparnajyothi-y Aug 13, 2024
29a37be
initial commit (#938)
priya-kinthali Sep 6, 2024
65b48c7
Create publish-immutable-actions.yml
Jcambass Sep 10, 2024
70dcb22
Merge pull request #941 from actions/Jcambass-patch-1
Jcambass Sep 10, 2024
3226af6
Upgrade IA publish
Jcambass Sep 16, 2024
e9675cc
Merge pull request #943 from actions/Jcambass-patch-1
Jcambass Sep 26, 2024
19dfb7b
Bump default versions to latest (#905)
jeffwidman Oct 4, 2024
f4c5a11
Revise `isGhes` logic (#963)
jww3 Oct 21, 2024
9c76e71
Bump pillow from 7.2 to 10.2.0 in /__tests__/data (#956)
aparnajyothi-y Oct 21, 2024
0b93645
Enhance workflows: Add macOS 13 support, upgrade publish-action, and …
priya-kinthali Oct 24, 2024
55aad42
Update error message for no dependencies to cache (#968)
aparnajyothi-y Nov 5, 2024
3fddbee
Enhance Workflows: Add Ubuntu-24, Remove Python 3.8 (#985)
priya-kinthali Dec 19, 2024
1928ae6
Update README.md (#1009)
Jan 16, 2025
b8cf3eb
Use the new cache service: upgrade `@actions/cache` to `^4.0.0` (#1007)
priyagupta108 Jan 21, 2025
e3dfaac
Configure Dependabot settings (#1008)
HarithaVattikuti Jan 22, 2025
d0b4fc4
Bump undici from 5.28.4 to 5.28.5 (#1012)
dependabot[bot] Jan 22, 2025
feb9c6e
Bump urllib3 from 1.25.9 to 1.26.19 in /__tests__/data (#895)
dependabot[bot] Jan 27, 2025
0dc2d2c
Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 (#1014)
dependabot[bot] Jan 27, 2025
ceb20b2
Bump @actions/http-client from 2.2.1 to 2.2.3 (#1020)
dependabot[bot] Jan 27, 2025
709bfa5
Bump requests from 2.24.0 to 2.32.2 in /__tests__/data (#1019)
dependabot[bot] Jan 27, 2025
4237552
Improve Advanced Usage examples (#645)
lrq3000 Jan 27, 2025
8039c45
fix: install PyPy on Linux ARM64 (#1011)
mayeut Feb 5, 2025
6ca8e85
Bump @vercel/ncc from 0.38.1 to 0.38.3 (#1016)
dependabot[bot] Feb 18, 2025
9e62be8
Support free threaded Python versions like '3.13t' (#973)
colesbury Mar 4, 2025
6fd11e1
Bump @actions/glob from 0.4.0 to 0.5.0 (#1015)
dependabot[bot] Mar 12, 2025
19e4675
Add support for .tool-versions file in setup-python (#1043)
mahabaleshwars Mar 13, 2025
8d9ed9a
Add e2e Testing for free threaded and Bump @action/cache from 4.0.0 t…
priya-kinthali Mar 24, 2025
e348410
Remove Ubuntu 20.04 from workflows due to deprecation from 2025-04-15…
aparnajyothi-y Apr 11, 2025
6ed2c67
Fix for Candidate Not Iterable Error (#1082)
aparnajyothi-y Apr 17, 2025
5d95bc1
Bump semver and @types/semver (#1091)
dependabot[bot] Apr 22, 2025
30eafe9
Bump prettier from 2.8.8 to 3.5.3 (#1046)
dependabot[bot] Apr 22, 2025
a26af69
Bump ts-jest from 29.1.2 to 29.3.2 (#1081)
dependabot[bot] Apr 24, 2025
5db1cf9
Enhance reading from .python-version (#787)
krystof-k May 21, 2025
5fa0ee6
Bump @actions/tool-cache from 2.0.1 to 2.0.2 (#1095)
dependabot[bot] Jun 18, 2025
e9c40fb
Add support for `pip-version` (#1129)
priyagupta108 Jun 20, 2025
1264885
Enhance cache-dependency-path handling to support files outside the w…
aparnajyothi-y Jun 25, 2025
532b046
Add Architecture-Specific PATH Management for Python with --user Flag…
aparnajyothi-y Jul 3, 2025
88ffd4d
Include python version in PyPy python-version output (#1110)
cdce8p Jul 21, 2025
3c6f142
update documentation (#1156)
priya-kinthali Jul 23, 2025
677a682
Merge remote-tracking branch 'upstream/main'
mmatl Jul 23, 2025
dfdaf8d
fix: fix
mmatl Jul 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Support free threaded Python versions like '3.13t' (actions#973)
* Support free threaded Python versions like '3.13t'

Python wheels, pyenv, and a number of other tools use 't' in the Python
version number to identify free threaded builds. For example, '3.13t',
'3.14.0a1', '3.14t-dev'.

This PR supports that syntax in `actions/setup-python`, strips the "t",
and adds "-freethreading" to the architecture to select the correct
Python version.

See actions#771

* Add free threading to advanced usage documentation

* Fix desugaring of `3.13.1t` and add test case.

* Add freethreaded input and fix handling of prerelease versions

* Fix lint

* Add 't' suffix to python-version output

* Use distinct cache key for free threaded Python

* Remove support for syntax like '3.14.0a1'

* Clarify use of 't' suffix

* Improve error message when trying to use free threaded Python versions before 3.13
  • Loading branch information
colesbury authored Mar 4, 2025
commit 9e62be81b28222addecf85e47571213eb7680449
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ steps:
- run: python my_script.py
```

**Free threaded Python**
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13t'
- run: python my_script.py
```

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.

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/).
Expand Down
43 changes: 43 additions & 0 deletions __tests__/find-python.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {desugarVersion, pythonVersionToSemantic} from '../src/find-python';

describe('desugarVersion', () => {
it.each([
['3.13', {version: '3.13', freethreaded: false}],
['3.13t', {version: '3.13', freethreaded: true}],
['3.13.1', {version: '3.13.1', freethreaded: false}],
['3.13.1t', {version: '3.13.1', freethreaded: true}],
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}]
])('%s -> %s', (input, expected) => {
expect(desugarVersion(input)).toEqual(expected);
});
});

// Test the combined desugarVersion and pythonVersionToSemantic functions
describe('pythonVersions', () => {
it.each([
['3.13', {version: '3.13', freethreaded: false}],
['3.13t', {version: '3.13', freethreaded: true}],
['3.13.1', {version: '3.13.1', freethreaded: false}],
['3.13.1t', {version: '3.13.1', freethreaded: true}],
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}]
])('%s -> %s', (input, expected) => {
const {version, freethreaded} = desugarVersion(input);
const semanticVersionSpec = pythonVersionToSemantic(version, false);
expect({version: semanticVersionSpec, freethreaded}).toEqual(expected);
});

it.each([
['3.13', {version: '~3.13.0-0', freethreaded: false}],
['3.13t', {version: '~3.13.0-0', freethreaded: true}],
['3.13.1', {version: '3.13.1', freethreaded: false}],
['3.13.1t', {version: '3.13.1', freethreaded: true}],
['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}]
])('%s (allowPreReleases=true) -> %s', (input, expected) => {
const {version, freethreaded} = desugarVersion(input);
const semanticVersionSpec = pythonVersionToSemantic(version, true);
expect({version: semanticVersionSpec, freethreaded}).toEqual(expected);
});
});
43 changes: 32 additions & 11 deletions __tests__/finder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Finder tests', () => {
await io.mkdirP(pythonDir);
fs.writeFileSync(`${pythonDir}.complete`, 'hello');
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await finder.useCpythonVersion('3.x', 'x64', true, false, false);
await finder.useCpythonVersion('3.x', 'x64', true, false, false, false);
expect(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation',
Expand All @@ -73,7 +73,7 @@ describe('Finder tests', () => {
await io.mkdirP(pythonDir);
fs.writeFileSync(`${pythonDir}.complete`, 'hello');
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await finder.useCpythonVersion('3.x', 'x64', false, false, false);
await finder.useCpythonVersion('3.x', 'x64', false, false, false, false);
expect(spyCoreAddPath).not.toHaveBeenCalled();
expect(spyCoreExportVariable).not.toHaveBeenCalled();
});
Expand All @@ -96,7 +96,7 @@ describe('Finder tests', () => {
});
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await expect(
finder.useCpythonVersion('1.2.3', 'x64', true, false, false)
finder.useCpythonVersion('1.2.3', 'x64', true, false, false, false)
).resolves.toEqual({
impl: 'CPython',
version: '1.2.3'
Expand Down Expand Up @@ -135,7 +135,14 @@ describe('Finder tests', () => {
});
// This will throw if it doesn't find it in the manifest (because no such version exists)
await expect(
finder.useCpythonVersion('1.2.4-beta.2', 'x64', false, false, false)
finder.useCpythonVersion(
'1.2.4-beta.2',
'x64',
false,
false,
false,
false
)
).resolves.toEqual({
impl: 'CPython',
version: '1.2.4-beta.2'
Expand Down Expand Up @@ -186,7 +193,7 @@ describe('Finder tests', () => {

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

expect(infoSpy).toHaveBeenCalledWith("Resolved as '1.2.3'");
expect(infoSpy).toHaveBeenCalledWith(
Expand All @@ -197,7 +204,14 @@ describe('Finder tests', () => {
);
expect(installSpy).toHaveBeenCalled();
expect(addPathSpy).toHaveBeenCalledWith(expPath);
await finder.useCpythonVersion('1.2.4-beta.2', 'x64', false, true, false);
await finder.useCpythonVersion(
'1.2.4-beta.2',
'x64',
false,
true,
false,
false
);
expect(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation',
Expand All @@ -224,7 +238,7 @@ describe('Finder tests', () => {
});
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await expect(
finder.useCpythonVersion('1.2', 'x64', false, false, false)
finder.useCpythonVersion('1.2', 'x64', false, false, false, false)
).resolves.toEqual({
impl: 'CPython',
version: '1.2.3'
Expand All @@ -251,25 +265,32 @@ describe('Finder tests', () => {
});
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await expect(
finder.useCpythonVersion('1.1', 'x64', false, false, false)
finder.useCpythonVersion('1.1', 'x64', false, false, false, false)
).rejects.toThrow();
await expect(
finder.useCpythonVersion('1.1', 'x64', false, false, true)
finder.useCpythonVersion('1.1', 'x64', false, false, true, false)
).resolves.toEqual({
impl: 'CPython',
version: '1.1.0-beta.2'
});
// Check 1.1.0 version specifier does not fallback to '1.1.0-beta.2'
await expect(
finder.useCpythonVersion('1.1.0', 'x64', false, false, true)
finder.useCpythonVersion('1.1.0', 'x64', false, false, true, false)
).rejects.toThrow();
});

it('Errors if Python is not installed', async () => {
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
let thrown = false;
try {
await finder.useCpythonVersion('3.300000', 'x64', true, false, false);
await finder.useCpythonVersion(
'3.300000',
'x64',
true,
false,
false,
false
);
} catch {
thrown = true;
}
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ inputs:
allow-prereleases:
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."
default: false
freethreaded:
description: "When 'true', use the freethreaded version of Python."
default: false
outputs:
python-version:
description: "The installed Python or PyPy version. Useful when given a version range as input."
Expand Down
61 changes: 51 additions & 10 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -99514,7 +99514,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.pythonVersionToSemantic = exports.useCpythonVersion = void 0;
exports.pythonVersionToSemantic = exports.desugarVersion = exports.useCpythonVersion = void 0;
const os = __importStar(__nccwpck_require__(857));
const path = __importStar(__nccwpck_require__(6928));
const utils_1 = __nccwpck_require__(1798);
Expand Down Expand Up @@ -99542,13 +99542,22 @@ function binDir(installDir) {
return path.join(installDir, 'bin');
}
}
function useCpythonVersion(version, architecture, updateEnvironment, checkLatest, allowPreReleases) {
function useCpythonVersion(version, architecture, updateEnvironment, checkLatest, allowPreReleases, freethreaded) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
let manifest = null;
const desugaredVersionSpec = desugarDevVersion(version);
const { version: desugaredVersionSpec, freethreaded: versionFreethreaded } = desugarVersion(version);
let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec, allowPreReleases);
if (versionFreethreaded) {
// Use the freethreaded version if it was specified in the input, e.g., 3.13t
freethreaded = true;
}
core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`);
if (freethreaded) {
// Free threaded versions use an architecture suffix like `x64-freethreaded`
core.debug(`Using freethreaded version of ${semanticVersionSpec}`);
architecture += '-freethreaded';
}
if (checkLatest) {
manifest = yield installer.getManifest();
const resolvedVersion = (_a = (yield installer.findReleaseFromManifest(semanticVersionSpec, architecture, manifest))) === null || _a === void 0 ? void 0 : _a.version;
Expand All @@ -99572,12 +99581,16 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest
}
if (!installDir) {
const osInfo = yield (0, utils_1.getOSInfo)();
throw new Error([
const msg = [
`The version '${version}' with architecture '${architecture}' was not found for ${osInfo
? `${osInfo.osName} ${osInfo.osVersion}`
: 'this operating system'}.`,
`The list of all available versions can be found here: ${installer.MANIFEST_URL}`
].join(os.EOL));
: 'this operating system'}.`
];
if (freethreaded) {
msg.push(`Free threaded versions are only available for Python 3.13.0 and later.`);
}
msg.push(`The list of all available versions can be found here: ${installer.MANIFEST_URL}`);
throw new Error(msg.join(os.EOL));
}
const _binDir = binDir(installDir);
const binaryExtension = utils_1.IS_WINDOWS ? '.exe' : '';
Expand Down Expand Up @@ -99617,12 +99630,39 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest
// On Linux and macOS, pip will create the --user directory and add it to PATH as needed.
}
const installed = versionFromPath(installDir);
core.setOutput('python-version', installed);
let pythonVersion = installed;
if (freethreaded) {
// Add the freethreaded suffix to the version (e.g., 3.13.1t)
pythonVersion += 't';
}
core.setOutput('python-version', pythonVersion);
core.setOutput('python-path', pythonPath);
return { impl: 'CPython', version: installed };
return { impl: 'CPython', version: pythonVersion };
});
}
exports.useCpythonVersion = useCpythonVersion;
/* Desugar free threaded and dev versions */
function desugarVersion(versionSpec) {
const { version, freethreaded } = desugarFreeThreadedVersion(versionSpec);
return { version: desugarDevVersion(version), freethreaded };
}
exports.desugarVersion = desugarVersion;
/* Identify freethreaded versions like, 3.13t, 3.13.1t, 3.13t-dev.
* Returns the version without the `t` and the architectures suffix, if freethreaded */
function desugarFreeThreadedVersion(versionSpec) {
const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/;
if (majorMinor.test(versionSpec)) {
return { version: versionSpec.replace(majorMinor, '$1'), freethreaded: true };
}
const devVersion = /^(\d+\.\d+)(t)(-dev)$/;
if (devVersion.test(versionSpec)) {
return {
version: versionSpec.replace(devVersion, '$1$3'),
freethreaded: true
};
}
return { version: versionSpec, freethreaded: false };
}
/** Convert versions like `3.8-dev` to a version like `~3.8.0-0`. */
function desugarDevVersion(versionSpec) {
const devVersion = /^(\d+)\.(\d+)-dev$/;
Expand Down Expand Up @@ -100365,6 +100405,7 @@ function run() {
const versions = resolveVersionInput();
const checkLatest = core.getBooleanInput('check-latest');
const allowPreReleases = core.getBooleanInput('allow-prereleases');
const freethreaded = core.getBooleanInput('freethreaded');
if (versions.length) {
let pythonVersion = '';
const arch = core.getInput('architecture') || os.arch();
Expand All @@ -100385,7 +100426,7 @@ function run() {
if (version.startsWith('2')) {
core.warning('The support for python 2.7 was removed on June 19, 2023. Related issue: https://github.com/actions/setup-python/issues/672');
}
const installed = yield finder.useCpythonVersion(version, arch, updateEnvironment, checkLatest, allowPreReleases);
const installed = yield finder.useCpythonVersion(version, arch, updateEnvironment, checkLatest, allowPreReleases, freethreaded);
pythonVersion = installed.version;
core.info(`Successfully set up ${installed.impl} (${pythonVersion})`);
}
Expand Down
25 changes: 25 additions & 0 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,31 @@ steps:
- run: python my_script.py
```
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.
You can use the **t** suffix when specifying the major and minor version (e.g., `3.13t`), with a patch version (e.g., `3.13.1t`), or with the **x.y-dev syntax** (e.g., `3.14t-dev`).
Free threaded Python is only available starting with the 3.13 release.

```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13t'
- run: python my_script.py
```

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.

```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '>=3.13'
freethreaded: true
- run: python my_script.py
```

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

- **[ranges](https://github.com/npm/node-semver#ranges)** to download and set up the latest available version of Python satisfying a range:
Expand Down
Loading