diff --git a/news/6121.feature b/news/6121.feature new file mode 100644 index 000000000..afe1f3857 --- /dev/null +++ b/news/6121.feature @@ -0,0 +1,2 @@ +Include the wheel's tags in the log message explanation when a candidate +wheel link is found incompatible. diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 62845b657..5b42a7c72 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -384,7 +384,15 @@ class CandidateEvaluator(object): return (False, reason) if not self._is_wheel_supported(wheel): - return (False, 'it is not compatible with this Python') + # Include the wheel's tags in the reason string to + # simplify troubleshooting compatibility issues. + file_tags = wheel.get_formatted_file_tags() + reason = ( + "none of the wheel's tags match: {}".format( + ', '.join(file_tags) + ) + ) + return (False, reason) version = wheel.version @@ -1066,7 +1074,9 @@ class PackageFinder(object): def _log_skipped_link(self, link, reason): # type: (Link, str) -> None if link not in self._logged_links: - logger.debug('Skipping link %s; %s', link, reason) + # Put the link at the end so the reason is more visible and + # because the link string is usually very long. + logger.debug('Skipping link: %s: %s', reason, link) self._logged_links.add(link) def get_install_candidate(self, link, search): diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 18f25df4a..970dadf8f 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -663,6 +663,16 @@ def check_compatibility(version, name): ) +def format_tag(file_tag): + # type: (Tuple[str, ...]) -> str + """ + Format three tags in the form "--". + + :param file_tag: A 3-tuple of tags (python_tag, abi_tag, platform_tag). + """ + return '-'.join(file_tag) + + class Wheel(object): """A wheel file""" @@ -702,6 +712,13 @@ class Wheel(object): for y in self.abis for z in self.plats } + def get_formatted_file_tags(self): + # type: () -> List[str] + """ + Return the wheel's tags as a sorted list of strings. + """ + return sorted(format_tag(tag) for tag in self.file_tags) + def support_index_min(self, tags=None): # type: (Optional[List[Pep425Tag]]) -> Optional[int] """ diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index cf405ca3f..a5d842c1e 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -105,7 +105,10 @@ def test_command_line_append_flags(script, virtualenv, data): "Analyzing links from page https://test.pypi.org" in result.stdout ) - assert "Skipping link %s" % data.find_links in result.stdout + assert ( + 'Skipping link: not a file: {}'.format(data.find_links) in + result.stdout + ), 'stdout: {}'.format(result.stdout) @pytest.mark.network @@ -127,7 +130,10 @@ def test_command_line_appends_correctly(script, data): "Analyzing links from page https://test.pypi.org" in result.stdout ), result.stdout - assert "Skipping link %s" % data.find_links in result.stdout + assert ( + 'Skipping link: not a file: {}'.format(data.find_links) in + result.stdout + ), 'stdout: {}'.format(result.stdout) def test_config_file_override_stack(script, virtualenv): diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index d8f40e0dd..3512b93a8 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -133,10 +133,7 @@ class TestWheel: with pytest.raises(DistributionNotFound): finder.find_requirement(req, True) - assert ( - "invalid.whl; invalid wheel filename" - in caplog.text - ) + assert 'Skipping link: invalid wheel filename:' in caplog.text def test_not_find_wheel_not_supported(self, data, monkeypatch): """ diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 4323bc405..094c23af4 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -130,6 +130,24 @@ class TestCandidateEvaluator: actual = evaluator.evaluate_link(link, search=search) assert actual == expected + def test_evaluate_link__incompatible_wheel(self): + """ + Test an incompatible wheel. + """ + link = Link('https://example.com/sample-1.0-py2.py3-none-any.whl') + search = Search( + supplied='sample', canonical='sample', formats=['binary'], + ) + # Pass an empty list for the valid tags to make sure nothing matches. + evaluator = CandidateEvaluator( + [], py_version_info=(3, 6, 4), + ) + actual = evaluator.evaluate_link(link, search=search) + expected = ( + False, "none of the wheel's tags match: py2-none-any, py3-none-any" + ) + assert actual == expected + def test_sort_locations_file_expand_dir(data): """ diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 14cde104b..cae9ef35e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -68,6 +68,15 @@ def make_test_install_req(base_name=None): return req +@pytest.mark.parametrize('file_tag, expected', [ + (('py27', 'none', 'any'), 'py27-none-any'), + (('cp33', 'cp32dmu', 'linux_x86_64'), 'cp33-cp32dmu-linux_x86_64'), +]) +def test_format_tag(file_tag, expected): + actual = wheel.format_tag(file_tag) + assert actual == expected + + @pytest.mark.parametrize( "base_name, autobuilding, cache_available, expected", [