diff --git a/lib/manager/poetry/__fixtures__/pyproject.1.toml b/lib/manager/poetry/__fixtures__/pyproject.1.toml index d57ebe42d52be2c1235ca30f2bdf6c0493079196..b6d4da07d089bbdf52d00eeb95f3fd324232ab3e 100644 --- a/lib/manager/poetry/__fixtures__/pyproject.1.toml +++ b/lib/manager/poetry/__fixtures__/pyproject.1.toml @@ -9,6 +9,7 @@ dep1_ = "0.0.0" dep1 = "0.0.0" dep2 = "^0.6.0" dep3 = "^0.33.6" +python = "~2.7 || ^3.4" [tool.poetry.extras] extra_dep1 = "^0.8.3" @@ -17,4 +18,8 @@ extra_dep3 = "^0.4.0" [tool.poetry.dev-dependencies] dev_dep1 = "^3.0" -dev_dep2 = "Invalid version." \ No newline at end of file +dev_dep2 = "Invalid version." + +[build-system] +requires = ["poetry>=1.0", "wheel"] +build-backend = "poetry.masonry.api" diff --git a/lib/manager/poetry/__fixtures__/pyproject.8.toml b/lib/manager/poetry/__fixtures__/pyproject.8.toml index 35f8fd227d08c62f6712fb9b486b0481e028f369..dabc529434ee12da1121586b3c8f301502becede 100644 --- a/lib/manager/poetry/__fixtures__/pyproject.8.toml +++ b/lib/manager/poetry/__fixtures__/pyproject.8.toml @@ -17,4 +17,3 @@ url = "https://bar.baz/+simple/" [[tool.poetry.source]] name = "baz" -url = "https://bar.baz/+simple/" diff --git a/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap b/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap index bc01d94f1fdf7c41734fdf9fcc0889923366ee27..cf12974a98d33ae8d0cd84bb4c10304321f1d130 100644 --- a/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap +++ b/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap @@ -56,22 +56,56 @@ Array [ ] `; +exports[`.updateArtifacts() returns updated poetry.lock using docker (constaints) 1`] = ` +Array [ + Object { + "cmd": "docker pull renovate/python:2.7.5", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker ps --filter name=renovate_python -aq", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker run --rm --name=renovate_python --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/python:2.7.5 bash -l -c \\"pip install poetry>=1.0 && poetry update --lock --no-interaction dep1\\"", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "timeout": 900000, + }, + }, +] +`; + exports[`.updateArtifacts() returns updated poetry.lock using docker 1`] = ` Array [ Object { - "cmd": "docker pull renovate/poetry", + "cmd": "docker pull renovate/python:3.4.2", "options": Object { "encoding": "utf-8", }, }, Object { - "cmd": "docker ps --filter name=renovate_poetry -aq", + "cmd": "docker ps --filter name=renovate_python -aq", "options": Object { "encoding": "utf-8", }, }, Object { - "cmd": "docker run --rm --name=renovate_poetry --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/poetry bash -l -c \\"poetry update --lock --no-interaction dep1\\"", + "cmd": "docker run --rm --name=renovate_python --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/python:3.4.2 bash -l -c \\"pip install poetry && poetry update --lock --no-interaction dep1\\"", "options": Object { "cwd": "/tmp/github/some/repo", "encoding": "utf-8", @@ -111,3 +145,25 @@ Array [ }, ] `; + +exports[`.updateArtifacts() returns updated pyproject.lock 1`] = ` +Array [ + Object { + "cmd": "poetry update --lock --no-interaction dep1", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "timeout": 900000, + }, + }, +] +`; diff --git a/lib/manager/poetry/__snapshots__/extract.spec.ts.snap b/lib/manager/poetry/__snapshots__/extract.spec.ts.snap index 3359944de8679f2e2742dd35db780df5c1898a53..2ce6206bbc0c32351bda12ebf0b723293b02eaae 100644 --- a/lib/manager/poetry/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/poetry/__snapshots__/extract.spec.ts.snap @@ -9,6 +9,7 @@ Array [ exports[`lib/manager/poetry/extract extractPackageFile() extracts mixed versioning types 1`] = ` Object { + "compatibility": Object {}, "deps": Array [ Object { "currentValue": "0.2", @@ -472,6 +473,16 @@ Array [ }, "versioning": "poetry", }, + Object { + "currentValue": "~2.7 || ^3.4", + "datasource": "pypi", + "depName": "python", + "depType": "dependencies", + "managerData": Object { + "nestedVersion": false, + }, + "versioning": "poetry", + }, Object { "currentValue": "^3.0", "datasource": "pypi", diff --git a/lib/manager/poetry/artifacts.spec.ts b/lib/manager/poetry/artifacts.spec.ts index b22ce30fe6aba51d306444eefcb55f05c3557c68..59034c9ada0e2be32979cdc5bac115eb0c5904ff 100644 --- a/lib/manager/poetry/artifacts.spec.ts +++ b/lib/manager/poetry/artifacts.spec.ts @@ -3,6 +3,7 @@ import _fs from 'fs-extra'; import { join } from 'upath'; import { envMock, mockExecAll } from '../../../test/execUtil'; import { mocked } from '../../../test/util'; +import * as _docker from '../../datasource/docker'; import { setExecConfig } from '../../util/exec'; import { BinarySource } from '../../util/exec/common'; import { resetPrefetchedImages } from '../../util/exec/docker'; @@ -17,10 +18,12 @@ jest.mock('../../util/exec/docker/index', () => removeDanglingContainers: jest.fn(), }) ); +jest.mock('../../datasource/docker'); const fs: jest.Mocked<typeof _fs> = _fs as any; const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); +const docker = mocked(_docker); const config = { localDir: join('/tmp/github/some/repo'), @@ -70,7 +73,24 @@ describe('.updateArtifacts()', () => { expect(execSnapshots).toMatchSnapshot(); }); it('returns updated poetry.lock', async () => { - fs.readFile.mockResolvedValueOnce('Old poetry.lock' as any); + fs.readFile.mockResolvedValueOnce('[metadata]\n' as never); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockReturnValueOnce('New poetry.lock' as any); + const updatedDeps = ['dep1']; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + + it('returns updated pyproject.lock', async () => { + fs.readFile.mockResolvedValueOnce(null); + fs.readFile.mockResolvedValueOnce('[metadata]\n' as never); const execSnapshots = mockExecAll(exec); fs.readFile.mockReturnValueOnce('New poetry.lock' as any); const updatedDeps = ['dep1']; @@ -90,9 +110,40 @@ describe('.updateArtifacts()', () => { binarySource: BinarySource.Docker, dockerUser: 'foobar', }); - fs.readFile.mockResolvedValueOnce('Old poetry.lock' as any); + fs.readFile.mockResolvedValueOnce('[metadata]\n' as any); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockReturnValueOnce('New poetry.lock' as any); + docker.getReleases.mockResolvedValueOnce({ + releases: [{ version: '2.7.5' }, { version: '3.4.2' }], + }); + const updatedDeps = ['dep1']; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: '{}', + config: { + ...config, + compatibility: { python: '~2.7 || ^3.4' }, + }, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('returns updated poetry.lock using docker (constaints)', async () => { + await setExecConfig({ + ...config, + binarySource: BinarySource.Docker, + dockerUser: 'foobar', + }); + fs.readFile.mockResolvedValueOnce( + '[metadata]\npython-versions = "~2.7 || ^3.4"' as any + ); const execSnapshots = mockExecAll(exec); fs.readFile.mockReturnValueOnce('New poetry.lock' as any); + docker.getReleases.mockResolvedValueOnce({ + releases: [{ version: '2.7.5' }, { version: '3.3.2' }], + }); const updatedDeps = ['dep1']; expect( await updateArtifacts({ @@ -101,6 +152,7 @@ describe('.updateArtifacts()', () => { newPackageFileContent: '{}', config: { ...config, + compatibility: { poetry: 'poetry>=1.0' }, }, }) ).not.toBeNull(); diff --git a/lib/manager/poetry/artifacts.ts b/lib/manager/poetry/artifacts.ts index 862dd6f88393cf4bc07eaa2da8ddebcd331755d5..d09a750491c46650ea9c7704525d30975eb42ac1 100644 --- a/lib/manager/poetry/artifacts.ts +++ b/lib/manager/poetry/artifacts.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import fs from 'fs-extra'; +import { parse } from 'toml'; import { logger } from '../../logger'; import { ExecOptions, exec } from '../../util/exec'; import { @@ -7,7 +8,33 @@ import { readLocalFile, writeLocalFile, } from '../../util/fs'; -import { UpdateArtifact, UpdateArtifactsResult } from '../common'; +import { + UpdateArtifact, + UpdateArtifactsConfig, + UpdateArtifactsResult, +} from '../common'; + +function getPythonConstraint( + existingLockFileContent: string, + config: UpdateArtifactsConfig +): string | undefined | null { + const { compatibility = {} } = config; + const { python } = compatibility; + + if (python) { + logger.debug('Using python constraint from config'); + return python; + } + try { + const data = parse(existingLockFileContent); + if (data?.metadata?.['python-versions']) { + return data?.metadata?.['python-versions']; + } + } catch (err) { + // Do nothing + } + return undefined; +} export async function updateArtifacts({ packageFileName, @@ -22,11 +49,11 @@ export async function updateArtifacts({ } // Try poetry.lock first let lockFileName = getSiblingFileName(packageFileName, 'poetry.lock'); - let existingLockFileContent = await readLocalFile(lockFileName); + let existingLockFileContent = await readLocalFile(lockFileName, 'utf8'); if (!existingLockFileContent) { // Try pyproject.lock next lockFileName = getSiblingFileName(packageFileName, 'pyproject.lock'); - existingLockFileContent = await readLocalFile(lockFileName); + existingLockFileContent = await readLocalFile(lockFileName, 'utf8'); if (!existingLockFileContent) { logger.debug(`No lock file found`); return null; @@ -45,12 +72,20 @@ export async function updateArtifacts({ cmd.push(`poetry update --lock --no-interaction ${dep}`); } } + const tagConstraint = getPythonConstraint(existingLockFileContent, config); const execOptions: ExecOptions = { cwdFile: packageFileName, - docker: { image: 'renovate/poetry' }, + docker: { + image: 'renovate/python', + tagConstraint, + tagScheme: 'poetry', + preCommands: [ + 'pip install ' + (config.compatibility?.poetry || 'poetry'), + ], + }, }; await exec(cmd, execOptions); - const newPoetryLockContent = await readLocalFile(lockFileName); + const newPoetryLockContent = await readLocalFile(lockFileName, 'utf8'); if (existingLockFileContent === newPoetryLockContent) { logger.debug(`${lockFileName} is unchanged`); return null; diff --git a/lib/manager/poetry/extract.spec.ts b/lib/manager/poetry/extract.spec.ts index 0eb4e7188da9283becffffd04aad128885434f98..f4af91e1f54f0fc336e3523697b5d4cf8935ebaa 100644 --- a/lib/manager/poetry/extract.spec.ts +++ b/lib/manager/poetry/extract.spec.ts @@ -61,7 +61,11 @@ describe('lib/manager/poetry/extract', () => { it('extracts multiple dependencies', () => { const res = extractPackageFile(pyproject1toml, filename); expect(res.deps).toMatchSnapshot(); - expect(res.deps).toHaveLength(9); + expect(res.deps).toHaveLength(10); + expect(res.compatibility).toEqual({ + poetry: 'poetry>=1.0 wheel', + python: '~2.7 || ^3.4', + }); }); it('extracts multiple dependencies (with dep = {version = "1.2.3"} case)', () => { const res = extractPackageFile(pyproject2toml, filename); diff --git a/lib/manager/poetry/extract.ts b/lib/manager/poetry/extract.ts index 1ff6ad88b440c9b8b3b075af572146d995a21206..d591d0dfae62cf02c0b2e93e18abbbbc87ac10c9 100644 --- a/lib/manager/poetry/extract.ts +++ b/lib/manager/poetry/extract.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import { parse } from 'toml'; import * as datasourcePypi from '../../datasource/pypi'; import { logger } from '../../logger'; @@ -111,5 +112,22 @@ export function extractPackageFile( return null; } - return { deps, registryUrls: extractRegistries(pyprojectfile) }; + const compatibility: Record<string, any> = {}; + + // https://python-poetry.org/docs/pyproject/#poetry-and-pep-517 + if ( + pyprojectfile['build-system']?.['build-backend'] === 'poetry.masonry.api' + ) { + compatibility.poetry = pyprojectfile['build-system']?.requires.join(' '); + } + + if (is.nonEmptyString(pyprojectfile.tool?.poetry?.['dependencies']?.python)) { + compatibility.python = pyprojectfile.tool?.poetry?.['dependencies']?.python; + } + + return { + deps, + registryUrls: extractRegistries(pyprojectfile), + compatibility, + }; } diff --git a/lib/manager/poetry/types.ts b/lib/manager/poetry/types.ts index 1e8189433ab510074192859f72528ef899b4da71..43330ece85b05c7677158e63396d3cb098a99734 100644 --- a/lib/manager/poetry/types.ts +++ b/lib/manager/poetry/types.ts @@ -9,6 +9,11 @@ export interface PoetryFile { tool?: { poetry?: PoetrySection; }; + + 'build-system'?: { + requires: string[]; + 'build-backend'?: string; + }; } export interface PoetryDependency {