From 5db1cf9a59fb97c40a68accab29236f0da7e94db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Wed, 21 May 2025 23:19:28 +0200 Subject: [PATCH] Enhance reading from .python-version (#787) * Enhance reading from .python-version * Fix typos * Fix lint * Add built files * Don't use EOL versions in `utils.test.ts` * Fix Prettier * Don't use unreleased versions in `utils.test.ts` * Update versions in `utils.test.ts` again --- __tests__/utils.test.ts | 66 +++++++++++++++++++++++++++++------------ dist/setup/index.js | 37 ++++++++++++++++------- src/setup-python.ts | 4 +-- src/utils.ts | 31 ++++++++++++++----- 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index 6c0f0e13..009749c6 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -10,7 +10,7 @@ import { validatePythonVersionFormatForPyPy, isCacheFeatureAvailable, getVersionInputFromFile, - getVersionInputFromPlainFile, + getVersionsInputFromPlainFile, getVersionInputFromTomlFile, getNextPageUrl, isGhes, @@ -24,10 +24,10 @@ jest.mock('@actions/core'); describe('validatePythonVersionFormatForPyPy', () => { it.each([ - ['3.6', true], - ['3.7', true], - ['3.6.x', false], - ['3.7.x', false], + ['3.12', true], + ['3.13', true], + ['3.12.x', false], + ['3.13.x', false], ['3.x', false], ['3', false] ])('%s -> %s', (input, expected) => { @@ -95,24 +95,52 @@ const tempDir = path.join( ); describe('Version from file test', () => { - it.each([getVersionInputFromPlainFile, getVersionInputFromFile])( + it.each([getVersionsInputFromPlainFile, getVersionInputFromFile])( 'Version from plain file test', async _fn => { await io.mkdirP(tempDir); const pythonVersionFileName = 'python-version.file'; const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); - const pythonVersionFileContent = '3.7'; + const pythonVersionFileContent = '3.13'; fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersionFileContent]); } ); + it.each([getVersionsInputFromPlainFile, getVersionInputFromFile])( + 'Versions from multiline plain file test', + async _fn => { + await io.mkdirP(tempDir); + const pythonVersionFileName = 'python-version.file'; + const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); + const pythonVersionFileContent = '3.13\r\n3.12'; + fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); + expect(_fn(pythonVersionFilePath)).toEqual(['3.13', '3.12']); + } + ); + it.each([getVersionsInputFromPlainFile, getVersionInputFromFile])( + 'Version from complex plain file test', + async _fn => { + await io.mkdirP(tempDir); + const pythonVersionFileName = 'python-version.file'; + const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); + const pythonVersionFileContent = + '3.13/envs/virtualenv\r# 3.12\n3.11\r\n3.10\r\n 3.9 \r\n'; + fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); + expect(_fn(pythonVersionFilePath)).toEqual([ + '3.13', + '3.11', + '3.10', + '3.9' + ]); + } + ); it.each([getVersionInputFromTomlFile, getVersionInputFromFile])( 'Version from standard pyproject.toml test', async _fn => { await io.mkdirP(tempDir); const pythonVersionFileName = 'pyproject.toml'; const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); - const pythonVersion = '>=3.7.0'; + const pythonVersion = '>=3.13.0'; const pythonVersionFileContent = `[project]\nrequires-python = "${pythonVersion}"`; fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]); @@ -124,7 +152,7 @@ describe('Version from file test', () => { await io.mkdirP(tempDir); const pythonVersionFileName = 'pyproject.toml'; const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); - const pythonVersion = '>=3.7.0'; + const pythonVersion = '>=3.13.0'; const pythonVersionFileContent = `[tool.poetry.dependencies]\npython = "${pythonVersion}"`; fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]); @@ -145,9 +173,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = 'python 3.9.10\nnodejs 16'; + const toolVersionContent = 'python 3.13.2\nnodejs 16'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']); + expect(_fn(toolVersionFilePath)).toEqual(['3.13.2']); } ); @@ -156,9 +184,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = '# python 3.8\npython 3.9'; + const toolVersionContent = '# python 3.13\npython 3.12'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.9']); + expect(_fn(toolVersionFilePath)).toEqual(['3.12']); } ); @@ -167,9 +195,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = ' python 3.10 '; + const toolVersionContent = ' python 3.13 '; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.10']); + expect(_fn(toolVersionFilePath)).toEqual(['3.13']); } ); @@ -178,9 +206,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = 'python v3.9.10'; + const toolVersionContent = 'python v3.13.2'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']); + expect(_fn(toolVersionFilePath)).toEqual(['3.13.2']); } ); @@ -189,9 +217,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = 'python pypy3.10-7.3.14'; + const toolVersionContent = 'python pypy3.10-7.3.19'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['pypy3.10-7.3.14']); + expect(_fn(toolVersionFilePath)).toEqual(['pypy3.10-7.3.19']); } ); diff --git a/dist/setup/index.js b/dist/setup/index.js index 87c6a816..4e3f2673 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -96929,7 +96929,7 @@ function cacheDependencies(cache, pythonVersion) { } function resolveVersionInputFromDefaultFile() { const couples = [ - ['.python-version', utils_1.getVersionInputFromPlainFile] + ['.python-version', utils_1.getVersionsInputFromPlainFile] ]; for (const [versionFile, _fn] of couples) { (0, utils_1.logWarning)(`Neither 'python-version' nor 'python-version-file' inputs were supplied. Attempting to find '${versionFile}' file.`); @@ -97066,7 +97066,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.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; +exports.getDownloadFileName = exports.getNextPageUrl = exports.getBinaryDirectory = exports.getVersionInputFromFile = exports.getVersionInputFromToolVersions = exports.getVersionsInputFromPlainFile = 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)); @@ -97247,7 +97247,7 @@ function extractValue(obj, keys) { * If none is present, returns an empty list. */ function getVersionInputFromTomlFile(versionFile) { - core.debug(`Trying to resolve version form ${versionFile}`); + core.debug(`Trying to resolve version from ${versionFile}`); let pyprojectFile = fs_1.default.readFileSync(versionFile, 'utf8'); // Normalize the line endings in the pyprojectFile pyprojectFile = pyprojectFile.replace(/\r\n/g, '\n'); @@ -97280,15 +97280,30 @@ function getVersionInputFromTomlFile(versionFile) { } exports.getVersionInputFromTomlFile = getVersionInputFromTomlFile; /** - * Python version extracted from a plain text file. + * Python versions extracted from a plain text file. + * - Resolves multiple versions from multiple lines. + * - Handles pyenv-virtualenv pointers (e.g. `3.10/envs/virtualenv`). + * - Ignores empty lines and lines starting with `#` + * - Trims whitespace. */ -function getVersionInputFromPlainFile(versionFile) { - core.debug(`Trying to resolve version form ${versionFile}`); - const version = fs_1.default.readFileSync(versionFile, 'utf8').trim(); - core.info(`Resolved ${versionFile} as ${version}`); - return [version]; +function getVersionsInputFromPlainFile(versionFile) { + core.debug(`Trying to resolve versions from ${versionFile}`); + const content = fs_1.default.readFileSync(versionFile, 'utf8').trim(); + const lines = content.split(/\r\n|\r|\n/); + const versions = lines + .map(line => { + if (line.startsWith('#') || line.trim() === '') { + return undefined; + } + let version = line.trim(); + version = version.split('/')[0]; + return version; + }) + .filter(version => version !== undefined); + core.info(`Resolved ${versionFile} as ${versions.join(', ')}`); + return versions; } -exports.getVersionInputFromPlainFile = getVersionInputFromPlainFile; +exports.getVersionsInputFromPlainFile = getVersionsInputFromPlainFile; /** * Python version extracted from a .tool-versions file. */ @@ -97331,7 +97346,7 @@ function getVersionInputFromFile(versionFile) { return getVersionInputFromToolVersions(versionFile); } else { - return getVersionInputFromPlainFile(versionFile); + return getVersionsInputFromPlainFile(versionFile); } } exports.getVersionInputFromFile = getVersionInputFromFile; diff --git a/src/setup-python.ts b/src/setup-python.ts index ab5931b8..5d585d73 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -11,7 +11,7 @@ import { logWarning, IS_MAC, getVersionInputFromFile, - getVersionInputFromPlainFile + getVersionsInputFromPlainFile } from './utils'; function isPyPyVersion(versionSpec: string) { @@ -35,7 +35,7 @@ async function cacheDependencies(cache: string, pythonVersion: string) { function resolveVersionInputFromDefaultFile(): string[] { const couples: [string, (versionFile: string) => string[]][] = [ - ['.python-version', getVersionInputFromPlainFile] + ['.python-version', getVersionsInputFromPlainFile] ]; for (const [versionFile, _fn] of couples) { logWarning( diff --git a/src/utils.ts b/src/utils.ts index 6274895e..f39006d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -228,7 +228,7 @@ function extractValue(obj: any, keys: string[]): string | undefined { * If none is present, returns an empty list. */ export function getVersionInputFromTomlFile(versionFile: string): string[] { - core.debug(`Trying to resolve version form ${versionFile}`); + core.debug(`Trying to resolve version from ${versionFile}`); let pyprojectFile = fs.readFileSync(versionFile, 'utf8'); // Normalize the line endings in the pyprojectFile @@ -269,13 +269,28 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] { } /** - * Python version extracted from a plain text file. + * Python versions extracted from a plain text file. + * - Resolves multiple versions from multiple lines. + * - Handles pyenv-virtualenv pointers (e.g. `3.10/envs/virtualenv`). + * - Ignores empty lines and lines starting with `#` + * - Trims whitespace. */ -export function getVersionInputFromPlainFile(versionFile: string): string[] { - core.debug(`Trying to resolve version form ${versionFile}`); - const version = fs.readFileSync(versionFile, 'utf8').trim(); - core.info(`Resolved ${versionFile} as ${version}`); - return [version]; +export function getVersionsInputFromPlainFile(versionFile: string): string[] { + core.debug(`Trying to resolve versions from ${versionFile}`); + const content = fs.readFileSync(versionFile, 'utf8').trim(); + const lines = content.split(/\r\n|\r|\n/); + const versions = lines + .map(line => { + if (line.startsWith('#') || line.trim() === '') { + return undefined; + } + let version: string = line.trim(); + version = version.split('/')[0]; + return version; + }) + .filter(version => version !== undefined) as string[]; + core.info(`Resolved ${versionFile} as ${versions.join(', ')}`); + return versions; } /** @@ -319,7 +334,7 @@ export function getVersionInputFromFile(versionFile: string): string[] { } else if (versionFile.match('.tool-versions')) { return getVersionInputFromToolVersions(versionFile); } else { - return getVersionInputFromPlainFile(versionFile); + return getVersionsInputFromPlainFile(versionFile); } }