Skip to content

Commit 19e4675

Browse files
Add support for .tool-versions file in setup-python (actions#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

Lines changed: 33 additions & 0 deletions
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

Lines changed: 78 additions & 1 deletion
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

Lines changed: 36 additions & 2 deletions
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

Lines changed: 11 additions & 2 deletions
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

Lines changed: 35 additions & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy