From eb2a6532f967fae3f4abd15c7923b67fef608b67 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Mon, 28 Oct 2024 18:14:11 +0000 Subject: [PATCH 01/10] 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 #771 --- dist/setup/index.js | 27 +++++++++++++++++++++++++-- src/find-python.ts | 30 ++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index e56f96608..97b5e5ceb 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -91038,9 +91038,15 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest return __awaiter(this, void 0, void 0, function* () { var _a; let manifest = null; - const desugaredVersionSpec = desugarDevVersion(version); - let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec, allowPreReleases); + const [desugaredVersionSpec, freethreaded] = desugarFreeThreadedVersion(version); + const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); + let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec2, allowPreReleases); 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; @@ -91115,6 +91121,23 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest }); } exports.useCpythonVersion = useCpythonVersion; +/* Identify freethreaded versions like, 3.13t, 3.13t-dev, 3.14.0a1t. Returns + * the version without the `t` and the architectures suffix, if freethreaded */ +function desugarFreeThreadedVersion(versionSpec) { + const prereleaseVersion = /(\d+\.\d+\.\d+)(t)((?:a|b|rc)\d*)/g; + if (prereleaseVersion.test(versionSpec)) { + return [versionSpec.replace(prereleaseVersion, '$1$3'), '-freethreaded']; + } + const majorMinor = /^(\d+\.\d+)(t)$/; + if (majorMinor.test(versionSpec)) { + return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; + } + const devVersion = /^(\d+\.\d+)(t)(-dev)$/; + if (devVersion.test(versionSpec)) { + return [versionSpec.replace(devVersion, '$1$3'), '-freethreaded']; + } + return [versionSpec, '']; +} /** Convert versions like `3.8-dev` to a version like `~3.8.0-0`. */ function desugarDevVersion(versionSpec) { const devVersion = /^(\d+)\.(\d+)-dev$/; diff --git a/src/find-python.ts b/src/find-python.ts index 77278770a..3c2b8b135 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -38,13 +38,21 @@ export async function useCpythonVersion( allowPreReleases: boolean ): Promise { let manifest: tc.IToolRelease[] | null = null; - const desugaredVersionSpec = desugarDevVersion(version); + const [desugaredVersionSpec, freethreaded] = + desugarFreeThreadedVersion(version); + const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); let semanticVersionSpec = pythonVersionToSemantic( - desugaredVersionSpec, + desugaredVersionSpec2, allowPreReleases ); 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 = await installer.getManifest(); const resolvedVersion = ( @@ -159,6 +167,24 @@ export async function useCpythonVersion( return {impl: 'CPython', version: installed}; } +/* Identify freethreaded versions like, 3.13t, 3.13t-dev, 3.14.0a1t. Returns + * the version without the `t` and the architectures suffix, if freethreaded */ +function desugarFreeThreadedVersion(versionSpec: string) { + const prereleaseVersion = /(\d+\.\d+\.\d+)(t)((?:a|b|rc)\d*)/g; + if (prereleaseVersion.test(versionSpec)) { + return [versionSpec.replace(prereleaseVersion, '$1$3'), '-freethreaded']; + } + const majorMinor = /^(\d+\.\d+)(t)$/; + if (majorMinor.test(versionSpec)) { + return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; + } + const devVersion = /^(\d+\.\d+)(t)(-dev)$/; + if (devVersion.test(versionSpec)) { + return [versionSpec.replace(devVersion, '$1$3'), '-freethreaded']; + } + return [versionSpec, '']; +} + /** Convert versions like `3.8-dev` to a version like `~3.8.0-0`. */ function desugarDevVersion(versionSpec: string) { const devVersion = /^(\d+)\.(\d+)-dev$/; From c70f76ebc5899ddfa563913ad6ee343ab2e88624 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 22 Jan 2025 19:38:22 +0000 Subject: [PATCH 02/10] Add free threading to advanced usage documentation --- README.md | 10 ++++++++++ docs/advanced-usage.md | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index 6ffab1fab..5e73889a1 100644 --- a/README.md +++ b/README.md @@ -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/). diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 774bcefea..dc8665d3d 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -77,6 +77,20 @@ steps: - run: python my_script.py ``` +Use the **t** suffix to select the [free threading](https://docs.python.org/3/howto/free-threading-python.html) version of Python. +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 +``` + +Pre-release free threading versions should be specified like `3.14.0ta3` or `3.14t-dev`. + 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: From d653c0b66e6071445f0ce9a77df552943ca12642 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 22 Jan 2025 20:22:49 +0000 Subject: [PATCH 03/10] Fix desugaring of `3.13.1t` and add test case. --- __tests__/find-python.test.ts | 18 ++++++++++++++++++ dist/setup/index.js | 20 +++++++++++++------- src/find-python.ts | 20 +++++++++++++------- 3 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 __tests__/find-python.test.ts diff --git a/__tests__/find-python.test.ts b/__tests__/find-python.test.ts new file mode 100644 index 000000000..e779a7a2d --- /dev/null +++ b/__tests__/find-python.test.ts @@ -0,0 +1,18 @@ +import {desugarVersion} from '../src/find-python'; + +describe('desugarVersion', () => { + it.each([ + ['3.13', ['3.13', '']], + ['3.13t', ['3.13', '-freethreaded']], + ['3.13.1', ['3.13.1', '']], + ['3.13.1t', ['3.13.1', '-freethreaded']], + ['3.14-dev', ['~3.14.0-0', '']], + ['3.14t-dev', ['~3.14.0-0', '-freethreaded']], + ['3.14.0a4', ['3.14.0a4', '']], + ['3.14.0ta4', ['3.14.0a4', '-freethreaded']], + ['3.14.0rc1', ['3.14.0rc1', '']], + ['3.14.0trc1', ['3.14.0rc1', '-freethreaded']] + ])('%s -> %s', (input, expected) => { + expect(desugarVersion(input)).toEqual(expected); + }); +}); diff --git a/dist/setup/index.js b/dist/setup/index.js index 97b5e5ceb..a0bed3bf6 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -91006,7 +91006,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__(2037)); const path = __importStar(__nccwpck_require__(1017)); const utils_1 = __nccwpck_require__(1314); @@ -91038,9 +91038,8 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest return __awaiter(this, void 0, void 0, function* () { var _a; let manifest = null; - const [desugaredVersionSpec, freethreaded] = desugarFreeThreadedVersion(version); - const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); - let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec2, allowPreReleases); + const [desugaredVersionSpec, freethreaded] = desugarVersion(version); + let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec, allowPreReleases); core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`); if (freethreaded) { // Free threaded versions use an architecture suffix like `x64-freethreaded` @@ -91121,14 +91120,21 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest }); } exports.useCpythonVersion = useCpythonVersion; -/* Identify freethreaded versions like, 3.13t, 3.13t-dev, 3.14.0a1t. Returns - * the version without the `t` and the architectures suffix, if freethreaded */ +/* Desugar free threaded and dev versions */ +function desugarVersion(versionSpec) { + const [desugaredVersionSpec, freethreaded] = desugarFreeThreadedVersion(versionSpec); + const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); + return [desugaredVersionSpec2, freethreaded]; +} +exports.desugarVersion = desugarVersion; +/* Identify freethreaded versions like, 3.13t, 3.13.1t, 3.13t-dev, 3.14.0a1t. + * Returns the version without the `t` and the architectures suffix, if freethreaded */ function desugarFreeThreadedVersion(versionSpec) { const prereleaseVersion = /(\d+\.\d+\.\d+)(t)((?:a|b|rc)\d*)/g; if (prereleaseVersion.test(versionSpec)) { return [versionSpec.replace(prereleaseVersion, '$1$3'), '-freethreaded']; } - const majorMinor = /^(\d+\.\d+)(t)$/; + const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/; if (majorMinor.test(versionSpec)) { return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; } diff --git a/src/find-python.ts b/src/find-python.ts index 3c2b8b135..6685d060d 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -38,11 +38,9 @@ export async function useCpythonVersion( allowPreReleases: boolean ): Promise { let manifest: tc.IToolRelease[] | null = null; - const [desugaredVersionSpec, freethreaded] = - desugarFreeThreadedVersion(version); - const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); + const [desugaredVersionSpec, freethreaded] = desugarVersion(version); let semanticVersionSpec = pythonVersionToSemantic( - desugaredVersionSpec2, + desugaredVersionSpec, allowPreReleases ); core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`); @@ -167,14 +165,22 @@ export async function useCpythonVersion( return {impl: 'CPython', version: installed}; } -/* Identify freethreaded versions like, 3.13t, 3.13t-dev, 3.14.0a1t. Returns - * the version without the `t` and the architectures suffix, if freethreaded */ +/* Desugar free threaded and dev versions */ +export function desugarVersion(versionSpec: string) { + const [desugaredVersionSpec, freethreaded] = + desugarFreeThreadedVersion(versionSpec); + const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); + return [desugaredVersionSpec2, freethreaded]; +} + +/* Identify freethreaded versions like, 3.13t, 3.13.1t, 3.13t-dev, 3.14.0a1t. + * Returns the version without the `t` and the architectures suffix, if freethreaded */ function desugarFreeThreadedVersion(versionSpec: string) { const prereleaseVersion = /(\d+\.\d+\.\d+)(t)((?:a|b|rc)\d*)/g; if (prereleaseVersion.test(versionSpec)) { return [versionSpec.replace(prereleaseVersion, '$1$3'), '-freethreaded']; } - const majorMinor = /^(\d+\.\d+)(t)$/; + const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/; if (majorMinor.test(versionSpec)) { return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; } From 72902a03bd88292ff03669b0214388a4e4590484 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Thu, 30 Jan 2025 19:46:04 +0000 Subject: [PATCH 04/10] Add freethreaded input and fix handling of prerelease versions --- __tests__/find-python.test.ts | 58 ++++++++++++++++++++++++++++------- __tests__/finder.test.ts | 43 +++++++++++++++++++------- action.yml | 3 ++ dist/setup/index.js | 48 ++++++++++++++++++++--------- docs/advanced-usage.md | 14 +++++++-- src/find-python.ts | 48 ++++++++++++++++++++--------- src/setup-python.ts | 4 ++- 7 files changed, 163 insertions(+), 55 deletions(-) diff --git a/__tests__/find-python.test.ts b/__tests__/find-python.test.ts index e779a7a2d..6178ceec8 100644 --- a/__tests__/find-python.test.ts +++ b/__tests__/find-python.test.ts @@ -1,18 +1,54 @@ -import {desugarVersion} from '../src/find-python'; +import {desugarVersion, pythonVersionToSemantic} from '../src/find-python'; describe('desugarVersion', () => { it.each([ - ['3.13', ['3.13', '']], - ['3.13t', ['3.13', '-freethreaded']], - ['3.13.1', ['3.13.1', '']], - ['3.13.1t', ['3.13.1', '-freethreaded']], - ['3.14-dev', ['~3.14.0-0', '']], - ['3.14t-dev', ['~3.14.0-0', '-freethreaded']], - ['3.14.0a4', ['3.14.0a4', '']], - ['3.14.0ta4', ['3.14.0a4', '-freethreaded']], - ['3.14.0rc1', ['3.14.0rc1', '']], - ['3.14.0trc1', ['3.14.0rc1', '-freethreaded']] + ['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}], + ['3.14.0a4', {version: '3.14.0a4', freethreaded: false}], + ['3.14.0rc1', {version: '3.14.0rc1', freethreaded: false}], + ['3.14.0rc1t', {version: '3.14.0rc1', 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}], + ['3.14.0a4', {version: '3.14.0-alpha.4', freethreaded: false}], + ['3.14.0a4t', {version: '3.14.0-alpha.4', freethreaded: true}], + ['3.14.0rc1', {version: '3.14.0-rc.1', freethreaded: false}], + ['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}] + ])('%s -> %s', (input, expected) => { + const {version, freethreaded} = desugarVersion(input); + let 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}], + ['3.14.0a4', {version: '3.14.0-alpha.4', freethreaded: false}], + ['3.14.0a4t', {version: '3.14.0-alpha.4', freethreaded: true}], + ['3.14.0rc1', {version: '3.14.0-rc.1', freethreaded: false}], + ['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}] + ])('%s (allowPreReleases=true) -> %s', (input, expected) => { + const {version, freethreaded} = desugarVersion(input); + let semanticVersionSpec = pythonVersionToSemantic(version, true); + expect({version: semanticVersionSpec, freethreaded}).toEqual(expected); + }); +}); diff --git a/__tests__/finder.test.ts b/__tests__/finder.test.ts index b1c3f9f1a..285a071c1 100644 --- a/__tests__/finder.test.ts +++ b/__tests__/finder.test.ts @@ -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', @@ -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(); }); @@ -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' @@ -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' @@ -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( @@ -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', @@ -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' @@ -251,17 +265,17 @@ 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(); }); @@ -269,7 +283,14 @@ describe('Finder tests', () => { // 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; } diff --git a/action.yml b/action.yml index 48755e9df..efa8de904 100644 --- a/action.yml +++ b/action.yml @@ -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." diff --git a/dist/setup/index.js b/dist/setup/index.js index a0bed3bf6..d3bf3ead7 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -91034,17 +91034,21 @@ 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, freethreaded] = desugarVersion(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; + architecture += '-freethreaded'; } if (checkLatest) { manifest = yield installer.getManifest(); @@ -91122,27 +91126,33 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest exports.useCpythonVersion = useCpythonVersion; /* Desugar free threaded and dev versions */ function desugarVersion(versionSpec) { - const [desugaredVersionSpec, freethreaded] = desugarFreeThreadedVersion(versionSpec); - const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); - return [desugaredVersionSpec2, freethreaded]; + 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, 3.14.0a1t. * Returns the version without the `t` and the architectures suffix, if freethreaded */ function desugarFreeThreadedVersion(versionSpec) { - const prereleaseVersion = /(\d+\.\d+\.\d+)(t)((?:a|b|rc)\d*)/g; + // e.g., 3.14.0a1t -> 3.14.0a1 + const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)(t)/g; if (prereleaseVersion.test(versionSpec)) { - return [versionSpec.replace(prereleaseVersion, '$1$3'), '-freethreaded']; + return { + version: versionSpec.replace(prereleaseVersion, '$1$2'), + freethreaded: true + }; } const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/; if (majorMinor.test(versionSpec)) { - return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; + return { version: versionSpec.replace(majorMinor, '$1'), freethreaded: true }; } const devVersion = /^(\d+\.\d+)(t)(-dev)$/; if (devVersion.test(versionSpec)) { - return [versionSpec.replace(devVersion, '$1$3'), '-freethreaded']; + return { + version: versionSpec.replace(devVersion, '$1$3'), + freethreaded: true + }; } - return [versionSpec, '']; + return { version: versionSpec, freethreaded: false }; } /** Convert versions like `3.8-dev` to a version like `~3.8.0-0`. */ function desugarDevVersion(versionSpec) { @@ -91157,15 +91167,22 @@ function versionFromPath(installDir) { } /** * Python's prelease versions look like `3.7.0b2`. - * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. + * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-beta.2`. * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. * * For easier use of the action, we also map 'x.y' to allow pre-release before 'x.y.0' release if allowPreReleases is true */ function pythonVersionToSemantic(versionSpec, allowPreReleases) { - const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; + const preleaseMap = { + a: 'alpha', + b: 'beta', + rc: 'rc' + }; + const prereleaseVersion = /(\d+\.\d+\.\d+)(a|b|rc)(\d+)/g; + let result = versionSpec.replace(prereleaseVersion, (_, p1, p2, p3) => { + return `${p1}-${preleaseMap[p2]}.${p3}`; + }); const majorMinor = /^(\d+)\.(\d+)$/; - let result = versionSpec.replace(prereleaseVersion, '$1-$2'); if (allowPreReleases) { result = result.replace(majorMinor, '~$1.$2.0-0'); } @@ -91881,6 +91898,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(); @@ -91901,7 +91919,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})`); } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index dc8665d3d..d6c092c7f 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -77,7 +77,7 @@ steps: - run: python my_script.py ``` -Use the **t** suffix to select the [free threading](https://docs.python.org/3/howto/free-threading-python.html) version of Python. +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`. Free threaded Python is only available starting with the 3.13 release. ```yaml @@ -89,7 +89,17 @@ steps: - run: python my_script.py ``` -Pre-release free threading versions should be specified like `3.14.0ta3` or `3.14t-dev`. +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: diff --git a/src/find-python.ts b/src/find-python.ts index 6685d060d..606c48020 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -35,20 +35,26 @@ export async function useCpythonVersion( architecture: string, updateEnvironment: boolean, checkLatest: boolean, - allowPreReleases: boolean + allowPreReleases: boolean, + freethreaded: boolean ): Promise { let manifest: tc.IToolRelease[] | null = null; - const [desugaredVersionSpec, freethreaded] = desugarVersion(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; + architecture += '-freethreaded'; } if (checkLatest) { @@ -167,28 +173,33 @@ export async function useCpythonVersion( /* Desugar free threaded and dev versions */ export function desugarVersion(versionSpec: string) { - const [desugaredVersionSpec, freethreaded] = - desugarFreeThreadedVersion(versionSpec); - const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); - return [desugaredVersionSpec2, freethreaded]; + const {version, freethreaded} = desugarFreeThreadedVersion(versionSpec); + return {version: desugarDevVersion(version), freethreaded}; } /* Identify freethreaded versions like, 3.13t, 3.13.1t, 3.13t-dev, 3.14.0a1t. * Returns the version without the `t` and the architectures suffix, if freethreaded */ function desugarFreeThreadedVersion(versionSpec: string) { - const prereleaseVersion = /(\d+\.\d+\.\d+)(t)((?:a|b|rc)\d*)/g; + // e.g., 3.14.0a1t -> 3.14.0a1 + const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)(t)/g; if (prereleaseVersion.test(versionSpec)) { - return [versionSpec.replace(prereleaseVersion, '$1$3'), '-freethreaded']; + return { + version: versionSpec.replace(prereleaseVersion, '$1$2'), + freethreaded: true + }; } const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/; if (majorMinor.test(versionSpec)) { - return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; + return {version: versionSpec.replace(majorMinor, '$1'), freethreaded: true}; } const devVersion = /^(\d+\.\d+)(t)(-dev)$/; if (devVersion.test(versionSpec)) { - return [versionSpec.replace(devVersion, '$1$3'), '-freethreaded']; + return { + version: versionSpec.replace(devVersion, '$1$3'), + freethreaded: true + }; } - return [versionSpec, '']; + return {version: versionSpec, freethreaded: false}; } /** Convert versions like `3.8-dev` to a version like `~3.8.0-0`. */ @@ -212,7 +223,7 @@ interface InstalledVersion { /** * Python's prelease versions look like `3.7.0b2`. - * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. + * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-beta.2`. * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. * * For easier use of the action, we also map 'x.y' to allow pre-release before 'x.y.0' release if allowPreReleases is true @@ -221,9 +232,16 @@ export function pythonVersionToSemantic( versionSpec: string, allowPreReleases: boolean ) { - const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; + const preleaseMap: {[key: string]: string} = { + a: 'alpha', + b: 'beta', + rc: 'rc' + }; + const prereleaseVersion = /(\d+\.\d+\.\d+)(a|b|rc)(\d+)/g; + let result = versionSpec.replace(prereleaseVersion, (_, p1, p2, p3) => { + return `${p1}-${preleaseMap[p2]}.${p3}`; + }); const majorMinor = /^(\d+)\.(\d+)$/; - let result = versionSpec.replace(prereleaseVersion, '$1-$2'); if (allowPreReleases) { result = result.replace(majorMinor, '~$1.$2.0-0'); } diff --git a/src/setup-python.ts b/src/setup-python.ts index 0dd45f0ca..ab5931b82 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -92,6 +92,7 @@ async 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 = ''; @@ -132,7 +133,8 @@ async function run() { arch, updateEnvironment, checkLatest, - allowPreReleases + allowPreReleases, + freethreaded ); pythonVersion = installed.version; core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); From 3794e54127a1ed18173be95c864c26c107f67502 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 31 Jan 2025 21:40:26 +0000 Subject: [PATCH 05/10] Fix lint --- __tests__/find-python.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/find-python.test.ts b/__tests__/find-python.test.ts index 6178ceec8..956c5cbdd 100644 --- a/__tests__/find-python.test.ts +++ b/__tests__/find-python.test.ts @@ -31,7 +31,7 @@ describe('pythonVersions', () => { ['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}] ])('%s -> %s', (input, expected) => { const {version, freethreaded} = desugarVersion(input); - let semanticVersionSpec = pythonVersionToSemantic(version, false); + const semanticVersionSpec = pythonVersionToSemantic(version, false); expect({version: semanticVersionSpec, freethreaded}).toEqual(expected); }); @@ -48,7 +48,7 @@ describe('pythonVersions', () => { ['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}] ])('%s (allowPreReleases=true) -> %s', (input, expected) => { const {version, freethreaded} = desugarVersion(input); - let semanticVersionSpec = pythonVersionToSemantic(version, true); + const semanticVersionSpec = pythonVersionToSemantic(version, true); expect({version: semanticVersionSpec, freethreaded}).toEqual(expected); }); }); From 1ec1b0d2bd0e9943cc3a4b6a4e7e82a14fd341c6 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 5 Feb 2025 21:06:09 +0000 Subject: [PATCH 06/10] Add 't' suffix to python-version output --- dist/setup/index.js | 7 ++++++- src/find-python.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index d3bf3ead7..b06b72ead 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -91118,7 +91118,12 @@ 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 }; }); diff --git a/src/find-python.ts b/src/find-python.ts index 606c48020..1cf47fa4a 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -165,7 +165,12 @@ export async function useCpythonVersion( } 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}; From 8f1f09d543282eb63ff4659a4e1aeb146bdade69 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Mon, 10 Feb 2025 17:20:39 +0000 Subject: [PATCH 07/10] Use distinct cache key for free threaded Python --- dist/setup/index.js | 2 +- src/find-python.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index b06b72ead..41a7f5d77 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -91125,7 +91125,7 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest } core.setOutput('python-version', pythonVersion); core.setOutput('python-path', pythonPath); - return { impl: 'CPython', version: installed }; + return { impl: 'CPython', version: pythonVersion }; }); } exports.useCpythonVersion = useCpythonVersion; diff --git a/src/find-python.ts b/src/find-python.ts index 1cf47fa4a..4852cad05 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -173,7 +173,7 @@ export async function useCpythonVersion( core.setOutput('python-version', pythonVersion); core.setOutput('python-path', pythonPath); - return {impl: 'CPython', version: installed}; + return {impl: 'CPython', version: pythonVersion}; } /* Desugar free threaded and dev versions */ From bdd8c56024d7f764c68322c7cef963453dc71a29 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Mon, 10 Feb 2025 18:13:21 +0000 Subject: [PATCH 08/10] Remove support for syntax like '3.14.0a1' --- __tests__/find-python.test.ts | 17 +++-------------- dist/setup/index.js | 23 ++++------------------- docs/advanced-usage.md | 2 +- src/find-python.ts | 23 ++++------------------- 4 files changed, 12 insertions(+), 53 deletions(-) diff --git a/__tests__/find-python.test.ts b/__tests__/find-python.test.ts index 956c5cbdd..c7b827c42 100644 --- a/__tests__/find-python.test.ts +++ b/__tests__/find-python.test.ts @@ -7,10 +7,7 @@ describe('desugarVersion', () => { ['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}], - ['3.14.0a4', {version: '3.14.0a4', freethreaded: false}], - ['3.14.0rc1', {version: '3.14.0rc1', freethreaded: false}], - ['3.14.0rc1t', {version: '3.14.0rc1', freethreaded: true}] + ['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}] ])('%s -> %s', (input, expected) => { expect(desugarVersion(input)).toEqual(expected); }); @@ -24,11 +21,7 @@ describe('pythonVersions', () => { ['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}], - ['3.14.0a4', {version: '3.14.0-alpha.4', freethreaded: false}], - ['3.14.0a4t', {version: '3.14.0-alpha.4', freethreaded: true}], - ['3.14.0rc1', {version: '3.14.0-rc.1', freethreaded: false}], - ['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}] + ['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}] ])('%s -> %s', (input, expected) => { const {version, freethreaded} = desugarVersion(input); const semanticVersionSpec = pythonVersionToSemantic(version, false); @@ -41,11 +34,7 @@ describe('pythonVersions', () => { ['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}], - ['3.14.0a4', {version: '3.14.0-alpha.4', freethreaded: false}], - ['3.14.0a4t', {version: '3.14.0-alpha.4', freethreaded: true}], - ['3.14.0rc1', {version: '3.14.0-rc.1', freethreaded: false}], - ['3.14.0rc1t', {version: '3.14.0-rc.1', freethreaded: true}] + ['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); diff --git a/dist/setup/index.js b/dist/setup/index.js index 41a7f5d77..aada8df6f 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -91135,17 +91135,9 @@ function desugarVersion(versionSpec) { return { version: desugarDevVersion(version), freethreaded }; } exports.desugarVersion = desugarVersion; -/* Identify freethreaded versions like, 3.13t, 3.13.1t, 3.13t-dev, 3.14.0a1t. +/* 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) { - // e.g., 3.14.0a1t -> 3.14.0a1 - const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)(t)/g; - if (prereleaseVersion.test(versionSpec)) { - return { - version: versionSpec.replace(prereleaseVersion, '$1$2'), - freethreaded: true - }; - } const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/; if (majorMinor.test(versionSpec)) { return { version: versionSpec.replace(majorMinor, '$1'), freethreaded: true }; @@ -91172,22 +91164,15 @@ function versionFromPath(installDir) { } /** * Python's prelease versions look like `3.7.0b2`. - * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-beta.2`. + * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. * * For easier use of the action, we also map 'x.y' to allow pre-release before 'x.y.0' release if allowPreReleases is true */ function pythonVersionToSemantic(versionSpec, allowPreReleases) { - const preleaseMap = { - a: 'alpha', - b: 'beta', - rc: 'rc' - }; - const prereleaseVersion = /(\d+\.\d+\.\d+)(a|b|rc)(\d+)/g; - let result = versionSpec.replace(prereleaseVersion, (_, p1, p2, p3) => { - return `${p1}-${preleaseMap[p2]}.${p3}`; - }); + const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; const majorMinor = /^(\d+)\.(\d+)$/; + let result = versionSpec.replace(prereleaseVersion, '$1-$2'); if (allowPreReleases) { result = result.replace(majorMinor, '~$1.$2.0-0'); } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index d6c092c7f..0958f5a74 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -77,7 +77,7 @@ 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. Pre-release free threading versions can be specified like `3.14.0a3t` or `3.14t-dev`. +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.14t-dev`. Free threaded Python is only available starting with the 3.13 release. ```yaml diff --git a/src/find-python.ts b/src/find-python.ts index 4852cad05..e50ded1f0 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -182,17 +182,9 @@ export function desugarVersion(versionSpec: string) { return {version: desugarDevVersion(version), freethreaded}; } -/* Identify freethreaded versions like, 3.13t, 3.13.1t, 3.13t-dev, 3.14.0a1t. +/* 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: string) { - // e.g., 3.14.0a1t -> 3.14.0a1 - const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)(t)/g; - if (prereleaseVersion.test(versionSpec)) { - return { - version: versionSpec.replace(prereleaseVersion, '$1$2'), - freethreaded: true - }; - } const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/; if (majorMinor.test(versionSpec)) { return {version: versionSpec.replace(majorMinor, '$1'), freethreaded: true}; @@ -228,7 +220,7 @@ interface InstalledVersion { /** * Python's prelease versions look like `3.7.0b2`. - * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-beta.2`. + * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. * * For easier use of the action, we also map 'x.y' to allow pre-release before 'x.y.0' release if allowPreReleases is true @@ -237,16 +229,9 @@ export function pythonVersionToSemantic( versionSpec: string, allowPreReleases: boolean ) { - const preleaseMap: {[key: string]: string} = { - a: 'alpha', - b: 'beta', - rc: 'rc' - }; - const prereleaseVersion = /(\d+\.\d+\.\d+)(a|b|rc)(\d+)/g; - let result = versionSpec.replace(prereleaseVersion, (_, p1, p2, p3) => { - return `${p1}-${preleaseMap[p2]}.${p3}`; - }); + const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; const majorMinor = /^(\d+)\.(\d+)$/; + let result = versionSpec.replace(prereleaseVersion, '$1-$2'); if (allowPreReleases) { result = result.replace(majorMinor, '~$1.$2.0-0'); } From 197140633cfdb2ba40d732838196edcd39e10e94 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 14 Feb 2025 17:17:53 +0000 Subject: [PATCH 09/10] Clarify use of 't' suffix --- docs/advanced-usage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 0958f5a74..1db617c34 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -77,7 +77,8 @@ 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. Pre-release free threading versions can be specified like `3.14t-dev`. +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 From 0681a6bff7f9a0a6cc043c5c724f8e8d57728b0e Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 14 Feb 2025 17:26:31 +0000 Subject: [PATCH 10/10] Improve error message when trying to use free threaded Python versions before 3.13 --- dist/setup/index.js | 12 ++++++++---- src/find-python.ts | 24 +++++++++++++++--------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index aada8df6f..a1b94a316 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -91073,12 +91073,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' : ''; diff --git a/src/find-python.ts b/src/find-python.ts index e50ded1f0..ddb027cb7 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -102,16 +102,22 @@ export async function useCpythonVersion( if (!installDir) { const osInfo = await getOSInfo(); - throw new Error( - [ - `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) + const msg = [ + `The version '${version}' with architecture '${architecture}' was not found for ${ + osInfo + ? `${osInfo.osName} ${osInfo.osVersion}` + : '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);