diff --git a/lib/config/definitions.js b/lib/config/definitions.js index bb844f547fb0385600e280a298f54536c58aacba..c45e3f149af5b29014c3eca350b8386c0e78f7e5 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -320,6 +320,16 @@ const options = [ mergeable: true, cli: false, }, + { + name: 'registryUrls', + description: + 'List of URLs to try for dependency lookup. Package manager-specific', + type: 'list', + default: null, + stage: 'package', + cli: false, + env: false, + }, // depType { name: 'ignoreDeps', diff --git a/lib/datasource/pypi.js b/lib/datasource/pypi.js index 69da41203799805cbb929ccb65a3cfe3eed06c31..9bdda93008b2424264f16d3372d173f83623b44b 100644 --- a/lib/datasource/pypi.js +++ b/lib/datasource/pypi.js @@ -1,15 +1,22 @@ const got = require('got'); const { isVersion, sortVersions } = require('../versioning')('pep440'); +const url = require('url'); +const is = require('@sindresorhus/is'); module.exports = { getDependency, }; -async function getDependency(purl) { +async function getDependency(purl, config = {}) { const { fullname: depName } = purl; + let hostUrl = 'https://pypi.org/pypi/'; + if (!is.empty(config.registryUrls)) { + [hostUrl] = config.registryUrls; + } + const lookupUrl = url.resolve(hostUrl, `${depName}/json`); try { const dependency = {}; - const rep = await got(`https://pypi.org/pypi/${depName}/json`, { + const rep = await got(lookupUrl, { json: true, }); const dep = rep && rep.body; diff --git a/lib/manager/pip_requirements/extract.js b/lib/manager/pip_requirements/extract.js index b7283f4529d133f19285f2290255dd90934f75b4..cd3e7a336739c75987ea103741424dc9f71f62f4 100644 --- a/lib/manager/pip_requirements/extract.js +++ b/lib/manager/pip_requirements/extract.js @@ -16,6 +16,14 @@ module.exports = { function extractDependencies(content) { logger.debug('pip_requirements.extractDependencies()'); + let registryUrls; + content.split('\n').forEach(line => { + if (line.startsWith('--index-url ')) { + const registryUrl = line.substring('--index-url '.length); + registryUrls = [registryUrl]; + } + }); + const regex = new RegExp(`^(${packagePattern})(${specifierPattern})$`, 'g'); const deps = content .split('\n') @@ -26,13 +34,17 @@ function extractDependencies(content) { return null; } const [, depName, currentValue] = matches; - return { + const dep = { depName, currentValue, lineNumber, purl: 'pkg:pypi/' + depName, versionScheme: 'pep440', }; + if (registryUrls) { + dep.registryUrls = registryUrls; + } + return dep; }) .filter(Boolean); if (!deps.length) { diff --git a/test/datasource/__snapshots__/pypi.spec.js.snap b/test/datasource/__snapshots__/pypi.spec.js.snap index 32eece8d04c0a3a21c8619b7ac5e09b3748b5f69..1ef2c124950fa47565a7c24811f07a37398d4946 100644 --- a/test/datasource/__snapshots__/pypi.spec.js.snap +++ b/test/datasource/__snapshots__/pypi.spec.js.snap @@ -102,3 +102,14 @@ Object { "releases": Array [], } `; + +exports[`datasource/pypi getDependency supports custom datasource url 1`] = ` +Array [ + Array [ + "https://custom.pypi.net/azure-cli-monitor/json", + Object { + "json": true, + }, + ], +] +`; diff --git a/test/datasource/pypi.spec.js b/test/datasource/pypi.spec.js index e8bc3da2d585adcbbc59d616192a42e6cf5aef09..bc2d62c51c873ca8a24a3796551d1e4cd4fa764b 100644 --- a/test/datasource/pypi.spec.js +++ b/test/datasource/pypi.spec.js @@ -8,6 +8,9 @@ const res1 = fs.readFileSync('test/_fixtures/pypi/azure-cli-monitor.json'); describe('datasource/pypi', () => { describe('getDependency', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); it('returns null for empty result', async () => { got.mockReturnValueOnce({}); expect(await datasource.getDependency('pkg:pypi/something')).toBeNull(); @@ -26,6 +29,16 @@ describe('datasource/pypi', () => { await datasource.getDependency('pkg:pypi/azure-cli-monitor') ).toMatchSnapshot(); }); + it('supports custom datasource url', async () => { + got.mockReturnValueOnce({ + body: JSON.parse(res1), + }); + const config = { + registryUrls: ['https://custom.pypi.net/foo'], + }; + await datasource.getDependency('pkg:pypi/azure-cli-monitor', config); + expect(got.mock.calls).toMatchSnapshot(); + }); it('returns non-github home_page', async () => { got.mockReturnValueOnce({ body: { diff --git a/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap b/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap index 76806148973d207b39e3bf34e9552b0d061417f4..c366526d88cc18be53f125f25ec8c2d5f6b39006 100644 --- a/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap +++ b/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap @@ -7,6 +7,9 @@ Array [ "depName": "some-package", "lineNumber": 2, "purl": "pkg:pypi/some-package", + "registryUrls": Array [ + "http://example.com/private-pypi/", + ], "versionScheme": "pep440", }, Object { @@ -14,6 +17,9 @@ Array [ "depName": "some-other-package", "lineNumber": 3, "purl": "pkg:pypi/some-other-package", + "registryUrls": Array [ + "http://example.com/private-pypi/", + ], "versionScheme": "pep440", }, Object { @@ -21,6 +27,9 @@ Array [ "depName": "not_semver", "lineNumber": 4, "purl": "pkg:pypi/not_semver", + "registryUrls": Array [ + "http://example.com/private-pypi/", + ], "versionScheme": "pep440", }, ] diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md index 87ad02bd5e5bc7c98e7eef59fb3db96bf20c0ce5..30bd58f7c4da993f5ffcc9aaf45544633c0c1615 100644 --- a/website/docs/configuration-options.md +++ b/website/docs/configuration-options.md @@ -553,6 +553,10 @@ By default, Renovate will detect if it has proposed an update to a project befor Typically you shouldn't need to modify this setting. +## registryUrls + +This is only necessary in case you need to manually configure a registry URL to use for datasource lookups. Applies to PyPI (pip) only for now. Supports only one URL for now but is defined as a list for forwards compatibility. + ## renovateFork By default, Renovate will skip over any repositories that are forked, even if they contain a `renovate.json`, because that config may have been from the source repository. To enable Renovate on forked repositories, you need to add `renovateFork: true` to your renovate config. diff --git a/website/docs/python.md b/website/docs/python.md index c05a9033b176ba0388b1b80fb0ddb47900f1221c..753f3ad7bc395651459a899b88799fd810c3a769 100644 --- a/website/docs/python.md +++ b/website/docs/python.md @@ -28,6 +28,32 @@ The default file matching regex for requirements.txt aims to pick up the most po } ``` +## Alternate registries + +Renovate will default to performing all lookups on pypi.org, but it also supports alternative index URLs. There are two ways to achieve this: + +#### index-url in `requirements.txt` + +The index URL can be specified in the first line of the file, For example: + +``` +--index-url http://example.com/private-pypi/ +some-package==0.3.1 +some-other-package==1.0.0 +``` + +#### Specify URL in configuration + +The configuration option `registryUrls` can be used to configure an alternate index URL. Example: + +```json + "python": { + "registryUrls": ["http://example.com/private-pypi/"] + } +``` + +Note: an index-url found in the `requirements.txt` will take precedent over a registryUrl configured like the above. To override the URL found in `requirements.txt`, you need to configure it in `packageRules`, as they are applied _after_ package file extraction. + ## Disabling Python Support The most direct way to disable all Python support in Renovate is like this: