diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index dc1912d360eae14bb20b6a33117ddb045118055b..a8ea74ea3a5af0bcce5f6639242209463e3c0da7 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1599,7 +1599,7 @@ Behavior: - `bump` = e.g. bump the range even if the new version satisfies the existing range, e.g. `^1.0.0` -> `^1.1.0` - `replace` = Replace the range with a newer one if the new version falls outside it, e.g. `^1.0.0` -> `^2.0.0` - `widen` = Widen the range with newer one, e.g. `^1.0.0` -> `^1.0.0 || ^2.0.0` -- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `composer`, `npm`, and `yarn`, so far +- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `composer`, `npm`, `yarn` and `poetry` so far Renovate's `"auto"` strategy works like this for npm: diff --git a/lib/manager/poetry/__fixtures__/pyproject.11.toml b/lib/manager/poetry/__fixtures__/pyproject.11.toml new file mode 100644 index 0000000000000000000000000000000000000000..bf71d8e580bf5222bd3e6c68190d482822551c47 --- /dev/null +++ b/lib/manager/poetry/__fixtures__/pyproject.11.toml @@ -0,0 +1,3 @@ +[tool.poetry.dependencies] +python = "^3.9" +boto3 = "*" diff --git a/lib/manager/poetry/__fixtures__/pyproject.11.toml.lock b/lib/manager/poetry/__fixtures__/pyproject.11.toml.lock new file mode 100644 index 0000000000000000000000000000000000000000..5768c71a3d674886e9065a8f03dd777770426b81 --- /dev/null +++ b/lib/manager/poetry/__fixtures__/pyproject.11.toml.lock @@ -0,0 +1,76 @@ +[[package]] +name = "boto3" +version = "1.17.5" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +botocore = ">=1.20.7,<1.21.0" +jmespath = ">=0.7.1,<1.0.0" +s3transfer = ">=0.3.0,<0.4.0" + +[[package]] +name = "botocore" +version = "1.20.7" +description = "Low-level, data-driven core of boto 3." +category = "main" +optional = false +python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +jmespath = ">=0.7.1,<1.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[[package]] +name = "jmespath" +version = "0.10.0" +description = "JSON Matching Expressions" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "s3transfer" +version = "0.3.4" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] diff --git a/lib/manager/poetry/__snapshots__/extract.spec.ts.snap b/lib/manager/poetry/__snapshots__/extract.spec.ts.snap index 3b0748c87d72004041a8ba16c5849d8bc9b3a1c5..507777ba84914ecbe411a13a4497f678e3e1b88a 100644 --- a/lib/manager/poetry/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/poetry/__snapshots__/extract.spec.ts.snap @@ -548,3 +548,25 @@ Array [ }, ] `; + +exports[`lib/manager/poetry/extract extractPackageFile() resolves lockedVersions from the lockfile 1`] = ` +Object { + "constraints": Object { + "python": "^3.9", + }, + "deps": Array [ + Object { + "currentValue": "*", + "datasource": "pypi", + "depName": "boto3", + "depType": "dependencies", + "lockedVersion": "1.17.5", + "managerData": Object { + "nestedVersion": false, + }, + "versioning": "poetry", + }, + ], + "registryUrls": null, +} +`; diff --git a/lib/manager/poetry/extract.spec.ts b/lib/manager/poetry/extract.spec.ts index b91ca400507a3ffdb85000d2082cf21a4e9a2dc9..a66d7904eebf374092f0c490397ab4570bf49fac 100644 --- a/lib/manager/poetry/extract.spec.ts +++ b/lib/manager/poetry/extract.spec.ts @@ -1,6 +1,9 @@ import { readFileSync } from 'fs'; +import { fs } from '../../../test/util'; import { extractPackageFile } from './extract'; +jest.mock('../../util/fs'); + const pyproject1toml = readFileSync( 'lib/manager/poetry/__fixtures__/pyproject.1.toml', 'utf8' @@ -46,6 +49,18 @@ const pyproject9toml = readFileSync( 'utf8' ); +// pyproject.10.toml use by artifacts + +const pyproject11toml = readFileSync( + 'lib/manager/poetry/__fixtures__/pyproject.11.toml', + 'utf8' +); + +const pyproject11tomlLock = readFileSync( + 'lib/manager/poetry/__fixtures__/pyproject.11.toml.lock', + 'utf8' +); + describe('lib/manager/poetry/extract', () => { describe('extractPackageFile()', () => { let filename: string; @@ -58,14 +73,14 @@ describe('lib/manager/poetry/extract', () => { afterEach(() => { process.env = OLD_ENV; }); - it('returns null for empty', () => { - expect(extractPackageFile('nothing here', filename)).toBeNull(); + it('returns null for empty', async () => { + expect(await extractPackageFile('nothing here', filename)).toBeNull(); }); - it('returns null for parsed file without poetry section', () => { - expect(extractPackageFile(pyproject5toml, filename)).toBeNull(); + it('returns null for parsed file without poetry section', async () => { + expect(await extractPackageFile(pyproject5toml, filename)).toBeNull(); }); - it('extracts multiple dependencies', () => { - const res = extractPackageFile(pyproject1toml, filename); + it('extracts multiple dependencies', async () => { + const res = await extractPackageFile(pyproject1toml, filename); expect(res.deps).toMatchSnapshot(); expect(res.deps).toHaveLength(9); expect(res.constraints).toEqual({ @@ -73,72 +88,77 @@ describe('lib/manager/poetry/extract', () => { python: '~2.7 || ^3.4', }); }); - it('extracts multiple dependencies (with dep = {version = "1.2.3"} case)', () => { - const res = extractPackageFile(pyproject2toml, filename); + it('extracts multiple dependencies (with dep = {version = "1.2.3"} case)', async () => { + const res = await extractPackageFile(pyproject2toml, filename); expect(res.deps).toMatchSnapshot(); expect(res.deps).toHaveLength(7); }); - it('handles case with no dependencies', () => { - const res = extractPackageFile(pyproject3toml, filename); + it('handles case with no dependencies', async () => { + const res = await extractPackageFile(pyproject3toml, filename); expect(res).toBeNull(); }); - it('handles multiple constraint dependencies', () => { - const res = extractPackageFile(pyproject4toml, filename); + it('handles multiple constraint dependencies', async () => { + const res = await extractPackageFile(pyproject4toml, filename); expect(res.deps).toMatchSnapshot(); expect(res.deps).toHaveLength(1); }); - it('extracts registries', () => { - const res = extractPackageFile(pyproject6toml, filename); + it('extracts registries', async () => { + const res = await extractPackageFile(pyproject6toml, filename); expect(res.registryUrls).toMatchSnapshot(); expect(res.registryUrls).toHaveLength(3); }); - it('can parse empty registries', () => { - const res = extractPackageFile(pyproject7toml, filename); + it('can parse empty registries', async () => { + const res = await extractPackageFile(pyproject7toml, filename); expect(res.registryUrls).toBeNull(); }); - it('can parse missing registries', () => { - const res = extractPackageFile(pyproject1toml, filename); + it('can parse missing registries', async () => { + const res = await extractPackageFile(pyproject1toml, filename); expect(res.registryUrls).toBeNull(); }); - it('dedupes registries', () => { - const res = extractPackageFile(pyproject8toml, filename); + it('dedupes registries', async () => { + const res = await extractPackageFile(pyproject8toml, filename); expect(res.registryUrls).toMatchSnapshot(); }); - it('extracts mixed versioning types', () => { - const res = extractPackageFile(pyproject9toml, filename); + it('extracts mixed versioning types', async () => { + const res = await extractPackageFile(pyproject9toml, filename); + expect(res).toMatchSnapshot(); + }); + it('resolves lockedVersions from the lockfile', async () => { + fs.readLocalFile.mockResolvedValue(pyproject11tomlLock); + const res = await extractPackageFile(pyproject11toml, filename); expect(res).toMatchSnapshot(); }); - it('skips git dependencies', () => { + it('skips git dependencies', async () => { const content = '[tool.poetry.dependencies]\r\nflask = {git = "https://github.com/pallets/flask.git"}\r\nwerkzeug = ">=0.14"'; - const res = extractPackageFile(content, filename).deps; + const res = (await extractPackageFile(content, filename)).deps; expect(res[0].depName).toBe('flask'); expect(res[0].currentValue).toBe(''); expect(res[0].skipReason).toBe('git-dependency'); expect(res).toHaveLength(2); }); - it('skips git dependencies with version', () => { + it('skips git dependencies with version', async () => { const content = '[tool.poetry.dependencies]\r\nflask = {git = "https://github.com/pallets/flask.git", version="1.2.3"}\r\nwerkzeug = ">=0.14"'; - const res = extractPackageFile(content, filename).deps; + const res = (await extractPackageFile(content, filename)).deps; expect(res[0].depName).toBe('flask'); expect(res[0].currentValue).toBe('1.2.3'); expect(res[0].skipReason).toBe('git-dependency'); expect(res).toHaveLength(2); }); - it('skips path dependencies', () => { + it('skips path dependencies', async () => { const content = '[tool.poetry.dependencies]\r\nflask = {path = "/some/path/"}\r\nwerkzeug = ">=0.14"'; - const res = extractPackageFile(content, filename).deps; + const res = (await extractPackageFile(content, filename)).deps; expect(res[0].depName).toBe('flask'); expect(res[0].currentValue).toBe(''); expect(res[0].skipReason).toBe('path-dependency'); expect(res).toHaveLength(2); }); - it('skips path dependencies with version', () => { + it('skips path dependencies with version', async () => { const content = '[tool.poetry.dependencies]\r\nflask = {path = "/some/path/", version = "1.2.3"}\r\nwerkzeug = ">=0.14"'; - const res = extractPackageFile(content, filename).deps; + const res = (await extractPackageFile(content, filename)).deps; expect(res[0].depName).toBe('flask'); expect(res[0].currentValue).toBe('1.2.3'); expect(res[0].skipReason).toBe('path-dependency'); diff --git a/lib/manager/poetry/extract.ts b/lib/manager/poetry/extract.ts index fb2bc1485326e38d3fbc6c5deab7876dfea69787..5b17c06e04e2673914bd4ea7fce1d05869fa5201 100644 --- a/lib/manager/poetry/extract.ts +++ b/lib/manager/poetry/extract.ts @@ -3,20 +3,28 @@ import is from '@sindresorhus/is'; import * as datasourcePypi from '../../datasource/pypi'; import { logger } from '../../logger'; import { SkipReason } from '../../types'; +import { getSiblingFileName, readLocalFile } from '../../util/fs'; import * as pep440Versioning from '../../versioning/pep440'; import * as poetryVersioning from '../../versioning/poetry'; import { PackageDependency, PackageFile } from '../common'; -import { PoetryFile, PoetrySection } from './types'; +import { + PoetryFile, + PoetryLock, + PoetryLockSection, + PoetrySection, +} from './types'; function extractFromSection( parsedFile: PoetryFile, - section: keyof PoetrySection + section: keyof PoetrySection, + poetryLockfile: Record<string, PoetryLockSection> ): PackageDependency[] { const deps = []; const sectionContent = parsedFile.tool.poetry[section]; if (!sectionContent) { return []; } + Object.keys(sectionContent).forEach((depName) => { if (depName === 'python') { return; @@ -55,6 +63,9 @@ function extractFromSection( managerData: { nestedVersion }, datasource: datasourcePypi.id, }; + if (dep.depName in poetryLockfile) { + dep.lockedVersion = poetryLockfile[dep.depName].version; + } if (skipReason) { dep.skipReason = skipReason; } else if (pep440Versioning.isValid(dep.currentValue)) { @@ -87,10 +98,10 @@ function extractRegistries(pyprojectfile: PoetryFile): string[] { return Array.from(registryUrls); } -export function extractPackageFile( +export async function extractPackageFile( content: string, fileName: string -): PackageFile | null { +): Promise<PackageFile | null> { logger.trace(`poetry.extractPackageFile(${fileName})`); let pyprojectfile: PoetryFile; try { @@ -103,10 +114,30 @@ export function extractPackageFile( logger.debug(`${fileName} contains no poetry section`); return null; } + + // handle the lockfile + const lockfileName = getSiblingFileName(fileName, 'poetry.lock'); + const lockContents = await readLocalFile(lockfileName, 'utf8'); + + let poetryLockfile: PoetryLock; + try { + poetryLockfile = parse(lockContents); + } catch (err) { + logger.debug({ err }, 'Error parsing pyproject.toml file'); + } + + const lockfileMapping: Record<string, PoetryLockSection> = {}; + if (poetryLockfile?.package) { + // Create a package->PoetryLockSection mapping + for (const poetryPackage of poetryLockfile.package) { + lockfileMapping[poetryPackage.name] = poetryPackage; + } + } + const deps = [ - ...extractFromSection(pyprojectfile, 'dependencies'), - ...extractFromSection(pyprojectfile, 'dev-dependencies'), - ...extractFromSection(pyprojectfile, 'extras'), + ...extractFromSection(pyprojectfile, 'dependencies', lockfileMapping), + ...extractFromSection(pyprojectfile, 'dev-dependencies', lockfileMapping), + ...extractFromSection(pyprojectfile, 'extras', lockfileMapping), ]; if (!deps.length) { return null; diff --git a/lib/manager/poetry/types.ts b/lib/manager/poetry/types.ts index 43330ece85b05c7677158e63396d3cb098a99734..5f4803e48a248dd9e72cf14c9858adb6a079668e 100644 --- a/lib/manager/poetry/types.ts +++ b/lib/manager/poetry/types.ts @@ -26,3 +26,12 @@ export interface PoetrySource { name?: string; url?: string; } + +export interface PoetryLockSection { + name?: string; + version?: string; +} + +export interface PoetryLock { + package?: PoetryLockSection[]; +} diff --git a/lib/versioning/poetry/index.spec.ts b/lib/versioning/poetry/index.spec.ts index a6175e7abcc2223281dbb73cd7e3fb63054f2d3e..d9f882c53d5b03fb74a37ab8b8cd5960ec87a04b 100644 --- a/lib/versioning/poetry/index.spec.ts +++ b/lib/versioning/poetry/index.spec.ts @@ -13,6 +13,9 @@ describe('semver.isValid(input)', () => { it('should reject semver without dash', () => { expect(semver.isValid('1.2.3foo')).toBeFalsy(); }); + it('should work with wildcards', () => { + expect(semver.isValid('*')).toBeTruthy(); + }); it('should support ranges', () => { expect(semver.isValid('~1.2.3')).toBeTruthy(); expect(semver.isValid('^1.2.3')).toBeTruthy(); @@ -48,6 +51,9 @@ describe('semver.matches()', () => { expect(semver.matches('4.2.0', '4.3.0, 3.0.0')).toBe(false); expect(semver.matches('4.2.0', '> 5.0.0, <= 6.0.0')).toBe(false); }); + it('handles wildcards', () => { + expect(semver.matches('4.2.0', '*')).toBe(true); + }); }); describe('semver.isLessThanRange()', () => { it('handles comma', () => {