From fe40f1ef4862c8ed4b4a987e77fb365859f78748 Mon Sep 17 00:00:00 2001 From: rtaum <rtaum@users.noreply.github.com> Date: Thu, 7 Feb 2019 08:56:02 +0100 Subject: [PATCH] feat(python): add simple endpoint support (#3125) Closes #2970 --- lib/datasource/pypi/index.js | 57 +++++++++++++++- lib/manager/pipenv/extract.js | 4 +- .../_fixtures/pypi/versions-html-badfile.html | 11 ++++ test/_fixtures/pypi/versions-html.html | 23 +++++++ .../__snapshots__/pypi.spec.js.snap | 58 +++++++++++++--- test/datasource/pypi.spec.js | 66 +++++++++++++++++++ .../pipenv/__snapshots__/extract.spec.js.snap | 18 ++--- test/manager/pipenv/extract.spec.js | 11 ---- 8 files changed, 213 insertions(+), 35 deletions(-) create mode 100644 test/_fixtures/pypi/versions-html-badfile.html create mode 100644 test/_fixtures/pypi/versions-html.html diff --git a/lib/datasource/pypi/index.js b/lib/datasource/pypi/index.js index dae36548a3..124eaefe86 100644 --- a/lib/datasource/pypi/index.js +++ b/lib/datasource/pypi/index.js @@ -1,6 +1,6 @@ const url = require('url'); const is = require('@sindresorhus/is'); - +const { parse } = require('node-html-parser'); const { matches } = require('../../versioning/pep440'); const got = require('../../util/got'); @@ -35,8 +35,14 @@ async function getPkgReleases({ compatibility, lookupName, registryUrls }) { if (process.env.PIP_INDEX_URL) { hostUrls = [process.env.PIP_INDEX_URL]; } - for (const hostUrl of hostUrls) { - const dep = await getDependency(lookupName, hostUrl, compatibility); + for (let hostUrl of hostUrls) { + hostUrl += hostUrl.endsWith('/') ? '' : '/'; + let dep; + if (hostUrl.endsWith('/simple/')) { + dep = getSimpleDependency(lookupName, hostUrl); + } else { + dep = await getDependency(lookupName, hostUrl, compatibility); + } if (dep !== null) { return dep; } @@ -91,3 +97,48 @@ async function getDependency(depName, hostUrl, compatibility) { return null; } } + +async function getSimpleDependency(depName, hostUrl) { + const lookupUrl = url.resolve(hostUrl, `${depName}`); + try { + const dependency = {}; + const response = await got(url.parse(lookupUrl), { + json: false, + }); + const dep = response && response.body; + if (!dep) { + logger.debug({ dependency: depName }, 'pip package not found'); + return null; + } + const root = parse(dep); + const links = root.querySelectorAll('a'); + const versions = new Set(); + for (const link of links) { + const result = extractVersionFromLinkText(link.text); + if (result) { + versions.add(result); + } + } + dependency.releases = []; + if (versions && versions.size > 0) { + dependency.releases = [...versions].map(version => ({ + version, + })); + } + return dependency; + } catch (err) { + logger.info( + 'pypi dependency not found: ' + depName + '(searching in ' + hostUrl + ')' + ); + return null; + } +} + +function extractVersionFromLinkText(text) { + const versionRegexp = /\d+(\.\d+)+/; + const result = text.match(versionRegexp); + if (result && result.length > 0) { + return result[0]; + } + return null; +} diff --git a/lib/manager/pipenv/extract.js b/lib/manager/pipenv/extract.js index 17414ab15b..5f6840ce28 100644 --- a/lib/manager/pipenv/extract.js +++ b/lib/manager/pipenv/extract.js @@ -25,9 +25,7 @@ function extractPackageFile(content) { } let registryUrls; if (pipfile.source) { - registryUrls = pipfile.source.map(source => - source.url.replace(/simple(\/)?$/, 'pypi/') - ); + registryUrls = pipfile.source.map(source => source.url); } const deps = [ diff --git a/test/_fixtures/pypi/versions-html-badfile.html b/test/_fixtures/pypi/versions-html-badfile.html new file mode 100644 index 0000000000..a2155662d8 --- /dev/null +++ b/test/_fixtures/pypi/versions-html-badfile.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <title>Links for dj-database-url</title> + </head> + <body> + <h1>Links for dj-database-url</h1> + <a href="https://files.pythonhosted.org/packages/04/89/29cdbc86a0890a4f1e46b6f4bb9b7959e461e0202f6a305bd8b586cc1404/dj-database-url-0.1.2.tar.gz#sha256=6169f2c272326e3cced6999effb19013365ea73f6ed6c731efa4e346711d8969">dj-database-url.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/bd/80/f8430a065c09367cd766cdea08f80d11b625944a653f96c2bd02d183355b/dj-database-url-0.1.3.tar.gz#sha256=222744896dcbe939aa940217c940a8a95981be13beb9af639a1da024be4f9411">dj-database-url.tar.gz</a><br/> + </body> +</html> diff --git a/test/_fixtures/pypi/versions-html.html b/test/_fixtures/pypi/versions-html.html new file mode 100644 index 0000000000..4872820cc8 --- /dev/null +++ b/test/_fixtures/pypi/versions-html.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> + <head> + <title>Links for dj-database-url</title> + </head> + <body> + <h1>Links for dj-database-url</h1> + <a href="https://files.pythonhosted.org/packages/04/89/29cdbc86a0890a4f1e46b6f4bb9b7959e461e0202f6a305bd8b586cc1404/dj-database-url-0.1.2.tar.gz#sha256=6169f2c272326e3cced6999effb19013365ea73f6ed6c731efa4e346711d8969">dj-database-url-0.1.2.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/bd/80/f8430a065c09367cd766cdea08f80d11b625944a653f96c2bd02d183355b/dj-database-url-0.1.3.tar.gz#sha256=222744896dcbe939aa940217c940a8a95981be13beb9af639a1da024be4f9411">dj-database-url-0.1.3.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/3e/78/c18103be8b9f06a3cb9ee93adb63f91c16f8c8781c8abb0e5b06acef7106/dj-database-url-0.1.4.tar.gz#sha256=14faa143247e267aefd807490e1e89e3ad9fac0a06c4aee3f9fe328849bd15cf">dj-database-url-0.1.4.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/d6/cb/ab7fc10ea1e3571c34bcd74c3b3090e2d3c378d01ad79550fc967cd84114/dj-database-url-0.2.0.tar.gz#sha256=5edd253ccb407a0bd19e91c4c9bbe164632639767086b4a38f2d20e00010488b">dj-database-url-0.2.0.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/a6/94/72572715f45dd132ddc1e26aa4e8c2b13395759386a541e7f00124ceda11/dj-database-url-0.2.1.tar.gz#sha256=f95c0b2e9e70cc246bd101720e1be492524ecf0dd5ea39241b51ef142faefecc">dj-database-url-0.2.1.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/19/11/2867ccdaa0203ed14d9f725b26b6dd3264c4209a16d5bf0096597ecf9a7a/dj-database-url-0.2.2.tar.gz#sha256=492a7294b85ad8ac1b13be0b7337f381d2d44c4da185f289ab7c26dd765ef6cb">dj-database-url-0.2.2.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/e1/0e/2cceb7afb13cf784e385928530af59b49dd1524a76323e293f18ea28a6de/dj-database-url-0.3.0.tar.gz#sha256=f2e273ed34acbb560962d5cf12917936d8df02297df09bd3089b8546d4584138">dj-database-url-0.3.0.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/ef/b6/9283fcf61ced22bf90e7b4a84ba5b53d126b2c9b0dc9b667347698097026/dj_database_url-0.3.0-py2.py3-none-any.whl#sha256=ca01768fdecde134301f3170743226f60edff5c3935f12437378ebd911506353">dj_database_url-0.3.0-py2.py3-none-any.whl</a><br/> + <a href="https://files.pythonhosted.org/packages/64/d9/99774e3f66683ded1d3aa3f66045f671cefc0b550aca4ccfeaeeed4e074a/dj-database-url-0.4.0.tar.gz#sha256=858312abb7b330ea875733a65806a36ad04d7b8451c6ce8835118a2fa10d6870">dj-database-url-0.4.0.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/39/9f/30f937db9f9e7a4e4e3205682af4c34c65d647ff9850897ddfbbf5dc6178/dj-database-url-0.4.1.tar.gz#sha256=7f4c78d2a090df8dfaf56d5d3ff7bbee17360436e4879558317e2314424864cd">dj-database-url-0.4.1.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/c8/4b/b23dbcf4c5711f26e2222bb2e300915c9c8d35e643b0af00c2d8f36c9490/dj-database-url-0.4.2.tar.gz#sha256=a6832d8445ee9d788c5baa48aef8130bf61fdc442f7d9a548424d25cd85c9f08">dj-database-url-0.4.2.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/91/84/50cbfabb91593cff18a37046986f7c2eb69224a694a52ae614711dfa11c6/dj_database_url-0.4.2-py2.py3-none-any.whl#sha256=e16d94c382ea0564c48038fa7fe8d9c890ef1ab1a8ec4cb48e732c124b9482fd">dj_database_url-0.4.2-py2.py3-none-any.whl</a><br/> + <a href="https://files.pythonhosted.org/packages/01/c4/98fbf678e810029be8078419f7bba626aafa2e81bc38748757db954c477c/dj-database-url-0.5.0.tar.gz#sha256=4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163">dj-database-url-0.5.0.tar.gz</a><br/> + <a href="https://files.pythonhosted.org/packages/d4/a6/4b8578c1848690d0c307c7c0596af2077536c9ef2a04d42b00fabaa7e49d/dj_database_url-0.5.0-py2.py3-none-any.whl#sha256=851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9">dj_database_url-0.5.0-py2.py3-none-any.whl</a><br/> + </body> +</html> diff --git a/test/datasource/__snapshots__/pypi.spec.js.snap b/test/datasource/__snapshots__/pypi.spec.js.snap index 4f248bf390..27ee1735f2 100644 --- a/test/datasource/__snapshots__/pypi.spec.js.snap +++ b/test/datasource/__snapshots__/pypi.spec.js.snap @@ -1,5 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`datasource/pypi getPkgReleases process data from simple endpoint 1`] = ` +Object { + "releases": Array [ + Object { + "version": "0.1.2", + }, + Object { + "version": "0.1.3", + }, + Object { + "version": "0.1.4", + }, + Object { + "version": "0.2.0", + }, + Object { + "version": "0.2.1", + }, + Object { + "version": "0.2.2", + }, + Object { + "version": "0.3.0", + }, + Object { + "version": "0.4.0", + }, + Object { + "version": "0.4.1", + }, + Object { + "version": "0.4.2", + }, + Object { + "version": "0.5.0", + }, + ], +} +`; + exports[`datasource/pypi getPkgReleases processes real data 1`] = ` Object { "releases": Array [ @@ -130,9 +170,9 @@ Array [ "hash": null, "host": "custom.pypi.net", "hostname": "custom.pypi.net", - "href": "https://custom.pypi.net/azure-cli-monitor/json", - "path": "/azure-cli-monitor/json", - "pathname": "/azure-cli-monitor/json", + "href": "https://custom.pypi.net/foo/azure-cli-monitor/json", + "path": "/foo/azure-cli-monitor/json", + "pathname": "/foo/azure-cli-monitor/json", "port": null, "protocol": "https:", "query": null, @@ -178,9 +218,9 @@ Array [ "hash": null, "host": "custom.pypi.net", "hostname": "custom.pypi.net", - "href": "https://custom.pypi.net/azure-cli-monitor/json", - "path": "/azure-cli-monitor/json", - "pathname": "/azure-cli-monitor/json", + "href": "https://custom.pypi.net/foo/azure-cli-monitor/json", + "path": "/foo/azure-cli-monitor/json", + "pathname": "/foo/azure-cli-monitor/json", "port": null, "protocol": "https:", "query": null, @@ -197,9 +237,9 @@ Array [ "hash": null, "host": "second-index", "hostname": "second-index", - "href": "https://second-index/azure-cli-monitor/json", - "path": "/azure-cli-monitor/json", - "pathname": "/azure-cli-monitor/json", + "href": "https://second-index/foo/azure-cli-monitor/json", + "path": "/foo/azure-cli-monitor/json", + "pathname": "/foo/azure-cli-monitor/json", "port": null, "protocol": "https:", "query": null, diff --git a/test/datasource/pypi.spec.js b/test/datasource/pypi.spec.js index d9cb78702a..86701a6dcf 100644 --- a/test/datasource/pypi.spec.js +++ b/test/datasource/pypi.spec.js @@ -5,6 +5,10 @@ const datasource = require('../../lib/datasource'); jest.mock('../../lib/util/got'); const res1 = fs.readFileSync('test/_fixtures/pypi/azure-cli-monitor.json'); +const htmlResponse = fs.readFileSync('test/_fixtures/pypi/versions-html.html'); +const badResponse = fs.readFileSync( + 'test/_fixtures/pypi/versions-html-badfile.html' +); describe('datasource/pypi', () => { describe('getPkgReleases', () => { @@ -160,5 +164,67 @@ describe('datasource/pypi', () => { }) ).toMatchSnapshot(); }); + it('process data from simple endpoint', async () => { + got.mockReturnValueOnce({ + body: htmlResponse + '', + }); + const config = { + registryUrls: ['https://pypi.org/simple/'], + }; + expect( + await datasource.getPkgReleases({ + ...config, + compatibility: { python: '2.7' }, + datasource: 'pypi', + depName: 'dj-database-url', + }) + ).toMatchSnapshot(); + }); + it('returns null for empty resonse', async () => { + got.mockReturnValueOnce({}); + const config = { + registryUrls: ['https://pypi.org/simple/'], + }; + expect( + await datasource.getPkgReleases({ + ...config, + compatibility: { python: '2.7' }, + datasource: 'pypi', + depName: 'dj-database-url', + }) + ).toBeNull(); + }); + it('returns null for 404 response from simple endpoint', async () => { + got.mockImplementationOnce(() => { + throw new Error(); + }); + const config = { + registryUrls: ['https://pypi.org/simple/'], + }; + expect( + await datasource.getPkgReleases({ + ...config, + compatibility: { python: '2.7' }, + datasource: 'pypi', + depName: 'dj-database-url', + }) + ).toBeNull(); + }); + it('returns null for response with no versions', async () => { + got.mockReturnValueOnce({ + body: badResponse + '', + }); + const config = { + registryUrls: ['https://pypi.org/simple/'], + }; + expect( + await datasource.getPkgReleases({ + ...config, + compatibility: { python: '2.7' }, + datasource: 'pypi', + depName: 'dj-database-url', + }) + ).toEqual({ releases: [] }); + }); }); }); diff --git a/test/manager/pipenv/__snapshots__/extract.spec.js.snap b/test/manager/pipenv/__snapshots__/extract.spec.js.snap index e19291857d..40c46af618 100644 --- a/test/manager/pipenv/__snapshots__/extract.spec.js.snap +++ b/test/manager/pipenv/__snapshots__/extract.spec.js.snap @@ -9,7 +9,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", "http://example.com/private-pypi/", ], }, @@ -20,7 +20,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", "http://example.com/private-pypi/", ], }, @@ -31,7 +31,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": true, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", "http://example.com/private-pypi/", ], }, @@ -42,7 +42,7 @@ Array [ "depType": "dev-packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", "http://example.com/private-pypi/", ], }, @@ -58,7 +58,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", ], }, Object { @@ -68,7 +68,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", ], }, Object { @@ -78,7 +78,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", ], }, Object { @@ -88,7 +88,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", ], }, Object { @@ -98,7 +98,7 @@ Array [ "depType": "packages", "pipenvNestedVersion": false, "registryUrls": Array [ - "https://pypi.org/pypi/", + "https://pypi.org/simple", ], }, ] diff --git a/test/manager/pipenv/extract.spec.js b/test/manager/pipenv/extract.spec.js index b4d9180ce4..578515708a 100644 --- a/test/manager/pipenv/extract.spec.js +++ b/test/manager/pipenv/extract.spec.js @@ -44,16 +44,5 @@ describe('lib/manager/pipenv/extract', () => { const res = extractPackageFile(content, config).deps; expect(res[0].registryUrls).toEqual(['source-url', 'other-source-url']); }); - it('converts simple-API URLs to JSON-API URLs', () => { - const content = - '[[source]]\r\nurl = "https://my-pypi/foo/simple/"\r\n' + - '[[source]]\r\nurl = "https://other-pypi/foo/simple"\r\n' + - '[packages]\r\nfoo = "==1.0.0"\r\n'; - const res = extractPackageFile(content, config).deps; - expect(res[0].registryUrls).toEqual([ - 'https://my-pypi/foo/pypi/', - 'https://other-pypi/foo/pypi/', - ]); - }); }); }); -- GitLab