Skip to content
79 changes: 78 additions & 1 deletion __tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
getNextPageUrl,
isGhes,
IS_WINDOWS,
getDownloadFileName
getDownloadFileName,
getVersionInputFromToolVersions
} from '../src/utils';

jest.mock('@actions/cache');
Expand Down Expand Up @@ -139,6 +140,82 @@ describe('Version from file test', () => {
expect(_fn(pythonVersionFilePath)).toEqual([]);
}
);
it.each([getVersionInputFromToolVersions])(
'Version from .tool-versions',
async _fn => {
const toolVersionFileName = '.tool-versions';
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
const toolVersionContent = 'python 3.9.10\nnodejs 16';
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']);
}
);

it.each([getVersionInputFromToolVersions])(
'Version from .tool-versions with comment',
async _fn => {
const toolVersionFileName = '.tool-versions';
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
const toolVersionContent = '# python 3.8\npython 3.9';
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
expect(_fn(toolVersionFilePath)).toEqual(['3.9']);
}
);

it.each([getVersionInputFromToolVersions])(
'Version from .tool-versions with whitespace',
async _fn => {
const toolVersionFileName = '.tool-versions';
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
const toolVersionContent = ' python 3.10 ';
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
expect(_fn(toolVersionFilePath)).toEqual(['3.10']);
}
);

it.each([getVersionInputFromToolVersions])(
'Version from .tool-versions with v prefix',
async _fn => {
const toolVersionFileName = '.tool-versions';
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
const toolVersionContent = 'python v3.9.10';
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']);
}
);

it.each([getVersionInputFromToolVersions])(
'Version from .tool-versions with v prefix',
async _fn => {
const toolVersionFileName = '.tool-versions';
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
const toolVersionContent = 'python pypy3.10-7.3.14';
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
expect(_fn(toolVersionFilePath)).toEqual(['pypy3.10-7.3.14']);
}
);

it.each([getVersionInputFromToolVersions])(
'Version from .tool-versions with v prefix',
async _fn => {
const toolVersionFileName = '.tool-versions';
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
const toolVersionContent = 'python 3.14.0a5t';
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
expect(_fn(toolVersionFilePath)).toEqual(['3.14.0a5t']);
}
);

it.each([getVersionInputFromToolVersions])(
'Version from .tool-versions with v prefix',
async _fn => {
const toolVersionFileName = '.tool-versions';
const toolVersionFilePath = path.join(tempDir, toolVersionFileName);
const toolVersionContent = 'python 3.14t-dev';
fs.writeFileSync(toolVersionFilePath, toolVersionContent);
expect(_fn(toolVersionFilePath)).toEqual(['3.14t-dev']);
}
);
});

describe('getNextPageUrl', () => {
Expand Down
41 changes: 39 additions & 2 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100453,7 +100453,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
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;
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;
/* eslint no-unsafe-finally: "off" */
const cache = __importStar(__nccwpck_require__(5116));
const core = __importStar(__nccwpck_require__(7484));
Expand Down Expand Up @@ -100677,12 +100677,49 @@ function getVersionInputFromPlainFile(versionFile) {
}
exports.getVersionInputFromPlainFile = getVersionInputFromPlainFile;
/**
* Python version extracted from a plain or TOML file.
* Python version extracted from a .tool-versions file.
*/
function getVersionInputFromToolVersions(versionFile) {
var _a;
if (!fs_1.default.existsSync(versionFile)) {
core.warning(`File ${versionFile} does not exist.`);
return [];
}
try {
const fileContents = fs_1.default.readFileSync(versionFile, 'utf8');
const lines = fileContents.split('\n');
const versions = [];
for (const line of lines) {
// Skip commented lines
if (line.trim().startsWith('#')) {
continue;
}
const match = line.match(/^\s*python\s*v?(?<version>[^\s]+(?:\s*[-<>=!]+[^\s]+)*)\s*(-\s([^\s].*))?\s*$/);
if (match) {
return [((_a = match.groups) === null || _a === void 0 ? void 0 : _a.version.trim()) || ''];
}
}
if (versions.length === 0) {
core.warning(`No Python version found in ${versionFile}`);
}
return versions;
}
catch (error) {
core.error(`Error reading ${versionFile}: ${error.message}`);
return [];
}
}
exports.getVersionInputFromToolVersions = getVersionInputFromToolVersions;
/**
* Python version extracted from a plain, .tool-versions or TOML file.
*/
function getVersionInputFromFile(versionFile) {
if (versionFile.endsWith('.toml')) {
return getVersionInputFromTomlFile(versionFile);
}
else if (versionFile.match('.tool-versions')) {
return getVersionInputFromToolVersions(versionFile);
}
else {
return getVersionInputFromPlainFile(versionFile);
}
Expand Down
13 changes: 11 additions & 2 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,9 @@ jobs:

## Using the `python-version-file` input

`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.
`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.

>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.
>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)).

```yaml
steps:
Expand All @@ -275,6 +275,15 @@ steps:
- run: python my_script.py
```

```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: '.tool-versions' # Read python version from a file .tool-versions
- run: python my_script.py
```

## Check latest version

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.
Expand Down
41 changes: 40 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,50 @@
}

/**
* Python version extracted from a plain or TOML file.
* Python version extracted from a .tool-versions file.
*/
export function getVersionInputFromToolVersions(versionFile: string): string[] {
if (!fs.existsSync(versionFile)) {
core.warning(`File ${versionFile} does not exist.`);
return [];
}

try {
const fileContents = fs.readFileSync(versionFile, 'utf8');
const lines = fileContents.split('\n');
const versions: string[] = [];

for (const line of lines) {
// Skip commented lines
if (line.trim().startsWith('#')) {
continue;
}
const match = line.match(
/^\s*python\s*v?(?<version>[^\s]+(?:\s*[-<>=!]+[^\s]+)*)\s*(-\s([^\s].*))?\s*$/
);
if (match) {
return [match.groups?.version.trim() || ''];
}
}

if (versions.length === 0) {
core.warning(`No Python version found in ${versionFile}`);
}

return versions;
} catch (error) {
core.error(`Error reading ${versionFile}: ${(error as Error).message}`);
return [];
}
}
/**
* Python version extracted from a plain, .tool-versions or TOML file.
*/
export function getVersionInputFromFile(versionFile: string): string[] {
if (versionFile.endsWith('.toml')) {
return getVersionInputFromTomlFile(versionFile);
} else if (versionFile.match('.tool-versions')) {
return getVersionInputFromToolVersions(versionFile);
} else {
return getVersionInputFromPlainFile(versionFile);
}
Expand Down