Skip to content

Commit 19e4675

Browse files
Add support for .tool-versions file in setup-python (#1043)
* add support for .tool-versions file * update regex * optimize code * update test-python.yml for .tool-versions * fix format-check errors * fix formatting in test-python.yml * Fix test-python.yml error * workflow update with latest versions * update test cases * fix lint issue
1 parent 6fd11e1 commit 19e4675

File tree

5 files changed

+193
-6
lines changed

5 files changed

+193
-6
lines changed

.github/workflows/test-python.yml

+33
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,39 @@ jobs:
245245
- name: Run simple code
246246
run: python -c 'import math; print(math.factorial(5))'
247247

248+
setup-versions-from-tool-versions-file:
249+
name: Setup ${{ matrix.python }} ${{ matrix.os }} .tool-versions file
250+
runs-on: ${{ matrix.os }}
251+
strategy:
252+
fail-fast: false
253+
matrix:
254+
os:
255+
[
256+
macos-latest,
257+
windows-latest,
258+
ubuntu-20.04,
259+
ubuntu-22.04,
260+
macos-13,
261+
ubuntu-latest
262+
]
263+
python: [3.13.0, 3.14-dev, pypy3.11-7.3.18, graalpy-24.1.2]
264+
exclude:
265+
- os: windows-latest
266+
python: graalpy-24.1.2
267+
steps:
268+
- name: Checkout
269+
uses: actions/checkout@v4
270+
271+
- name: build-tool-versions-file ${{ matrix.python }}
272+
run: |
273+
echo "python ${{ matrix.python }}" > .tool-versions
274+
275+
- name: setup-python using .tool-versions ${{ matrix.python }}
276+
id: setup-python-tool-versions
277+
uses: ./
278+
with:
279+
python-version-file: .tool-versions
280+
248281
setup-pre-release-version-from-manifest:
249282
name: Setup 3.14.0-alpha.1 ${{ matrix.os }}
250283
runs-on: ${{ matrix.os }}

__tests__/utils.test.ts

+78-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
getNextPageUrl,
1616
isGhes,
1717
IS_WINDOWS,
18-
getDownloadFileName
18+
getDownloadFileName,
19+
getVersionInputFromToolVersions
1920
} from '../src/utils';
2021

2122
jest.mock('@actions/cache');
@@ -139,6 +140,82 @@ describe('Version from file test', () => {
139140
expect(_fn(pythonVersionFilePath)).toEqual([]);
140141
}
141142
);
143+
it.each([getVersionInputFromToolVersions])(
144+
'Version from .tool-versions',
145+
async _fn => {
146+
const toolVersionFileName = '.tool-versions';
147+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
148+
const toolVersionContent = 'python 3.9.10\nnodejs 16';
149+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
150+
expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']);
151+
}
152+
);
153+
154+
it.each([getVersionInputFromToolVersions])(
155+
'Version from .tool-versions with comment',
156+
async _fn => {
157+
const toolVersionFileName = '.tool-versions';
158+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
159+
const toolVersionContent = '# python 3.8\npython 3.9';
160+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
161+
expect(_fn(toolVersionFilePath)).toEqual(['3.9']);
162+
}
163+
);
164+
165+
it.each([getVersionInputFromToolVersions])(
166+
'Version from .tool-versions with whitespace',
167+
async _fn => {
168+
const toolVersionFileName = '.tool-versions';
169+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
170+
const toolVersionContent = ' python 3.10 ';
171+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
172+
expect(_fn(toolVersionFilePath)).toEqual(['3.10']);
173+
}
174+
);
175+
176+
it.each([getVersionInputFromToolVersions])(
177+
'Version from .tool-versions with v prefix',
178+
async _fn => {
179+
const toolVersionFileName = '.tool-versions';
180+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
181+
const toolVersionContent = 'python v3.9.10';
182+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
183+
expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']);
184+
}
185+
);
186+
187+
it.each([getVersionInputFromToolVersions])(
188+
'Version from .tool-versions with pypy version',
189+
async _fn => {
190+
const toolVersionFileName = '.tool-versions';
191+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
192+
const toolVersionContent = 'python pypy3.10-7.3.14';
193+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
194+
expect(_fn(toolVersionFilePath)).toEqual(['pypy3.10-7.3.14']);
195+
}
196+
);
197+
198+
it.each([getVersionInputFromToolVersions])(
199+
'Version from .tool-versions with alpha Releases',
200+
async _fn => {
201+
const toolVersionFileName = '.tool-versions';
202+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
203+
const toolVersionContent = 'python 3.14.0a5t';
204+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
205+
expect(_fn(toolVersionFilePath)).toEqual(['3.14.0a5t']);
206+
}
207+
);
208+
209+
it.each([getVersionInputFromToolVersions])(
210+
'Version from .tool-versions with dev suffix',
211+
async _fn => {
212+
const toolVersionFileName = '.tool-versions';
213+
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
214+
const toolVersionContent = 'python 3.14t-dev';
215+
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
216+
expect(_fn(toolVersionFilePath)).toEqual(['3.14t-dev']);
217+
}
218+
);
142219
});
143220

144221
describe('getNextPageUrl', () => {

dist/setup/index.js

+36-2
Original file line numberDiff line numberDiff line change
@@ -100535,7 +100535,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
100535100535
return (mod && mod.__esModule) ? mod : { "default": mod };
100536100536
};
100537100537
Object.defineProperty(exports, "__esModule", ({ value: true }));
100538-
exports.getDownloadFileName = exports.getNextPageUrl = exports.getBinaryDirectory = exports.getVersionInputFromFile = exports.getVersionInputFromPlainFile = exports.getVersionInputFromTomlFile = exports.getOSInfo = exports.getLinuxInfo = exports.logWarning = exports.isCacheFeatureAvailable = exports.isGhes = exports.validatePythonVersionFormatForPyPy = exports.writeExactPyPyVersionFile = exports.readExactPyPyVersionFile = exports.getPyPyVersionFromPath = exports.isNightlyKeyword = exports.validateVersion = exports.createSymlinkInFolder = exports.WINDOWS_PLATFORMS = exports.WINDOWS_ARCHS = exports.IS_MAC = exports.IS_LINUX = exports.IS_WINDOWS = void 0;
100538+
exports.getDownloadFileName = exports.getNextPageUrl = exports.getBinaryDirectory = exports.getVersionInputFromFile = exports.getVersionInputFromToolVersions = exports.getVersionInputFromPlainFile = exports.getVersionInputFromTomlFile = exports.getOSInfo = exports.getLinuxInfo = exports.logWarning = exports.isCacheFeatureAvailable = exports.isGhes = exports.validatePythonVersionFormatForPyPy = exports.writeExactPyPyVersionFile = exports.readExactPyPyVersionFile = exports.getPyPyVersionFromPath = exports.isNightlyKeyword = exports.validateVersion = exports.createSymlinkInFolder = exports.WINDOWS_PLATFORMS = exports.WINDOWS_ARCHS = exports.IS_MAC = exports.IS_LINUX = exports.IS_WINDOWS = void 0;
100539100539
/* eslint no-unsafe-finally: "off" */
100540100540
const cache = __importStar(__nccwpck_require__(5116));
100541100541
const core = __importStar(__nccwpck_require__(7484));
@@ -100759,12 +100759,46 @@ function getVersionInputFromPlainFile(versionFile) {
100759100759
}
100760100760
exports.getVersionInputFromPlainFile = getVersionInputFromPlainFile;
100761100761
/**
100762-
* Python version extracted from a plain or TOML file.
100762+
* Python version extracted from a .tool-versions file.
100763+
*/
100764+
function getVersionInputFromToolVersions(versionFile) {
100765+
var _a;
100766+
if (!fs_1.default.existsSync(versionFile)) {
100767+
core.warning(`File ${versionFile} does not exist.`);
100768+
return [];
100769+
}
100770+
try {
100771+
const fileContents = fs_1.default.readFileSync(versionFile, 'utf8');
100772+
const lines = fileContents.split('\n');
100773+
for (const line of lines) {
100774+
// Skip commented lines
100775+
if (line.trim().startsWith('#')) {
100776+
continue;
100777+
}
100778+
const match = line.match(/^\s*python\s*v?\s*(?<version>[^\s]+)\s*$/);
100779+
if (match) {
100780+
return [((_a = match.groups) === null || _a === void 0 ? void 0 : _a.version.trim()) || ''];
100781+
}
100782+
}
100783+
core.warning(`No Python version found in ${versionFile}`);
100784+
return [];
100785+
}
100786+
catch (error) {
100787+
core.error(`Error reading ${versionFile}: ${error.message}`);
100788+
return [];
100789+
}
100790+
}
100791+
exports.getVersionInputFromToolVersions = getVersionInputFromToolVersions;
100792+
/**
100793+
* Python version extracted from a plain, .tool-versions or TOML file.
100763100794
*/
100764100795
function getVersionInputFromFile(versionFile) {
100765100796
if (versionFile.endsWith('.toml')) {
100766100797
return getVersionInputFromTomlFile(versionFile);
100767100798
}
100799+
else if (versionFile.match('.tool-versions')) {
100800+
return getVersionInputFromToolVersions(versionFile);
100801+
}
100768100802
else {
100769100803
return getVersionInputFromPlainFile(versionFile);
100770100804
}

docs/advanced-usage.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,9 @@ jobs:
278278
279279
## Using the `python-version-file` input
280280

281-
`setup-python` action can read the Python or PyPy version from a version file. `python-version-file` input is used to specify the path to the version file. If the file that was supplied to `python-version-file` input doesn't exist, the action will fail with an error.
281+
`setup-python` action can read Python or PyPy version from a version file. `python-version-file` input is used for specifying the path to the version file. If the file that was supplied to `python-version-file` input doesn't exist, the action will fail with error.
282282

283-
>In case both `python-version` and `python-version-file` inputs are supplied, the `python-version-file` input will be ignored due to its lower priority.
283+
>In case both `python-version` and `python-version-file` inputs are supplied, the `python-version-file` input will be ignored due to its lower priority. The .tool-versions file supports version specifications in accordance with asdf standards, adhering to Semantic Versioning ([semver](https://semver.org)).
284284

285285
```yaml
286286
steps:
@@ -300,6 +300,15 @@ steps:
300300
- run: python my_script.py
301301
```
302302

303+
```yaml
304+
steps:
305+
- uses: actions/checkout@v4
306+
- uses: actions/setup-python@v5
307+
with:
308+
python-version-file: '.tool-versions' # Read python version from a file .tool-versions
309+
- run: python my_script.py
310+
```
311+
303312
## Check latest version
304313

305314
The `check-latest` flag defaults to `false`. Use the default or set `check-latest` to `false` if you prefer stability and if you want to ensure a specific `Python or PyPy` version is always used.

src/utils.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,45 @@ export function getVersionInputFromPlainFile(versionFile: string): string[] {
279279
}
280280

281281
/**
282-
* Python version extracted from a plain or TOML file.
282+
* Python version extracted from a .tool-versions file.
283+
*/
284+
export function getVersionInputFromToolVersions(versionFile: string): string[] {
285+
if (!fs.existsSync(versionFile)) {
286+
core.warning(`File ${versionFile} does not exist.`);
287+
return [];
288+
}
289+
290+
try {
291+
const fileContents = fs.readFileSync(versionFile, 'utf8');
292+
const lines = fileContents.split('\n');
293+
294+
for (const line of lines) {
295+
// Skip commented lines
296+
if (line.trim().startsWith('#')) {
297+
continue;
298+
}
299+
const match = line.match(/^\s*python\s*v?\s*(?<version>[^\s]+)\s*$/);
300+
if (match) {
301+
return [match.groups?.version.trim() || ''];
302+
}
303+
}
304+
305+
core.warning(`No Python version found in ${versionFile}`);
306+
307+
return [];
308+
} catch (error) {
309+
core.error(`Error reading ${versionFile}: ${(error as Error).message}`);
310+
return [];
311+
}
312+
}
313+
/**
314+
* Python version extracted from a plain, .tool-versions or TOML file.
283315
*/
284316
export function getVersionInputFromFile(versionFile: string): string[] {
285317
if (versionFile.endsWith('.toml')) {
286318
return getVersionInputFromTomlFile(versionFile);
319+
} else if (versionFile.match('.tool-versions')) {
320+
return getVersionInputFromToolVersions(versionFile);
287321
} else {
288322
return getVersionInputFromPlainFile(versionFile);
289323
}

0 commit comments

Comments
 (0)