From 19e4675e06535f6b54e894da5c1f044400bb4996 Mon Sep 17 00:00:00 2001 From: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com> Date: Thu, 13 Mar 2025 20:51:27 +0530 Subject: [PATCH] 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 --- .github/workflows/test-python.yml | 33 +++++++++++++ __tests__/utils.test.ts | 79 ++++++++++++++++++++++++++++++- dist/setup/index.js | 38 ++++++++++++++- docs/advanced-usage.md | 13 ++++- src/utils.ts | 36 +++++++++++++- 5 files changed, 193 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index ebb5bf05..8d68df25 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -245,6 +245,39 @@ jobs: - name: Run simple code run: python -c 'import math; print(math.factorial(5))' + setup-versions-from-tool-versions-file: + name: Setup ${{ matrix.python }} ${{ matrix.os }} .tool-versions file + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + [ + macos-latest, + windows-latest, + ubuntu-20.04, + ubuntu-22.04, + macos-13, + ubuntu-latest + ] + python: [3.13.0, 3.14-dev, pypy3.11-7.3.18, graalpy-24.1.2] + exclude: + - os: windows-latest + python: graalpy-24.1.2 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: build-tool-versions-file ${{ matrix.python }} + run: | + echo "python ${{ matrix.python }}" > .tool-versions + + - name: setup-python using .tool-versions ${{ matrix.python }} + id: setup-python-tool-versions + uses: ./ + with: + python-version-file: .tool-versions + setup-pre-release-version-from-manifest: name: Setup 3.14.0-alpha.1 ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index eac39ab6..6c0f0e13 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -15,7 +15,8 @@ import { getNextPageUrl, isGhes, IS_WINDOWS, - getDownloadFileName + getDownloadFileName, + getVersionInputFromToolVersions } from '../src/utils'; jest.mock('@actions/cache'); @@ -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 pypy version', + 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 alpha Releases', + 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 dev suffix', + 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', () => { diff --git a/dist/setup/index.js b/dist/setup/index.js index f4a95fad..78254955 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -100535,7 +100535,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)); @@ -100759,12 +100759,46 @@ 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'); + for (const line of lines) { + // Skip commented lines + if (line.trim().startsWith('#')) { + continue; + } + const match = line.match(/^\s*python\s*v?\s*(?[^\s]+)\s*$/); + if (match) { + return [((_a = match.groups) === null || _a === void 0 ? void 0 : _a.version.trim()) || '']; + } + } + core.warning(`No Python version found in ${versionFile}`); + return []; + } + 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); } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 07d445b6..72b35016 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -278,9 +278,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: @@ -300,6 +300,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. diff --git a/src/utils.ts b/src/utils.ts index a6dab63e..6274895e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -279,11 +279,45 @@ export function getVersionInputFromPlainFile(versionFile: string): string[] { } /** - * 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'); + + for (const line of lines) { + // Skip commented lines + if (line.trim().startsWith('#')) { + continue; + } + const match = line.match(/^\s*python\s*v?\s*(?[^\s]+)\s*$/); + if (match) { + return [match.groups?.version.trim() || '']; + } + } + + core.warning(`No Python version found in ${versionFile}`); + + return []; + } 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); }