From c3cf91b8b91475bc87dc390eb07753d866e9f76b Mon Sep 17 00:00:00 2001 From: Sergei Zharinov <zharinov@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:17:00 -0300 Subject: [PATCH] feat(pipenv): Use `@renovatebot/detect-tools` for constraints detection (#29787) Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- lib/modules/manager/pipenv/artifacts.spec.ts | 89 +++++++-- lib/modules/manager/pipenv/artifacts.ts | 179 ++----------------- lib/modules/manager/pipenv/extract.spec.ts | 35 +++- lib/modules/manager/pipenv/extract.ts | 24 ++- lib/modules/manager/pipenv/schema.ts | 30 ---- lib/modules/manager/pipenv/types.ts | 11 ++ package.json | 1 + pnpm-lock.yaml | 13 ++ 8 files changed, 148 insertions(+), 234 deletions(-) delete mode 100644 lib/modules/manager/pipenv/schema.ts diff --git a/lib/modules/manager/pipenv/artifacts.spec.ts b/lib/modules/manager/pipenv/artifacts.spec.ts index d509a94b7d..5cdb047e91 100644 --- a/lib/modules/manager/pipenv/artifacts.spec.ts +++ b/lib/modules/manager/pipenv/artifacts.spec.ts @@ -24,7 +24,7 @@ import { extractEnvironmentVariableName, getMatchingHostRule, } from './artifacts'; -import type { PipfileLockSchema } from './schema'; +import { PipfileLock } from './types'; import { updateArtifacts } from '.'; const datasource = mocked(_datasource); @@ -128,13 +128,14 @@ describe('modules/manager/pipenv/artifacts', () => { it('returns null if unchanged', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': JSON.stringify({ _meta: { requires: { python_full_version: '3.7.6' }, }, - } satisfies PipfileLockSchema), + } satisfies PipfileLock), }); const execSnapshots = mockExecAll(); @@ -165,19 +166,24 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); }); it('gets python full version from Pipfile', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ + '/Pipfile': Fixtures.get('Pipfile1'), '/Pipfile.lock': JSON.stringify({ _meta: { requires: { python_full_version: '3.7.6' }, }, - } satisfies PipfileLockSchema), + } satisfies PipfileLock), }); fsExtra.ensureDir.mockResolvedValue(undefined as never); @@ -213,19 +219,24 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ - [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], ]); }); it('gets python version from Pipfile', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ + '/Pipfile': Fixtures.get('Pipfile2'), '/Pipfile.lock': JSON.stringify({ _meta: { requires: { python_full_version: '3.7.6' }, }, - } satisfies PipfileLockSchema), + } satisfies PipfileLock), }); fsExtra.ensureDir.mockResolvedValue(undefined as never); @@ -261,12 +272,16 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ - [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], ]); }); it('gets full python version from .python-version', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': '{}', @@ -306,13 +321,17 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/.python-version')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); }); it('gets python stream, from .python-version', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + fsExtra.stat.mockResolvedValueOnce({} as never); fsExtra.ensureDir.mockResolvedValue(undefined as never); @@ -351,13 +370,17 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/.python-version')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); }); it('handles no constraint', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': 'unparseable pipfile lock', @@ -394,12 +417,17 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/.python-version')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); }); it('returns updated Pipfile.lock', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': ['current pipfile.lock', 'new pipfile.lock'], @@ -448,6 +476,7 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); @@ -455,12 +484,13 @@ describe('modules/manager/pipenv/artifacts', () => { it('supports docker mode', async () => { GlobalConfig.set(dockerAdminConfig); + fsExtra.stat.mockResolvedValueOnce({} as never); const pipFileLock = JSON.stringify({ _meta: { requires: { python_version: '3.7' } }, - } satisfies PipfileLockSchema); + } satisfies PipfileLock); mockFiles({ - '/Pipfile.lock': [pipFileLock, 'new lock'], + '/Pipfile.lock': [pipFileLock, pipFileLock, 'new lock'], }); fsExtra.ensureDir.mockResolvedValue(undefined as never); @@ -523,6 +553,9 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); @@ -530,10 +563,11 @@ describe('modules/manager/pipenv/artifacts', () => { it('supports install mode', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + fsExtra.stat.mockResolvedValueOnce({} as never); const pipFileLock = JSON.stringify({ _meta: { requires: { python_version: '3.6' } }, - } satisfies PipfileLockSchema); + } satisfies PipfileLock); mockFiles({ '/Pipfile.lock': [pipFileLock, 'new lock'], }); @@ -582,6 +616,9 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); @@ -589,6 +626,7 @@ describe('modules/manager/pipenv/artifacts', () => { it('defaults to latest if no lock constraints', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + fsExtra.stat.mockResolvedValueOnce({} as never); fsExtra.ensureDir.mockResolvedValue(undefined as never); mockFiles({ @@ -637,14 +675,18 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/.python-version')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); }); it('catches errors', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': 'Current Pipfile.lock', @@ -666,13 +708,12 @@ describe('modules/manager/pipenv/artifacts', () => { ]); expect(fsExtra.ensureDir.mock.calls).toEqual([]); - expect(fsExtra.readFile.mock.calls).toEqual([ - [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], - ]); + expect(fsExtra.readFile.mock.calls).toEqual([]); }); it('returns updated Pipenv.lock when doing lockfile maintenance', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': ['Current Pipfile.lock', 'New Pipfile.lock'], @@ -711,14 +752,15 @@ describe('modules/manager/pipenv/artifacts', () => { it('uses pipenv version from Pipfile', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); GlobalConfig.set(dockerAdminConfig); const oldLock = JSON.stringify({ default: { pipenv: { version: '==2020.8.13' } }, - } satisfies PipfileLockSchema); + } satisfies PipfileLock); mockFiles({ - '/Pipfile.lock': [oldLock, 'new lock'], + '/Pipfile.lock': [oldLock, oldLock, 'new lock'], }); const execSnapshots = mockExecAll(); @@ -774,22 +816,26 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/.python-version')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); }); it('uses pipenv version from Pipfile dev packages', async () => { GlobalConfig.set(dockerAdminConfig); + fsExtra.stat.mockResolvedValueOnce({} as never); fsExtra.ensureDir.mockResolvedValue(undefined as never); const oldLock = JSON.stringify({ develop: { pipenv: { version: '==2020.8.13' } }, - } satisfies PipfileLockSchema) as never; + } satisfies PipfileLock) as never; mockFiles({ - '/Pipfile.lock': [oldLock, 'new lock'], + '/Pipfile.lock': [oldLock, oldLock, 'new lock'], }); const execSnapshots = mockExecAll(); @@ -845,19 +891,23 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/.python-version')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], ]); }); it('uses pipenv version from config', async () => { GlobalConfig.set(dockerAdminConfig); + fsExtra.stat.mockResolvedValueOnce({} as never); fsExtra.ensureDir.mockResolvedValue(undefined as never); const oldLock = JSON.stringify({ default: { pipenv: { version: '==2020.8.13' } }, - } satisfies PipfileLockSchema) as never; + } satisfies PipfileLock) as never; mockFiles({ '/Pipfile.lock': [oldLock, 'new lock'], }); @@ -915,6 +965,7 @@ describe('modules/manager/pipenv/artifacts', () => { [expect.toEndWith(join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ + [expect.toEndWith(join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/.python-version')), 'utf8'], [expect.toEndWith(join('/tmp/github/some/repo/Pipfile.lock')), 'utf8'], @@ -923,6 +974,7 @@ describe('modules/manager/pipenv/artifacts', () => { it('passes private credential environment vars', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': ['current Pipfile.lock', 'New Pipfile.lock'], @@ -985,7 +1037,7 @@ describe('modules/manager/pipenv/artifacts', () => { ${'${USERNAME}'} | ${'USERNAME'} ${'${USERNAME:-default}'} | ${'USERNAME'} ${'${COMPLEX_NAME_1:-default}'} | ${'COMPLEX_NAME_1'} - `('extractEnvironmentVariableName(%p)', ({ credential, result }) => { + `('extractEnvironmentVariableName($credential)', ({ credential, result }) => { expect(extractEnvironmentVariableName(credential)).toEqual(result); }); @@ -999,6 +1051,7 @@ describe('modules/manager/pipenv/artifacts', () => { it('updates extraEnv if variable names differ from default', async () => { fsExtra.ensureDir.mockResolvedValue(undefined as never); + fsExtra.stat.mockResolvedValueOnce({} as never); mockFiles({ '/Pipfile.lock': ['current Pipfile.lock', 'New Pipfile.lock'], diff --git a/lib/modules/manager/pipenv/artifacts.ts b/lib/modules/manager/pipenv/artifacts.ts index aa05cbecbd..8d2760ce35 100644 --- a/lib/modules/manager/pipenv/artifacts.ts +++ b/lib/modules/manager/pipenv/artifacts.ts @@ -1,5 +1,5 @@ +import { pipenv as pipenvDetect } from '@renovatebot/detect-tools'; import is from '@sindresorhus/is'; -import semver from 'semver'; import { TEMPORARY_ERROR } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import type { HostRule } from '../../../types'; @@ -8,168 +8,19 @@ import type { ExecOptions, ExtraEnv, Opt } from '../../../util/exec/types'; import { deleteLocalFile, ensureCacheDir, - getSiblingFileName, + getParentDir, + localPathExists, readLocalFile, writeLocalFile, } from '../../../util/fs'; +import { ensureLocalPath } from '../../../util/fs/util'; import { getRepoStatus } from '../../../util/git'; import { find } from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; -import { parse as parseToml } from '../../../util/toml'; import { parseUrl } from '../../../util/url'; import { PypiDatasource } from '../../datasource/pypi'; -import pep440 from '../../versioning/pep440'; -import type { - UpdateArtifact, - UpdateArtifactsConfig, - UpdateArtifactsResult, -} from '../types'; +import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; import { extractPackageFile } from './extract'; -import { PipfileLockSchema } from './schema'; - -export async function getPythonConstraint( - pipfileName: string, - pipfileContent: string, - existingLockFileContent: string, - config: UpdateArtifactsConfig, -): Promise<string | undefined> { - const { constraints = {} } = config; - const { python } = constraints; - - if (python) { - logger.debug(`Using python constraint ${python} from config`); - return python; - } - - // Try Pipfile first because it may have had its Python version updated - try { - const pipfile = parseToml(pipfileContent) as any; - const pythonFullVersion = pipfile.requires.python_full_version; - if (pythonFullVersion) { - logger.debug( - `Using python full version ${pythonFullVersion} from Pipfile`, - ); - return `== ${pythonFullVersion}`; - } - const pythonVersion = pipfile.requires.python_version; - if (pythonVersion) { - logger.debug(`Using python version ${pythonVersion} from Pipfile`); - return `== ${pythonVersion}.*`; - } - } catch (err) { - logger.warn({ err }, 'Error parsing Pipfile'); - } - - // Try Pipfile.lock next - try { - const result = PipfileLockSchema.safeParse(existingLockFileContent); - // istanbul ignore if: not easily testable - if (!result.success) { - logger.warn({ err: result.error }, 'Invalid Pipfile.lock'); - return undefined; - } - // Exact python version has been included since 2022.10.9. It is more specific than the major.minor version - // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#2022109-2022-10-09 - const pythonFullVersion = result.data._meta?.requires?.python_full_version; - if (pythonFullVersion) { - logger.debug( - `Using python full version ${pythonFullVersion} from Pipfile.lock`, - ); - return `== ${pythonFullVersion}`; - } - // Before 2022.10.9, only the major.minor version was included - const pythonVersion = result.data._meta?.requires?.python_version; - if (pythonVersion) { - logger.debug(`Using python version ${pythonVersion} from Pipfile.lock`); - return `== ${pythonVersion}.*`; - } - } catch { - // Do nothing - } - - // Try looking for the contents of .python-version - const pythonVersionFileName = getSiblingFileName( - pipfileName, - '.python-version', - ); - try { - const pythonVersion = await readLocalFile(pythonVersionFileName, 'utf8'); - let pythonVersionConstraint; - if (pythonVersion && pep440.isVersion(pythonVersion)) { - if (pythonVersion.split('.').length >= 3) { - pythonVersionConstraint = `== ${pythonVersion}`; - } else { - pythonVersionConstraint = `== ${pythonVersion}.*`; - } - } - if (pythonVersionConstraint) { - logger.debug( - `Using python version ${pythonVersionConstraint} from ${pythonVersionFileName}`, - ); - return pythonVersionConstraint; - } - } catch { - // Do nothing - } - - return undefined; -} - -export function getPipenvConstraint( - existingLockFileContent: string, - config: UpdateArtifactsConfig, -): string { - const { constraints = {} } = config; - const { pipenv } = constraints; - - if (pipenv) { - logger.debug('Using pipenv constraint from config'); - return pipenv; - } - try { - const result = PipfileLockSchema.safeParse(existingLockFileContent); - // istanbul ignore if: not easily testable - if (!result.success) { - logger.warn({ error: result.error }, 'Invalid Pipfile.lock'); - return ''; - } - if (result.data.default?.pipenv?.version) { - return result.data.default.pipenv.version; - } - if (result.data.develop?.pipenv?.version) { - return result.data.develop.pipenv.version; - } - // Exact python version has been included since 2022.10.9 - const pythonFullVersion = result.data._meta?.requires?.python_full_version; - if (is.string(pythonFullVersion) && semver.valid(pythonFullVersion)) { - // python_full_version was added after 3.6 was already deprecated, so it should be impossible to have a 3.6 version - // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#2022109-2022-10-09 - if (semver.satisfies(pythonFullVersion, '3.7.*')) { - // Python 3.7 support was dropped in pipenv 2023.10.20 - // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#20231020-2023-10-20 - return '< 2023.10.20'; - } - // Future deprecations will go here - } - // Before 2022.10.9, only the major.minor version was included - const pythonVersion = result.data._meta?.requires?.python_version; - if (pythonVersion) { - if (pythonVersion === '3.6') { - // Python 3.6 was deprecated in 2022.4.20 - // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#2022420-2022-04-20 - return '< 2022.4.20'; - } - if (pythonVersion === '3.7') { - // Python 3.7 was deprecated in 2023.10.20 but we shouldn't reach here unless we are < 2022.10.9 - // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#20231020-2023-10-20 - return '< 2022.10.9'; - } - } - } catch { - // Do nothing - } - return ''; -} export function getMatchingHostRule(url: string): HostRule | null { const parsedUrl = parseUrl(url); @@ -278,8 +129,7 @@ export async function updateArtifacts({ logger.debug(`pipenv.updateArtifacts(${pipfileName})`); const lockFileName = pipfileName + '.lock'; - const existingLockFileContent = await readLocalFile(lockFileName, 'utf8'); - if (!existingLockFileContent) { + if (!(await localPathExists(lockFileName))) { logger.debug('No Pipfile.lock found'); return null; } @@ -289,16 +139,13 @@ export async function updateArtifacts({ await deleteLocalFile(lockFileName); } const cmd = 'pipenv lock'; - const tagConstraint = await getPythonConstraint( - pipfileName, - newPipfileContent, - existingLockFileContent, - config, - ); - const pipenvConstraint = getPipenvConstraint( - existingLockFileContent, - config, - ); + const pipfileDir = getParentDir(ensureLocalPath(pipfileName)); + const tagConstraint = + config.constraints?.python ?? + (await pipenvDetect.getPythonConstraint(pipfileDir)); + const pipenvConstraint = + config.constraints?.pipenv ?? + (await pipenvDetect.getPipenvConstraint(pipfileDir)); const extraEnv: Opt<ExtraEnv> = { PIPENV_CACHE_DIR: await ensureCacheDir('pipenv'), PIP_CACHE_DIR: await ensureCacheDir('pip'), diff --git a/lib/modules/manager/pipenv/extract.spec.ts b/lib/modules/manager/pipenv/extract.spec.ts index 2f36c98092..49727aa800 100644 --- a/lib/modules/manager/pipenv/extract.spec.ts +++ b/lib/modules/manager/pipenv/extract.spec.ts @@ -1,9 +1,13 @@ import { codeBlock } from 'common-tags'; +import * as _fsExtra from 'fs-extra'; +import { join } from 'upath'; import { Fixtures } from '../../../../test/fixtures'; -import { fs } from '../../../../test/util'; +import { mocked } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; import { extractPackageFile } from '.'; -jest.mock('../../../util/fs'); +jest.mock('fs-extra'); +const fsExtra = mocked(_fsExtra); const pipfile1 = Fixtures.get('Pipfile1'); const pipfile2 = Fixtures.get('Pipfile2'); @@ -12,6 +16,16 @@ const pipfile4 = Fixtures.get('Pipfile4'); const pipfile5 = Fixtures.get('Pipfile5'); describe('modules/manager/pipenv/extract', () => { + beforeEach(() => { + GlobalConfig.set({ + localDir: join('/tmp/github/some/repo'), + }); + }); + + afterEach(() => { + GlobalConfig.reset(); + }); + describe('extractPackageFile()', () => { it('returns null for empty', async () => { expect(await extractPackageFile('[packages]\r\n', 'Pipfile')).toBeNull(); @@ -22,7 +36,8 @@ describe('modules/manager/pipenv/extract', () => { }); it('extracts dependencies', async () => { - fs.localPathExists.mockResolvedValueOnce(true); + fsExtra.stat.mockResolvedValueOnce({} as never); + fsExtra.readFile.mockResolvedValueOnce(pipfile1 as never); const res = await extractPackageFile(pipfile1, 'Pipfile'); expect(res).toMatchObject({ deps: [ @@ -99,7 +114,7 @@ describe('modules/manager/pipenv/extract', () => { }, ], extractedConstraints: { - python: '== 3.6.*', + python: '== 3.6.2', }, lockFiles: ['Pipfile.lock'], registryUrls: [ @@ -118,7 +133,8 @@ describe('modules/manager/pipenv/extract', () => { }); it('extracts multiple dependencies', async () => { - fs.localPathExists.mockResolvedValueOnce(true); + fsExtra.stat.mockResolvedValueOnce({} as never); + fsExtra.readFile.mockResolvedValueOnce(pipfile2 as never); const res = await extractPackageFile(pipfile2, 'Pipfile'); expect(res).toMatchObject({ deps: [ @@ -222,7 +238,8 @@ describe('modules/manager/pipenv/extract', () => { }); it('extracts example pipfile', async () => { - fs.localPathExists.mockResolvedValueOnce(true); + fsExtra.stat.mockResolvedValueOnce({} as never); + fsExtra.readFile.mockResolvedValueOnce(pipfile4 as never); const res = await extractPackageFile(pipfile4, 'Pipfile'); expect(res).toMatchObject({ deps: [ @@ -287,7 +304,7 @@ describe('modules/manager/pipenv/extract', () => { }); it('supports custom index', async () => { - fs.localPathExists.mockResolvedValueOnce(true); + fsExtra.stat.mockResolvedValueOnce({} as never); const res = await extractPackageFile(pipfile5, 'Pipfile'); expect(res).toMatchObject({ deps: [ @@ -318,6 +335,7 @@ describe('modules/manager/pipenv/extract', () => { [requires] python_version = "3.8" `; + fsExtra.readFile.mockResolvedValueOnce(content as never); const res = await extractPackageFile(content, 'Pipfile'); expect(res?.extractedConstraints?.python).toBe('== 3.8.*'); }); @@ -329,6 +347,7 @@ describe('modules/manager/pipenv/extract', () => { [requires] python_full_version = "3.8.6" `; + fsExtra.readFile.mockResolvedValueOnce(content as never); const res = await extractPackageFile(content, 'Pipfile'); expect(res?.extractedConstraints?.python).toBe('== 3.8.6'); }); @@ -338,6 +357,7 @@ describe('modules/manager/pipenv/extract', () => { [packages] pipenv = "==2020.8.13" `; + fsExtra.readFile.mockResolvedValue(content as never); const res = await extractPackageFile(content, 'Pipfile'); expect(res?.extractedConstraints?.pipenv).toBe('==2020.8.13'); }); @@ -347,6 +367,7 @@ describe('modules/manager/pipenv/extract', () => { [dev-packages] pipenv = "==2020.8.13" `; + fsExtra.readFile.mockResolvedValue(content as never); const res = await extractPackageFile(content, 'Pipfile'); expect(res?.extractedConstraints?.pipenv).toBe('==2020.8.13'); }); diff --git a/lib/modules/manager/pipenv/extract.ts b/lib/modules/manager/pipenv/extract.ts index cfdae749cb..a770b5b25c 100644 --- a/lib/modules/manager/pipenv/extract.ts +++ b/lib/modules/manager/pipenv/extract.ts @@ -1,8 +1,10 @@ +import { pipenv as pipenvDetect } from '@renovatebot/detect-tools'; import { RANGE_PATTERN } from '@renovatebot/pep440'; import is from '@sindresorhus/is'; import { logger } from '../../../logger'; import type { SkipReason } from '../../../types'; -import { localPathExists } from '../../../util/fs'; +import { getParentDir, localPathExists } from '../../../util/fs'; +import { ensureLocalPath } from '../../../util/fs/util'; import { regEx } from '../../../util/regex'; import { parse as parseToml } from '../../../util/toml'; import { PypiDatasource } from '../../datasource/pypi'; @@ -142,8 +144,6 @@ export async function extractPackageFile( res.registryUrls = sources.map((source) => source.url); } - let pipenv_constraint: PipRequirement | undefined; - res.deps = Object.entries(pipfile) .map(([category, section]) => { if ( @@ -154,10 +154,6 @@ export async function extractPackageFile( return []; } - if (section.pipenv && !pipenv_constraint) { - pipenv_constraint = section.pipenv; - } - return extractFromSection(category, section, sources); }) .flat(); @@ -168,14 +164,16 @@ export async function extractPackageFile( const extractedConstraints: Record<string, any> = {}; - if (is.nonEmptyString(pipfile.requires?.python_version)) { - extractedConstraints.python = `== ${pipfile.requires.python_version}.*`; - } else if (is.nonEmptyString(pipfile.requires?.python_full_version)) { - extractedConstraints.python = `== ${pipfile.requires.python_full_version}`; + const pipfileDir = getParentDir(ensureLocalPath(packageFile)); + + const pythonConstraint = await pipenvDetect.getPythonConstraint(pipfileDir); + if (pythonConstraint) { + extractedConstraints.python = pythonConstraint; } - if (pipenv_constraint) { - extractedConstraints.pipenv = pipenv_constraint; + const pipenvConstraint = await pipenvDetect.getPipenvConstraint(pipfileDir); + if (pipenvConstraint) { + extractedConstraints.pipenv = pipenvConstraint; } const lockFileName = `${packageFile}.lock`; diff --git a/lib/modules/manager/pipenv/schema.ts b/lib/modules/manager/pipenv/schema.ts deleted file mode 100644 index c7a59de51a..0000000000 --- a/lib/modules/manager/pipenv/schema.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from 'zod'; -import { Json } from '../../../util/schema-utils'; - -const PipfileLockEntrySchema = z - .record( - z.string(), - z.object({ - version: z.string().optional(), - }), - ) - .optional(); - -export const PipfileLockSchema = Json.pipe( - z.object({ - _meta: z - .object({ - requires: z - .object({ - python_version: z.string().optional(), - python_full_version: z.string().optional(), - }) - .optional(), - }) - .optional(), - default: PipfileLockEntrySchema, - develop: PipfileLockEntrySchema, - }), -); - -export type PipfileLockSchema = z.infer<typeof PipfileLockSchema>; diff --git a/lib/modules/manager/pipenv/types.ts b/lib/modules/manager/pipenv/types.ts index 51cbb9c186..9e1cbca605 100644 --- a/lib/modules/manager/pipenv/types.ts +++ b/lib/modules/manager/pipenv/types.ts @@ -30,3 +30,14 @@ export type PipRequirement = file?: string; git?: string; }; + +export interface PipfileLock { + _meta?: { + requires?: { + python_version?: string; + python_full_version?: string; + }; + }; + default?: Record<string, { version?: string }>; + develop?: Record<string, { version?: string }>; +} diff --git a/package.json b/package.json index 5009058c59..c05fd221ba 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ "@opentelemetry/sdk-trace-node": "1.25.1", "@opentelemetry/semantic-conventions": "1.25.1", "@qnighy/marshal": "0.1.3", + "@renovatebot/detect-tools": "1.0.4", "@renovatebot/kbpgp": "3.0.1", "@renovatebot/osv-offline": "1.5.9", "@renovatebot/pep440": "3.0.20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0e2e4e52d..98fe683f99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@qnighy/marshal': specifier: 0.1.3 version: 0.1.3 + '@renovatebot/detect-tools': + specifier: 1.0.4 + version: 1.0.4 '@renovatebot/kbpgp': specifier: 3.0.1 version: 3.0.1 @@ -1507,6 +1510,9 @@ packages: peerDependencies: '@redis/client': ^1.0.0 + '@renovatebot/detect-tools@1.0.4': + resolution: {integrity: sha512-s7RvoGgEolHIZ5IQUVoYJMa3uVDxMuz9TDq9zl+j678wNNygT4CoqaArxkZLbGzDU7Znf+HhhMGV5/etUd3ljQ==} + '@renovatebot/eslint-plugin@file:tools/eslint': resolution: {directory: tools/eslint, type: directory} @@ -7821,6 +7827,13 @@ snapshots: dependencies: '@redis/client': 1.6.0 + '@renovatebot/detect-tools@1.0.4': + dependencies: + fs-extra: 11.2.0 + toml-eslint-parser: 0.10.0 + upath: 2.0.1 + zod: 3.23.8 + '@renovatebot/eslint-plugin@file:tools/eslint': {} '@renovatebot/kbpgp@3.0.1': -- GitLab