Skip to content

Commit

Permalink
feat(pipenv): better python constraints checking (#28878)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins committed May 8, 2024
1 parent 54ba9af commit 78e3ea6
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 9 deletions.
1 change: 1 addition & 0 deletions lib/modules/manager/pipenv/__fixtures__/Pipfile1
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ dev-package = "==0.1.0"

[requires]
python_version = "3.6"
python_full_version = "3.6.2"
133 changes: 133 additions & 0 deletions lib/modules/manager/pipenv/artifacts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe('modules/manager/pipenv/artifacts', () => {
// python
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '3.6.2' },
{ version: '3.6.5' },
{ version: '3.7.6' },
{ version: '3.8.5' },
Expand Down Expand Up @@ -137,6 +138,138 @@ describe('modules/manager/pipenv/artifacts', () => {
]);
});

it('gets python full version from Pipfile', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
pipFileLock._meta!.requires!.python_full_version = '3.7.6';
fs.ensureCacheDir.mockResolvedValueOnce(pipenvCacheDir);
fs.ensureCacheDir.mockResolvedValueOnce(virtualenvsCacheDir);
fs.readLocalFile.mockResolvedValueOnce(JSON.stringify(pipFileLock));
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(JSON.stringify(pipFileLock));

expect(
await updateArtifacts({
packageFileName: 'Pipfile',
updatedDeps: [],
newPackageFileContent: Fixtures.get('Pipfile1'),
config,
}),
).toBeNull();

expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool python 3.6.2' },
{},
{
cmd: 'pipenv lock',
options: {
cwd: '/tmp/github/some/repo',
env: {
PIPENV_CACHE_DIR: pipenvCacheDir,
},
},
},
]);
});

it('gets python version from Pipfile', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
pipFileLock._meta!.requires!.python_full_version = '3.7.6';
fs.ensureCacheDir.mockResolvedValueOnce(pipenvCacheDir);
fs.ensureCacheDir.mockResolvedValueOnce(virtualenvsCacheDir);
fs.readLocalFile.mockResolvedValueOnce(JSON.stringify(pipFileLock));
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(JSON.stringify(pipFileLock));

expect(
await updateArtifacts({
packageFileName: 'Pipfile',
updatedDeps: [],
newPackageFileContent: Fixtures.get('Pipfile2'),
config,
}),
).toBeNull();

expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool python 3.6.5' },
{},
{
cmd: 'pipenv lock',
options: {
cwd: '/tmp/github/some/repo',
env: {
PIPENV_CACHE_DIR: pipenvCacheDir,
},
},
},
]);
});

it('gets full python version from .python-version', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
fs.ensureCacheDir.mockResolvedValueOnce(pipenvCacheDir);
fs.ensureCacheDir.mockResolvedValueOnce(virtualenvsCacheDir);
fs.readLocalFile.mockResolvedValueOnce(JSON.stringify(pipFileLock));
const execSnapshots = mockExecAll();
fs.getSiblingFileName.mockResolvedValueOnce('.python-version' as never);
fs.readLocalFile.mockResolvedValueOnce('3.7.6');

expect(
await updateArtifacts({
packageFileName: 'Pipfile',
updatedDeps: [],
newPackageFileContent: 'some toml',
config,
}),
).toBeNull();

expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool python 3.7.6' },
{},
{
cmd: 'pipenv lock',
options: {
cwd: '/tmp/github/some/repo',
env: {
PIPENV_CACHE_DIR: pipenvCacheDir,
},
},
},
]);
});

it('gets python stream, from .python-version', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
fs.ensureCacheDir.mockResolvedValueOnce(pipenvCacheDir);
fs.ensureCacheDir.mockResolvedValueOnce(virtualenvsCacheDir);
fs.readLocalFile.mockResolvedValueOnce(JSON.stringify(pipFileLock));
const execSnapshots = mockExecAll();
fs.getSiblingFileName.mockResolvedValueOnce('.python-version' as never);
fs.readLocalFile.mockResolvedValueOnce('3.8');

expect(
await updateArtifacts({
packageFileName: 'Pipfile',
updatedDeps: [],
newPackageFileContent: 'some toml',
config,
}),
).toBeNull();

expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool python 3.8.5' },
{},
{
cmd: 'pipenv lock',
options: {
cwd: '/tmp/github/some/repo',
env: {
PIPENV_CACHE_DIR: pipenvCacheDir,
},
},
},
]);
});

it('handles no constraint', async () => {
fs.ensureCacheDir.mockResolvedValueOnce(pipenvCacheDir);
fs.ensureCacheDir.mockResolvedValueOnce(pipCacheDir);
Expand Down
79 changes: 70 additions & 9 deletions lib/modules/manager/pipenv/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import type { ExecOptions, ExtraEnv, Opt } from '../../../util/exec/types';
import {
deleteLocalFile,
ensureCacheDir,
getSiblingFileName,
readLocalFile,
writeLocalFile,
} from '../../../util/fs';
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,
Expand All @@ -24,38 +27,91 @@ import type {
import { extractPackageFile } from './extract';
import { PipfileLockSchema } from './schema';

export function getPythonConstraint(
export async function getPythonConstraint(
pipfileName: string,
pipfileContent: string,
existingLockFileContent: string,
config: UpdateArtifactsConfig,
): string | undefined {
): Promise<string | undefined> {
const { constraints = {} } = config;
const { python } = constraints;

if (python) {
logger.debug('Using python constraint from config');
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({ error: result.error }, 'Invalid Pipfile.lock');
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
if (result.data._meta?.requires?.python_full_version) {
const pythonFullVersion = result.data._meta.requires.python_full_version;
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
if (result.data._meta?.requires?.python_version) {
const pythonVersion = result.data._meta.requires.python_version;
const pythonVersion = result.data._meta?.requires?.python_version;
if (pythonVersion) {
logger.debug(`Using python version ${pythonVersion} from Pipfile.lock`);
return `== ${pythonVersion}.*`;
}
} catch (err) {
// 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 (err) {
// Do nothing
}

return undefined;
}

Expand Down Expand Up @@ -233,7 +289,12 @@ export async function updateArtifacts({
await deleteLocalFile(lockFileName);
}
const cmd = 'pipenv lock';
const tagConstraint = getPythonConstraint(existingLockFileContent, config);
const tagConstraint = await getPythonConstraint(
pipfileName,
newPipfileContent,
existingLockFileContent,
config,
);
const pipenvConstraint = getPipenvConstraint(
existingLockFileContent,
config,
Expand Down

0 comments on commit 78e3ea6

Please sign in to comment.