diff --git a/news/5780.bugfix b/news/5780.bugfix new file mode 100644 index 00000000000..c9aa72053cf --- /dev/null +++ b/news/5780.bugfix @@ -0,0 +1 @@ +Re-install PEP 508 dependencies when the URL changes. diff --git a/news/6402.bugfix b/news/6402.bugfix new file mode 100644 index 00000000000..c9aa72053cf --- /dev/null +++ b/news/6402.bugfix @@ -0,0 +1 @@ +Re-install PEP 508 dependencies when the URL changes. diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 7e57f22f73b..fe6300cf216 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -434,6 +434,21 @@ def install_req_from_req_string( "{} depends on {} ".format(comes_from.name, req) ) + if req.url: + # Create an InstallRequirement for a wheel-like PEP 508 URL with the + # same behavior as 'pip install req.url' + parts = parse_req_from_line(req.url, None) + link = Link(req.url) + if link.is_wheel: + return InstallRequirement( + parts.requirement, + comes_from=comes_from, + link=parts.link, + markers=parts.markers, + use_pep517=use_pep517, + isolated=isolated, + extras=req.extras, + ) return InstallRequirement( req, comes_from, isolated=isolated, use_pep517=use_pep517 ) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index a4bacfe9447..4d03ced5be2 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1592,6 +1592,37 @@ def test_install_pep508_with_url_in_install_requires(script): assert "Successfully installed packaging-15.3" in str(res), str(res) +def test_install_pep508_with_url_in_install_requires_url_change_wheel(script): + dep_v1_path = create_basic_wheel_for_package( + script, name='dep', version='1.0', + ) + dep_v2_path = create_basic_wheel_for_package( + script, name='dep', version='2.0', + ) + + pkga_path = create_basic_wheel_for_package( + script, name='pkga', version='1.0', + depends=['dep@' + path_to_url(dep_v1_path)], + ) + res = script.pip('install', pkga_path) + assert "Successfully installed dep-1.0" in str(res), str(res) + + pkga_path.unlink() + + # Updating the URL to the dependency installs the updated dependency + pkga_path = create_basic_wheel_for_package( + script, name='pkga', version='2.0', + depends=['dep@' + path_to_url(dep_v2_path)], + ) + res = script.pip('install', pkga_path) + assert "Successfully installed dep-2.0" in str(res), str(res) + + res = script.pip('install', pkga_path) + # pip can determine the version from a wheel's filename, so the + # dependency is not reinstalled if the URL doesn't change + assert "Requirement already satisfied: dep==2.0" in str(res), str(res) + + @pytest.mark.network @pytest.mark.parametrize('index', (PyPI.simple_url, TestPyPI.simple_url)) def test_install_from_test_pypi_with_ext_url_dep_is_blocked(script, index): diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py index c3482d5360a..ef0ab68858a 100644 --- a/tests/unit/test_req_install.py +++ b/tests/unit/test_req_install.py @@ -78,7 +78,7 @@ def test_install_req_from_string_without_comes_from(self): assert isinstance(install_req, InstallRequirement) assert install_req.link.url == wheel_url - assert install_req.req.url == wheel_url + assert install_req.req.url is None assert install_req.comes_from is None assert install_req.is_wheel @@ -106,5 +106,47 @@ def test_install_req_from_string_with_comes_from_without_link(self): assert isinstance(install_req, InstallRequirement) assert install_req.comes_from.link is None assert install_req.link.url == wheel_url - assert install_req.req.url == wheel_url + assert install_req.req.url is None assert install_req.is_wheel + + @pytest.mark.parametrize("use_pep517", [None, True, False]) + def test_install_req_from_string_pep508_url_wheel(self, use_pep517): + """ + install_req_from_string parses the version from PEP 508 URLs that point + to wheels so that updating the URL reinstalls the package. + """ + wheel_url = ("https://download.pytorch.org/whl/cu90/" + "torch-1.0.0-cp36-cp36m-win_amd64.whl") + install_str = "torch@ " + wheel_url + install_req = install_req_from_req_string( + install_str, use_pep517=use_pep517 + ) + + assert isinstance(install_req, InstallRequirement) + assert str(install_req.req) == "torch==1.0.0" + assert install_req.link.url == wheel_url + assert install_req.is_wheel + assert install_req.use_pep517 == use_pep517 + + @pytest.mark.parametrize("use_pep517", [None, True, False]) + def test_install_req_from_string_pep508_url_wheel_extras(self, use_pep517): + """ + install_req_from_string parses the version and extras from PEP 508 URLs + that point to wheels so that updating the URL reinstalls the package. + """ + wheel_url = ( + "https://files.pythonhosted.org/packages/51/bd/" + "23c926cd341ea6b7dd0b2a00aba99ae0f828be89d72b2190f27c11d4b7fb/" + "requests-2.22.0-py2.py3-none-any.whl" + ) + install_str = "requests[security] @ " + wheel_url + install_req = install_req_from_req_string( + install_str, use_pep517=use_pep517 + ) + + assert isinstance(install_req, InstallRequirement) + assert str(install_req.req) == "requests==2.22.0" + assert install_req.link.url == wheel_url + assert install_req.is_wheel + assert install_req.use_pep517 == use_pep517 + assert install_req.extras == {"security"}