diff --git a/lib/datasource/pypi.js b/lib/datasource/pypi.js index dd35de0932465b589d8c208a93d2c06c7818c8b6..04dd44c6a08c039659fe86809cd29daa824c584b 100644 --- a/lib/datasource/pypi.js +++ b/lib/datasource/pypi.js @@ -1,5 +1,5 @@ const got = require('got'); -const { isVersion, sortVersions } = require('../versioning')('semver'); +const { isVersion, sortVersions } = require('../versioning')('pep440'); module.exports = { getDependency, @@ -19,7 +19,7 @@ async function getDependency(purl) { } if (dep.info && dep.info.home_page) { if (dep.info.home_page.startsWith('https://github.com')) { - dependency.repository_url = dep.info.home_page; + dependency.repositoryUrl = dep.info.home_page; } else { dependency.homepage = dep.info.home_page; } diff --git a/lib/manager/pip_requirements/extract.js b/lib/manager/pip_requirements/extract.js index ef5cc7489126c720347d2de5e221ae0bd23e54f0..b7283f4529d133f19285f2290255dd90934f75b4 100644 --- a/lib/manager/pip_requirements/extract.js +++ b/lib/manager/pip_requirements/extract.js @@ -1,7 +1,12 @@ -const XRegExp = require('xregexp'); // based on https://www.python.org/dev/peps/pep-0508/#names const packagePattern = '[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]'; -const versionPattern = require('@renovate/pep440/lib/version').VERSION_PATTERN; +const rangePattern = require('@renovate/pep440/lib/specifier').RANGE_PATTERN; + +const specifierPartPattern = `\\s*${rangePattern.replace( + /\?<\w+>/g, + '?:' +)}\\s*`; +const specifierPattern = `${specifierPartPattern}(?:,${specifierPartPattern})*`; module.exports = { packagePattern, @@ -10,9 +15,8 @@ module.exports = { function extractDependencies(content) { logger.debug('pip_requirements.extractDependencies()'); - // TODO: for now we only support package==version - // future support for more complex ranges could be added later - const regex = new XRegExp(`^(${packagePattern})(==${versionPattern})$`, 'g'); + + const regex = new RegExp(`^(${packagePattern})(${specifierPattern})$`, 'g'); const deps = content .split('\n') .map((line, lineNumber) => { diff --git a/lib/versioning/pep440/range.js b/lib/versioning/pep440/range.js index dbfa449b2a84f1638a72d76e63a731fa8eae09d8..29c7a6b34944a4300b0c6f2df7aaeabeee8bdbe8 100644 --- a/lib/versioning/pep440/range.js +++ b/lib/versioning/pep440/range.js @@ -1,11 +1,128 @@ +const { gte, lte, satisfies } = require('@renovate/pep440'); + +const { parse: parseVersion } = require('@renovate/pep440/lib/version'); +const { parse: parseRange } = require('@renovate/pep440/lib/specifier'); + module.exports = { getNewValue, }; +function getFutureVersion(baseVersion, toVersion, step) { + const toRelease = parseVersion(toVersion).release; + const baseRelease = parseVersion(baseVersion).release; + let found = false; + const futureRelease = baseRelease.map((basePart, index) => { + if (found) { + return 0; + } + const toPart = toRelease[index] || 0; + if (toPart > basePart) { + found = true; + return toPart + step; + } + return toPart; + }); + if (!found) { + futureRelease[futureRelease.length - 1] += step; + } + return futureRelease.join('.'); +} + function getNewValue(currentValue, rangeStrategy, fromVersion, toVersion) { - if (rangeStrategy === 'pin' || currentValue.startsWith('==')) { + // easy pin + if (rangeStrategy === 'pin') { return '==' + toVersion; } - logger.warn('Unsupported currentValue: ' + currentValue); - return toVersion; + const ranges = parseRange(currentValue); + if (!ranges) { + logger.warn('Invalid currentValue: ' + currentValue); + return null; + } + if (!ranges.length) { + // an empty string is an allowed value for PEP440 range + // it means get any version + logger.warn('Empty currentValue: ' + currentValue); + return currentValue; + } + if (rangeStrategy === 'replace') { + if (satisfies(toVersion, currentValue)) { + return currentValue; + } + } + if (!['replace', 'bump'].includes(rangeStrategy)) { + logger.warn('Unsupported rangeStrategy: ' + rangeStrategy); + return null; + } + if (ranges.some(range => range.operator === '===')) { + // the operator "===" is used for legacy non PEP440 versions + logger.warn('Arbitrary equality not supported: ' + currentValue); + return null; + } + const result = ranges + .map(range => { + // used to exclude versions, + // we assume that's for a good reason + if (range.operator === '!=') { + return range.operator + range.version; + } + + // used to mark minimum supported version + if (['>', '>='].includes(range.operator)) { + if (lte(toVersion, range.version)) { + // this looks like a rollback + return '>=' + toVersion; + } + // this is similar to ~= + if (rangeStrategy === 'bump' && range.operator === '>=') { + return range.operator + toVersion; + } + // otherwise treat it same as exclude + return range.operator + range.version; + } + + // this is used to exclude future versions + if (range.operator === '<') { + // if toVersion is that future version + if (gte(toVersion, range.version)) { + // now here things get tricky + // we calculate the new future version + const futureVersion = getFutureVersion(range.version, toVersion, 1); + return range.operator + futureVersion; + } + // otherwise treat it same as exclude + return range.operator + range.version; + } + + // keep the .* suffix + if (range.prefix) { + const futureVersion = getFutureVersion(range.version, toVersion, 0); + return range.operator + futureVersion + '.*'; + } + + if (['==', '~=', '<='].includes(range.operator)) { + return range.operator + toVersion; + } + + // unless PEP440 changes, this won't happen + // istanbul ignore next + logger.error( + { toVersion, currentValue, range }, + 'pep440: failed to process range' + ); + // istanbul ignore next + return null; + }) + .filter(Boolean) + .join(', '); + + if (!satisfies(toVersion, result)) { + // we failed at creating the range + logger.error( + { result, toVersion, currentValue }, + 'pep440: failed to calcuate newValue' + ); + return null; + } + + return result; } diff --git a/package.json b/package.json index 6267ef63f095bcdd3063a3a27b1c1486e81be7a3..5870437328df7ec552348cf5ed739214bd7fe2f6 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,6 @@ "upath": "1.1.0", "validator": "10.3.0", "vso-node-api": "6.5.0", - "xregexp": "4.2.0", "yarn": "1.7.0" }, "devDependencies": { diff --git a/test/datasource/__snapshots__/pypi.spec.js.snap b/test/datasource/__snapshots__/pypi.spec.js.snap index bced52c0d137984f2153137d848575609b3c5163..df0949e1fe7d7059c83de3af51c13e39e2234beb 100644 --- a/test/datasource/__snapshots__/pypi.spec.js.snap +++ b/test/datasource/__snapshots__/pypi.spec.js.snap @@ -92,7 +92,7 @@ Object { "version": "0.1.7", }, ], - "repository_url": "https://github.com/Azure/azure-cli", + "repositoryUrl": "https://github.com/Azure/azure-cli", } `; diff --git a/test/versioning/pep440.spec.js b/test/versioning/pep440.spec.js index 83998bb82fc22d8555ebcdd9ea9759499b8e5b63..e7f877414ab8b74e80628dec7f2fb49c5239ff2d 100644 --- a/test/versioning/pep440.spec.js +++ b/test/versioning/pep440.spec.js @@ -79,14 +79,64 @@ describe('pep440.minSatisfyingVersion(versions, range)', () => { }); describe('pep440.getNewValue()', () => { - it('returns double equals', () => { - expect(pep440.getNewValue('==1.0.0', 'replace', '1.0.0', '1.0.1')).toBe( - '==1.0.1' - ); - }); - it('returns version', () => { - expect(pep440.getNewValue('>=1.0.0', 'replace', '1.0.0', '1.0.1')).toBe( - '1.0.1' - ); + const { getNewValue } = pep440; + + // cases: [currentValue, expectedBump] + [ + // simple cases + ['==1.0.3', '==1.2.3'], + ['>=1.2.0', '>=1.2.3'], + ['~=1.2.0', '~=1.2.3'], + ['~=1.0.3', '~=1.2.3'], + + // glob + ['==1.2.*', '==1.2.*'], + ['==1.0.*', '==1.2.*'], + + // future versions guard + ['<1.2.2.3', '<1.2.4.0'], + ['<1.2.3', '<1.2.4'], + ['<1.2', '<1.3'], + ['<1', '<2'], + ['<2.0.0', '<2.0.0'], + + // minimum version guard + ['>0.9.8', '>0.9.8'], + // rollback + ['>2.0.0', '>=1.2.3'], + ['>=2.0.0', '>=1.2.3'], + + // complex ranges + ['~=1.1.0, !=1.1.1', '~=1.2.3, !=1.1.1'], + + // invalid & not supported + [' ', ' '], + ['invalid', null], + ['===1.0.3', null], + // impossible + ['!=1.2.3', null], + ].forEach(([currentValue, expectedBump]) => { + const bumped = getNewValue(currentValue, 'bump', '1.0.0', '1.2.3'); + it(`bumps '${currentValue}' to '${expectedBump}'`, () => { + expect(bumped).toBe(expectedBump); + }); + + const replaced = getNewValue(currentValue, 'replace', '1.0.0', '1.2.3'); + const needReplace = pep440.matches('1.2.3', currentValue); + const expectedReplace = needReplace ? currentValue : bumped; + it(`replaces '${currentValue}' to '${expectedReplace}'`, () => { + expect(replaced).toBe(expectedReplace); + }); + + const pinned = getNewValue(currentValue, 'pin', '1.0.0', '1.2.3'); + const expectedPin = '==1.2.3'; + it(`pins '${currentValue}' to '${expectedPin}'`, () => { + expect(pinned).toBe(expectedPin); + }); + }); + + it('guards against unsupported rangeStrategy', () => { + const invalid = getNewValue('==1.2.3', 'invalid', '1.0.0', '1.2.3'); + expect(invalid).toBe(null); }); }); diff --git a/yarn.lock b/yarn.lock index 52fab85ec47c1f122bb28e711ad787bbda98b70b..379e56d93dbfd84baf208c72e170d6c9af1966b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6615,10 +6615,6 @@ xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" -xregexp@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.2.0.tgz#33f09542b0d7cabed46728eeacac4d5bd764ccf5" - xregexp@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.1.1.tgz#eb8a032aa028d403f7b1b22c47a5f16c24b21d8d"