Add freethreaded input and fix handling of prerelease versions

This commit is contained in:
Sam Gross 2025-01-30 19:46:04 +00:00
parent d653c0b66e
commit 72902a03bd
7 changed files with 163 additions and 55 deletions

View File

@ -1,18 +1,54 @@
import {desugarVersion} from '../src/find-python'; import {desugarVersion, pythonVersionToSemantic} from '../src/find-python';
describe('desugarVersion', () => { describe('desugarVersion', () => {
it.each([ it.each([
['3.13', ['3.13', '']], ['3.13', {version: '3.13', freethreaded: false}],
['3.13t', ['3.13', '-freethreaded']], ['3.13t', {version: '3.13', freethreaded: true}],
['3.13.1', ['3.13.1', '']], ['3.13.1', {version: '3.13.1', freethreaded: false}],
['3.13.1t', ['3.13.1', '-freethreaded']], ['3.13.1t', {version: '3.13.1', freethreaded: true}],
['3.14-dev', ['~3.14.0-0', '']], ['3.14-dev', {version: '~3.14.0-0', freethreaded: false}],
['3.14t-dev', ['~3.14.0-0', '-freethreaded']], ['3.14t-dev', {version: '~3.14.0-0', freethreaded: true}],
['3.14.0a4', ['3.14.0a4', '']], ['3.14.0a4', {version: '3.14.0a4', freethreaded: false}],
['3.14.0ta4', ['3.14.0a4', '-freethreaded']], ['3.14.0rc1', {version: '3.14.0rc1', freethreaded: false}],
['3.14.0rc1', ['3.14.0rc1', '']], ['3.14.0rc1t', {version: '3.14.0rc1', freethreaded: true}]
['3.14.0trc1', ['3.14.0rc1', '-freethreaded']]
])('%s -> %s', (input, expected) => { ])('%s -> %s', (input, expected) => {
expect(desugarVersion(input)).toEqual(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);
});
});

View File

@ -56,7 +56,7 @@ describe('Finder tests', () => {
await io.mkdirP(pythonDir); await io.mkdirP(pythonDir);
fs.writeFileSync(`${pythonDir}.complete`, 'hello'); 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) // 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(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith( expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation', 'pythonLocation',
@ -73,7 +73,7 @@ describe('Finder tests', () => {
await io.mkdirP(pythonDir); await io.mkdirP(pythonDir);
fs.writeFileSync(`${pythonDir}.complete`, 'hello'); 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) // 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(spyCoreAddPath).not.toHaveBeenCalled();
expect(spyCoreExportVariable).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) // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await expect( await expect(
finder.useCpythonVersion('1.2.3', 'x64', true, false, false) finder.useCpythonVersion('1.2.3', 'x64', true, false, false, false)
).resolves.toEqual({ ).resolves.toEqual({
impl: 'CPython', impl: 'CPython',
version: '1.2.3' 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) // This will throw if it doesn't find it in the manifest (because no such version exists)
await expect( 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({ ).resolves.toEqual({
impl: 'CPython', impl: 'CPython',
version: '1.2.4-beta.2' version: '1.2.4-beta.2'
@ -186,7 +193,7 @@ describe('Finder tests', () => {
fs.writeFileSync(`${pythonDir}.complete`, 'hello'); 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) // 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("Resolved as '1.2.3'");
expect(infoSpy).toHaveBeenCalledWith( expect(infoSpy).toHaveBeenCalledWith(
@ -197,7 +204,14 @@ describe('Finder tests', () => {
); );
expect(installSpy).toHaveBeenCalled(); expect(installSpy).toHaveBeenCalled();
expect(addPathSpy).toHaveBeenCalledWith(expPath); 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(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith( expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation', '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) // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await expect( await expect(
finder.useCpythonVersion('1.2', 'x64', false, false, false) finder.useCpythonVersion('1.2', 'x64', false, false, false, false)
).resolves.toEqual({ ).resolves.toEqual({
impl: 'CPython', impl: 'CPython',
version: '1.2.3' 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) // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await expect( await expect(
finder.useCpythonVersion('1.1', 'x64', false, false, false) finder.useCpythonVersion('1.1', 'x64', false, false, false, false)
).rejects.toThrow(); ).rejects.toThrow();
await expect( await expect(
finder.useCpythonVersion('1.1', 'x64', false, false, true) finder.useCpythonVersion('1.1', 'x64', false, false, true, false)
).resolves.toEqual({ ).resolves.toEqual({
impl: 'CPython', impl: 'CPython',
version: '1.1.0-beta.2' version: '1.1.0-beta.2'
}); });
// Check 1.1.0 version specifier does not fallback to '1.1.0-beta.2' // Check 1.1.0 version specifier does not fallback to '1.1.0-beta.2'
await expect( await expect(
finder.useCpythonVersion('1.1.0', 'x64', false, false, true) finder.useCpythonVersion('1.1.0', 'x64', false, false, true, false)
).rejects.toThrow(); ).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) // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
let thrown = false; let thrown = false;
try { try {
await finder.useCpythonVersion('3.300000', 'x64', true, false, false); await finder.useCpythonVersion(
'3.300000',
'x64',
true,
false,
false,
false
);
} catch { } catch {
thrown = true; thrown = true;
} }

View File

@ -26,6 +26,9 @@ inputs:
allow-prereleases: 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." 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 default: false
freethreaded:
description: "When 'true', use the freethreaded version of Python."
default: false
outputs: outputs:
python-version: python-version:
description: "The installed Python or PyPy version. Useful when given a version range as input." description: "The installed Python or PyPy version. Useful when given a version range as input."

48
dist/setup/index.js vendored
View File

@ -91034,17 +91034,21 @@ function binDir(installDir) {
return path.join(installDir, 'bin'); 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* () { return __awaiter(this, void 0, void 0, function* () {
var _a; var _a;
let manifest = null; let manifest = null;
const [desugaredVersionSpec, freethreaded] = desugarVersion(version); const { version: desugaredVersionSpec, freethreaded: versionFreethreaded } = desugarVersion(version);
let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec, allowPreReleases); 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}`); core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`);
if (freethreaded) { if (freethreaded) {
// Free threaded versions use an architecture suffix like `x64-freethreaded` // Free threaded versions use an architecture suffix like `x64-freethreaded`
core.debug(`Using freethreaded version of ${semanticVersionSpec}`); core.debug(`Using freethreaded version of ${semanticVersionSpec}`);
architecture += freethreaded; architecture += '-freethreaded';
} }
if (checkLatest) { if (checkLatest) {
manifest = yield installer.getManifest(); manifest = yield installer.getManifest();
@ -91122,27 +91126,33 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest
exports.useCpythonVersion = useCpythonVersion; exports.useCpythonVersion = useCpythonVersion;
/* Desugar free threaded and dev versions */ /* Desugar free threaded and dev versions */
function desugarVersion(versionSpec) { function desugarVersion(versionSpec) {
const [desugaredVersionSpec, freethreaded] = desugarFreeThreadedVersion(versionSpec); const { version, freethreaded } = desugarFreeThreadedVersion(versionSpec);
const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec); return { version: desugarDevVersion(version), freethreaded };
return [desugaredVersionSpec2, freethreaded];
} }
exports.desugarVersion = desugarVersion; 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, 3.14.0a1t.
* Returns the version without the `t` and the architectures suffix, if freethreaded */ * Returns the version without the `t` and the architectures suffix, if freethreaded */
function desugarFreeThreadedVersion(versionSpec) { 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)) { 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)$/; const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/;
if (majorMinor.test(versionSpec)) { if (majorMinor.test(versionSpec)) {
return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; return { version: versionSpec.replace(majorMinor, '$1'), freethreaded: true };
} }
const devVersion = /^(\d+\.\d+)(t)(-dev)$/; const devVersion = /^(\d+\.\d+)(t)(-dev)$/;
if (devVersion.test(versionSpec)) { 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`. */ /** Convert versions like `3.8-dev` to a version like `~3.8.0-0`. */
function desugarDevVersion(versionSpec) { function desugarDevVersion(versionSpec) {
@ -91157,15 +91167,22 @@ function versionFromPath(installDir) {
} }
/** /**
* Python's prelease versions look like `3.7.0b2`. * 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. * 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 * 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) { 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+)$/; const majorMinor = /^(\d+)\.(\d+)$/;
let result = versionSpec.replace(prereleaseVersion, '$1-$2');
if (allowPreReleases) { if (allowPreReleases) {
result = result.replace(majorMinor, '~$1.$2.0-0'); result = result.replace(majorMinor, '~$1.$2.0-0');
} }
@ -91881,6 +91898,7 @@ function run() {
const versions = resolveVersionInput(); const versions = resolveVersionInput();
const checkLatest = core.getBooleanInput('check-latest'); const checkLatest = core.getBooleanInput('check-latest');
const allowPreReleases = core.getBooleanInput('allow-prereleases'); const allowPreReleases = core.getBooleanInput('allow-prereleases');
const freethreaded = core.getBooleanInput('freethreaded');
if (versions.length) { if (versions.length) {
let pythonVersion = ''; let pythonVersion = '';
const arch = core.getInput('architecture') || os.arch(); const arch = core.getInput('architecture') || os.arch();
@ -91901,7 +91919,7 @@ function run() {
if (version.startsWith('2')) { 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'); 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; pythonVersion = installed.version;
core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); core.info(`Successfully set up ${installed.impl} (${pythonVersion})`);
} }

View File

@ -77,7 +77,7 @@ steps:
- run: python my_script.py - 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. Free threaded Python is only available starting with the 3.13 release.
```yaml ```yaml
@ -89,7 +89,17 @@ steps:
- run: python my_script.py - 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: You can also use several types of ranges that are specified in [semver](https://github.com/npm/node-semver#ranges), for instance:

View File

@ -35,20 +35,26 @@ export async function useCpythonVersion(
architecture: string, architecture: string,
updateEnvironment: boolean, updateEnvironment: boolean,
checkLatest: boolean, checkLatest: boolean,
allowPreReleases: boolean allowPreReleases: boolean,
freethreaded: boolean
): Promise<InstalledVersion> { ): Promise<InstalledVersion> {
let manifest: tc.IToolRelease[] | null = null; let manifest: tc.IToolRelease[] | null = null;
const [desugaredVersionSpec, freethreaded] = desugarVersion(version); const {version: desugaredVersionSpec, freethreaded: versionFreethreaded} =
desugarVersion(version);
let semanticVersionSpec = pythonVersionToSemantic( let semanticVersionSpec = pythonVersionToSemantic(
desugaredVersionSpec, desugaredVersionSpec,
allowPreReleases 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}`); core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`);
if (freethreaded) { if (freethreaded) {
// Free threaded versions use an architecture suffix like `x64-freethreaded` // Free threaded versions use an architecture suffix like `x64-freethreaded`
core.debug(`Using freethreaded version of ${semanticVersionSpec}`); core.debug(`Using freethreaded version of ${semanticVersionSpec}`);
architecture += freethreaded; architecture += '-freethreaded';
} }
if (checkLatest) { if (checkLatest) {
@ -167,28 +173,33 @@ export async function useCpythonVersion(
/* Desugar free threaded and dev versions */ /* Desugar free threaded and dev versions */
export function desugarVersion(versionSpec: string) { export function desugarVersion(versionSpec: string) {
const [desugaredVersionSpec, freethreaded] = const {version, freethreaded} = desugarFreeThreadedVersion(versionSpec);
desugarFreeThreadedVersion(versionSpec); return {version: desugarDevVersion(version), freethreaded};
const desugaredVersionSpec2 = desugarDevVersion(desugaredVersionSpec);
return [desugaredVersionSpec2, 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, 3.14.0a1t.
* Returns the version without the `t` and the architectures suffix, if freethreaded */ * Returns the version without the `t` and the architectures suffix, if freethreaded */
function desugarFreeThreadedVersion(versionSpec: string) { 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)) { 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)$/; const majorMinor = /^(\d+\.\d+(\.\d+)?)(t)$/;
if (majorMinor.test(versionSpec)) { if (majorMinor.test(versionSpec)) {
return [versionSpec.replace(majorMinor, '$1'), '-freethreaded']; return {version: versionSpec.replace(majorMinor, '$1'), freethreaded: true};
} }
const devVersion = /^(\d+\.\d+)(t)(-dev)$/; const devVersion = /^(\d+\.\d+)(t)(-dev)$/;
if (devVersion.test(versionSpec)) { 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`. */ /** 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`. * 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. * 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 * 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, versionSpec: string,
allowPreReleases: boolean 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+)$/; const majorMinor = /^(\d+)\.(\d+)$/;
let result = versionSpec.replace(prereleaseVersion, '$1-$2');
if (allowPreReleases) { if (allowPreReleases) {
result = result.replace(majorMinor, '~$1.$2.0-0'); result = result.replace(majorMinor, '~$1.$2.0-0');
} }

View File

@ -92,6 +92,7 @@ async function run() {
const versions = resolveVersionInput(); const versions = resolveVersionInput();
const checkLatest = core.getBooleanInput('check-latest'); const checkLatest = core.getBooleanInput('check-latest');
const allowPreReleases = core.getBooleanInput('allow-prereleases'); const allowPreReleases = core.getBooleanInput('allow-prereleases');
const freethreaded = core.getBooleanInput('freethreaded');
if (versions.length) { if (versions.length) {
let pythonVersion = ''; let pythonVersion = '';
@ -132,7 +133,8 @@ async function run() {
arch, arch,
updateEnvironment, updateEnvironment,
checkLatest, checkLatest,
allowPreReleases allowPreReleases,
freethreaded
); );
pythonVersion = installed.version; pythonVersion = installed.version;
core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); core.info(`Successfully set up ${installed.impl} (${pythonVersion})`);