From 3368819156e07006d6c83c514e1a04359c9da389 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sat, 13 Oct 2018 03:16:38 +0800 Subject: [PATCH] Split, simplify, and fix egg_info_matches --- news/5870.bugfix | 1 + src/pip/_internal/index.py | 29 ++++++++--------------------- src/pip/_internal/wheel.py | 12 ++++++++++-- tests/unit/test_index.py | 26 ++++++++++++-------------- tests/unit/test_wheel.py | 20 ++++++++++++++++++++ 5 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 news/5870.bugfix diff --git a/news/5870.bugfix b/news/5870.bugfix new file mode 100644 index 000000000..7498015ab --- /dev/null +++ b/news/5870.bugfix @@ -0,0 +1 @@ +Canonicalize sdist file names so they can be matched to a canonicalized package name passed to ``pip install``. diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 012e87a82..3a4faa964 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -815,7 +815,7 @@ class PackageFinder(object): return if not version: - version = egg_info_matches(egg_info, search.supplied, link) + version = _egg_info_matches(egg_info, search.canonical, link) if version is None: self._log_skipped_link( link, 'Missing project version for %s' % search.supplied) @@ -846,33 +846,20 @@ class PackageFinder(object): return InstallationCandidate(search.supplied, version, link) -def egg_info_matches( - egg_info, search_name, link, - _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): +def _egg_info_matches(egg_info, canonical_name, link): """Pull the version part out of a string. :param egg_info: The string to parse. E.g. foo-2.1 - :param search_name: The name of the package this belongs to. None to - infer the name. Note that this cannot unambiguously parse strings - like foo-2-2 which might be foo, 2-2 or foo-2, 2. + :param canonical_name: The canonicalized name of the package this + belongs to. :param link: The link the string came from, for logging on failure. """ - match = _egg_info_re.search(egg_info) - if not match: - logger.debug('Could not parse version from link: %s', link) + if not canonicalize_name(egg_info).startswith(canonical_name): return None - if search_name is None: - full_match = match.group(0) - return full_match.split('-', 1)[-1] - name = match.group(0).lower() - # To match the "safe" name that pkg_resources creates: - name = name.replace('_', '-') - # project name and version must be separated by a dash - look_for = search_name.lower() + "-" - if name.startswith(look_for): - return match.group(0)[len(look_for):] - else: + # Project name and version must be separated by a dash. + if egg_info[len(canonical_name)] != "-": return None + return egg_info[(len(canonical_name) + 1):] def _determine_base_url(document, page_url): diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 5ce890eba..0a6d33cb0 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -620,6 +620,15 @@ class Wheel(object): return bool(set(tags).intersection(self.file_tags)) +def _contains_egg_info( + s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): + """Determine whether the string looks like an egg_info. + + :param s: The string to parse. E.g. foo-2.1 + """ + return bool(_egg_info_re.search(s)) + + class WheelBuilder(object): """Build wheels from a RequirementSet.""" @@ -712,7 +721,6 @@ class WheelBuilder(object): newly built wheel, in preparation for installation. :return: True if all the wheels built correctly. """ - from pip._internal import index from pip._internal.models.link import Link building_is_possible = self._wheel_dir or ( @@ -742,7 +750,7 @@ class WheelBuilder(object): if autobuilding: link = req.link base, ext = link.splitext() - if index.egg_info_matches(base, None, link) is None: + if not _contains_egg_info(base): # E.g. local directory. Build wheel just for this run. ephem_cache = True if "binary" not in format_control.get_allowed_formats( diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index ce81a9deb..0d46b87ce 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -7,7 +7,8 @@ from pip._vendor import html5lib, requests from pip._internal.download import PipSession from pip._internal.index import ( - Link, PackageFinder, _determine_base_url, _get_html_page, egg_info_matches, + Link, PackageFinder, _determine_base_url, _egg_info_matches, + _get_html_page, ) @@ -165,32 +166,29 @@ def test_get_formatted_locations_basic_auth(): @pytest.mark.parametrize( - ("egg_info", "search_name", "expected"), + ("egg_info", "canonical_name", "expected"), [ # Trivial. ("pip-18.0", "pip", "18.0"), - ("pip-18.0", None, "18.0"), + ("zope-interface-4.5.0", "zope-interface", "4.5.0"), - # Non-canonical names. + # Canonicalized name match non-canonicalized egg info. (pypa/pip#5870) ("Jinja2-2.10", "jinja2", "2.10"), - ("jinja2-2.10", "Jinja2", "2.10"), + ("zope.interface-4.5.0", "zope-interface", "4.5.0"), + ("zope_interface-4.5.0", "zope-interface", "4.5.0"), - # Ambiguous names. Should be smart enough if the package name is - # provided, otherwise make a guess. + # Should be smart enough to parse ambiguous names from the provided + # package name. ("foo-2-2", "foo", "2-2"), ("foo-2-2", "foo-2", "2"), - ("foo-2-2", None, "2-2"), - ("im-valid", None, "valid"), - # Invalid names. - ("invalid", None, None), - ("im_invalid", None, None), + # Invalid. ("the-package-name-8.19", "does-not-match", None), ], ) -def test_egg_info_matches(egg_info, search_name, expected): +def test_egg_info_matches(egg_info, canonical_name, expected): link = None # Only used for reporting. - version = egg_info_matches(egg_info, search_name, link) + version = _egg_info_matches(egg_info, canonical_name, link) assert version == expected diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index a9be9bfe8..d2fcde65c 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -13,6 +13,26 @@ from pip._internal.utils.misc import unpack_file from tests.lib import DATA_DIR +@pytest.mark.parametrize( + "s, expected", + [ + # Trivial. + ("pip-18.0", True), + + # Ambiguous. + ("foo-2-2", True), + ("im-valid", True), + + # Invalid. + ("invalid", False), + ("im_invalid", False), + ], +) +def test_contains_egg_info(s, expected): + result = wheel._contains_egg_info(s) + assert result == expected + + @pytest.mark.parametrize("console_scripts", ["pip = pip._internal.main:pip", "pip:pip = pip._internal.main:pip"])