Skip to content

Commit

Permalink
Merge pull request #8 from colesbury/quansight
Browse files Browse the repository at this point in the history
Fix handling of prerelease and major.minor.patch versions
  • Loading branch information
andfoy authored Feb 3, 2025
2 parents bcd8245 + c19e886 commit ecd68b5
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 55 deletions.
58 changes: 47 additions & 11 deletions __tests__/find-python.test.ts
Original file line number Diff line number Diff line change
@@ -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);
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}],
['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);
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
48 changes: 33 additions & 15 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand All @@ -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');
}
Expand Down Expand Up @@ -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();
Expand All @@ -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})`);
}
Expand Down
14 changes: 12 additions & 2 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down
Loading

0 comments on commit ecd68b5

Please sign in to comment.