From 918b154518e0e6b545991dee7170616ac3694355 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 12:12:17 -0500 Subject: [PATCH 01/12] Move info_dir calculation up This will let us use the value for later processing. --- src/pip/_internal/operations/install/wheel.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 438f96303cf..745a6d34210 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -324,6 +324,31 @@ def install_unpacked_wheel( # TODO: Look into moving this into a dedicated class for representing an # installation. + source = wheeldir.rstrip(os.path.sep) + os.path.sep + subdirs = os.listdir(source) + info_dirs = [s for s in subdirs if s.endswith('.dist-info')] + + assert info_dirs, "{} .dist-info directory not found".format( + req_description + ) + + assert len(info_dirs) == 1, ( + '{} multiple .dist-info directories found: {}'.format( + req_description, ', '.join(info_dirs) + ) + ) + + info_dir = info_dirs[0] + + info_dir_name = canonicalize_name(info_dir) + canonical_name = canonicalize_name(name) + if not info_dir_name.startswith(canonical_name): + raise UnsupportedWheel( + "{} .dist-info directory {!r} does not start with {!r}".format( + req_description, info_dir, canonical_name + ) + ) + try: version = wheel_version(wheeldir) except UnsupportedWheel as e: @@ -338,9 +363,6 @@ def install_unpacked_wheel( else: lib_dir = scheme.platlib - source = wheeldir.rstrip(os.path.sep) + os.path.sep - subdirs = os.listdir(source) - info_dirs = [s for s in subdirs if s.endswith('.dist-info')] data_dirs = [s for s in subdirs if s.endswith('.data')] # Record details of the files moved @@ -434,27 +456,6 @@ def clobber( clobber(source, lib_dir, True) - assert info_dirs, "{} .dist-info directory not found".format( - req_description - ) - - assert len(info_dirs) == 1, ( - '{} multiple .dist-info directories found: {}'.format( - req_description, ', '.join(info_dirs) - ) - ) - - info_dir = info_dirs[0] - - info_dir_name = canonicalize_name(info_dir) - canonical_name = canonicalize_name(name) - if not info_dir_name.startswith(canonical_name): - raise UnsupportedWheel( - "{} .dist-info directory {!r} does not start with {!r}".format( - req_description, info_dir, canonical_name - ) - ) - dest_info_dir = os.path.join(lib_dir, info_dir) # Get the defined entry points From d66bc398be1e87281951d17fcc2375516afa025d Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 12:28:26 -0500 Subject: [PATCH 02/12] Split wheel_metadata from wheel_version This will let us re-use the wheel_metadata for other parts of processing, and by parameterizing checks in terms of metadata we will be able to substitute in metadata derived directly from the zip. --- src/pip/_internal/operations/install/wheel.py | 20 ++++++++++----- tests/unit/test_wheel.py | 25 +++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 745a6d34210..667a139c7a9 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -33,6 +33,7 @@ from pip._internal.utils.unpacking import unpack_file if MYPY_CHECK_RUNNING: + from email.message import Message from typing import ( Dict, List, Optional, Sequence, Tuple, IO, Text, Any, Iterable, Callable, Set, @@ -350,7 +351,8 @@ def install_unpacked_wheel( ) try: - version = wheel_version(wheeldir) + metadata = wheel_metadata(wheeldir) + version = wheel_version(metadata) except UnsupportedWheel as e: raise UnsupportedWheel( "{} has an invalid wheel, {}".format(name, str(e)) @@ -657,10 +659,10 @@ def install_wheel( ) -def wheel_version(source_dir): - # type: (Optional[str]) -> Tuple[int, ...] - """Return the Wheel-Version of an extracted wheel, if possible. - Otherwise, raise UnsupportedWheel if we couldn't parse / extract it. +def wheel_metadata(source_dir): + # type: (Optional[str]) -> Message + """Return the WHEEL metadata of an extracted wheel, if possible. + Otherwise, raise UnsupportedWheel. """ try: dists = [d for d in pkg_resources.find_on_path(None, source_dir)] @@ -684,8 +686,14 @@ def wheel_version(source_dir): # FeedParser (used by Parser) does not raise any exceptions. The returned # message may have .defects populated, but for backwards-compatibility we # currently ignore them. - wheel_data = Parser().parsestr(wheel_text) + return Parser().parsestr(wheel_text) + +def wheel_version(wheel_data): + # type: (Message) -> Tuple[int, ...] + """Given WHEEL metadata, return the parsed Wheel-Version. + Otherwise, raise UnsupportedWheel. + """ version_text = wheel_data["Wheel-Version"] if version_text is None: raise UnsupportedWheel("WHEEL is missing Wheel-Version") diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 9689c0ec173..4c941dc5921 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -196,42 +196,43 @@ def test_wheel_version(tmpdir, data): unpack_file(data.packages.joinpath(future_wheel), tmpdir + 'future') - assert wheel.wheel_version(tmpdir + 'future') == future_version + metadata = wheel.wheel_metadata(tmpdir + 'future') + assert wheel.wheel_version(metadata) == future_version -def test_wheel_version_fails_on_error(monkeypatch): +def test_wheel_metadata_fails_on_error(monkeypatch): err = RuntimeError("example") monkeypatch.setattr(pkg_resources, "find_on_path", Mock(side_effect=err)) with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(".") + wheel.wheel_metadata(".") assert repr(err) in str(e.value) -def test_wheel_version_fails_no_dists(tmpdir): +def test_wheel_metadata_fails_no_dists(tmpdir): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir)) assert "no contained distribution found" in str(e.value) -def test_wheel_version_fails_missing_wheel(tmpdir): +def test_wheel_metadata_fails_missing_wheel(tmpdir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir)) assert "could not read WHEEL file" in str(e.value) @skip_if_python2 -def test_wheel_version_fails_on_bad_encoding(tmpdir): +def test_wheel_metadata_fails_on_bad_encoding(tmpdir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir)) assert "error decoding WHEEL" in str(e.value) @@ -241,8 +242,9 @@ def test_wheel_version_fails_on_no_wheel_version(tmpdir): dist_info_dir.joinpath("METADATA").touch() dist_info_dir.joinpath("WHEEL").touch() + metadata = wheel.wheel_metadata(str(tmpdir)) with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_version(metadata) assert "missing Wheel-Version" in str(e.value) @@ -259,8 +261,9 @@ def test_wheel_version_fails_on_bad_wheel_version(tmpdir, version): "Wheel-Version: {}".format(version) ) + metadata = wheel.wheel_metadata(str(tmpdir)) with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_version(metadata) assert "invalid Wheel-Version" in str(e.value) From ca729c89de909a64f76bd191a9d5f43db9e9d93b Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 12:32:11 -0500 Subject: [PATCH 03/12] Simplify wheel_version tests --- tests/unit/test_wheel.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 4c941dc5921..fa3f7eeea4d 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -3,6 +3,7 @@ import logging import os import textwrap +from email import message_from_string import pytest from mock import Mock, patch @@ -236,15 +237,9 @@ def test_wheel_metadata_fails_on_bad_encoding(tmpdir): assert "error decoding WHEEL" in str(e.value) -def test_wheel_version_fails_on_no_wheel_version(tmpdir): - dist_info_dir = tmpdir / "simple-0.1.0.dist-info" - dist_info_dir.mkdir() - dist_info_dir.joinpath("METADATA").touch() - dist_info_dir.joinpath("WHEEL").touch() - - metadata = wheel.wheel_metadata(str(tmpdir)) +def test_wheel_version_fails_on_no_wheel_version(): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(metadata) + wheel.wheel_version(message_from_string("")) assert "missing Wheel-Version" in str(e.value) @@ -253,17 +248,11 @@ def test_wheel_version_fails_on_no_wheel_version(tmpdir): ("1.b",), ("1.",), ]) -def test_wheel_version_fails_on_bad_wheel_version(tmpdir, version): - dist_info_dir = tmpdir / "simple-0.1.0.dist-info" - dist_info_dir.mkdir() - dist_info_dir.joinpath("METADATA").touch() - dist_info_dir.joinpath("WHEEL").write_text( - "Wheel-Version: {}".format(version) - ) - - metadata = wheel.wheel_metadata(str(tmpdir)) +def test_wheel_version_fails_on_bad_wheel_version(version): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(metadata) + wheel.wheel_version( + message_from_string("Wheel-Version: {}".format(version)) + ) assert "invalid Wheel-Version" in str(e.value) From 42c6dd78b935d73b0f94bce06bf937b2af564501 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 12:45:13 -0500 Subject: [PATCH 04/12] Use WHEEL metadata to determine Root-Is-Purelib --- src/pip/_internal/operations/install/wheel.py | 7 ++++++- tests/unit/test_wheel.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 667a139c7a9..9b7d62cd3c3 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -117,6 +117,11 @@ def root_is_purelib(name, wheeldir): return False +def wheel_root_is_purelib(metadata): + # type: (Message) -> bool + return metadata.get("Root-Is-Purelib", "").lower() == "true" + + def get_entrypoints(filename): # type: (str) -> Tuple[Dict[str, str], Dict[str, str]] if not os.path.exists(filename): @@ -360,7 +365,7 @@ def install_unpacked_wheel( check_compatibility(version, name) - if root_is_purelib(name, wheeldir): + if wheel_root_is_purelib(metadata): lib_dir = scheme.purelib else: lib_dir = scheme.platlib diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index fa3f7eeea4d..d40878eedd0 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -256,6 +256,18 @@ def test_wheel_version_fails_on_bad_wheel_version(version): assert "invalid Wheel-Version" in str(e.value) +@pytest.mark.parametrize("text,expected", [ + ("Root-Is-Purelib: true", True), + ("Root-Is-Purelib: false", False), + ("Root-Is-Purelib: hello", False), + ("", False), + ("root-is-purelib: true", True), + ("root-is-purelib: True", True), +]) +def test_wheel_root_is_purelib(text, expected): + assert wheel.wheel_root_is_purelib(message_from_string(text)) == expected + + def test_check_compatibility(): name = 'test' vc = wheel.VERSION_COMPATIBLE From c9b0742508568616f1ce3af77f6d5e3737e5ba88 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 12:49:08 -0500 Subject: [PATCH 05/12] Remove old root_is_purelib The _invalidversion_ tests are not applicable to the new function since we do not use a regex to find the applicable folder. --- src/pip/_internal/operations/install/wheel.py | 19 ------------------- .../plat_wheel-1.7.dist-info/WHEEL | 4 ---- .../WHEEL | 4 ---- .../pure_wheel-1.7.dist-info/WHEEL | 4 ---- .../WHEEL | 4 ---- tests/unit/test_wheel.py | 16 ---------------- 6 files changed, 51 deletions(-) delete mode 100644 tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL delete mode 100644 tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL delete mode 100644 tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL delete mode 100644 tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 9b7d62cd3c3..13ff0b3c460 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -98,25 +98,6 @@ def fix_script(path): return None -dist_info_re = re.compile(r"""^(?P(?P.+?)(-(?P.+?))?) - \.dist-info$""", re.VERBOSE) - - -def root_is_purelib(name, wheeldir): - # type: (str, str) -> bool - """True if the extracted wheel in wheeldir should go into purelib.""" - name_folded = name.replace("-", "_") - for item in os.listdir(wheeldir): - match = dist_info_re.match(item) - if match and match.group('name') == name_folded: - with open(os.path.join(wheeldir, item, 'WHEEL')) as wheel: - for line in wheel: - line = line.lower().rstrip() - if line == "root-is-purelib: true": - return True - return False - - def wheel_root_is_purelib(metadata): # type: (Message) -> bool return metadata.get("Root-Is-Purelib", "").lower() == "true" diff --git a/tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL b/tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL deleted file mode 100644 index 0a698cb6bf9..00000000000 --- a/tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: false - diff --git a/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL b/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL deleted file mode 100644 index 0a698cb6bf9..00000000000 --- a/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: false - diff --git a/tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL b/tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL deleted file mode 100644 index 782313d161b..00000000000 --- a/tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: true - diff --git a/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL b/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL deleted file mode 100644 index 782313d161b..00000000000 --- a/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: true - diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index d40878eedd0..a9aea152d9f 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -300,22 +300,6 @@ def test_unpack_wheel_no_flatten(self, tmpdir): unpack_file(filepath, tmpdir) assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info')) - def test_purelib_platlib(self, data): - """ - Test the "wheel is purelib/platlib" code. - """ - packages = [ - ("pure_wheel", data.packages.joinpath("pure_wheel-1.7"), True), - ("plat_wheel", data.packages.joinpath("plat_wheel-1.7"), False), - ("pure_wheel", data.packages.joinpath( - "pure_wheel-_invalidversion_"), True), - ("plat_wheel", data.packages.joinpath( - "plat_wheel-_invalidversion_"), False), - ] - - for name, path, expected in packages: - assert wheel.root_is_purelib(name, path) == expected - class TestInstallUnpackedWheel(object): """ From c51f020b9834ec56874feac1626d43e4bfdc1d40 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 13:00:57 -0500 Subject: [PATCH 06/12] Read WHEEL from .dist-info instead of using has_metadata This will make it easier to transition to the already-determined dist-info directory and reduces some of our dependence on pkg_resources. Despite the name, the `egg_info` member is also populated for .dist-info dirs. ensure_str uses encoding='utf-8' and errors='strict' for Python 3 by default, which matches the behavior in `pkg_resources.NullProvider.get_metadata`. --- src/pip/_internal/operations/install/wheel.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 13ff0b3c460..ee9508bd569 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -23,7 +23,7 @@ from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.six import StringIO +from pip._vendor.six import StringIO, ensure_str from pip._internal.exceptions import InstallationError, UnsupportedWheel from pip._internal.locations import get_major_minor_version @@ -662,8 +662,11 @@ def wheel_metadata(source_dir): dist = dists[0] + dist_info_dir = os.path.basename(dist.egg_info) + try: - wheel_text = dist.get_metadata('WHEEL') + with open(os.path.join(source_dir, dist_info_dir, "WHEEL"), "rb") as f: + wheel_text = ensure_str(f.read()) except (IOError, OSError) as e: raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) except UnicodeDecodeError as e: From 32115b55afa18301e30ed23899c1ae5d418337bc Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 13:23:48 -0500 Subject: [PATCH 07/12] Extract getting wheel .dist-info directory into a separate function --- src/pip/_internal/operations/install/wheel.py | 59 +++++++++++-------- tests/unit/test_wheel.py | 27 +++++++++ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index ee9508bd569..ecba5e8bccb 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -312,29 +312,7 @@ def install_unpacked_wheel( # installation. source = wheeldir.rstrip(os.path.sep) + os.path.sep - subdirs = os.listdir(source) - info_dirs = [s for s in subdirs if s.endswith('.dist-info')] - - assert info_dirs, "{} .dist-info directory not found".format( - req_description - ) - - assert len(info_dirs) == 1, ( - '{} multiple .dist-info directories found: {}'.format( - req_description, ', '.join(info_dirs) - ) - ) - - info_dir = info_dirs[0] - - info_dir_name = canonicalize_name(info_dir) - canonical_name = canonicalize_name(name) - if not info_dir_name.startswith(canonical_name): - raise UnsupportedWheel( - "{} .dist-info directory {!r} does not start with {!r}".format( - req_description, info_dir, canonical_name - ) - ) + info_dir = wheel_dist_info_dir(source, req_description, name) try: metadata = wheel_metadata(wheeldir) @@ -351,6 +329,7 @@ def install_unpacked_wheel( else: lib_dir = scheme.platlib + subdirs = os.listdir(source) data_dirs = [s for s in subdirs if s.endswith('.data')] # Record details of the files moved @@ -645,6 +624,40 @@ def install_wheel( ) +def wheel_dist_info_dir(source, req_description, name): + # type: (str, str, str) -> str + """Returns the name of the contained .dist-info directory. + + Raises AssertionError or UnsupportedWheel if not found, >1 found, or + it doesn't match the provided name. + """ + subdirs = os.listdir(source) + info_dirs = [s for s in subdirs if s.endswith('.dist-info')] + + assert info_dirs, "{} .dist-info directory not found".format( + req_description + ) + + assert len(info_dirs) == 1, ( + '{} multiple .dist-info directories found: {}'.format( + req_description, ', '.join(info_dirs) + ) + ) + + info_dir = info_dirs[0] + + info_dir_name = canonicalize_name(info_dir) + canonical_name = canonicalize_name(name) + if not info_dir_name.startswith(canonical_name): + raise UnsupportedWheel( + "{} .dist-info directory {!r} does not start with {!r}".format( + req_description, info_dir, canonical_name + ) + ) + + return info_dir + + def wheel_metadata(source_dir): # type: (Optional[str]) -> Message """Return the WHEEL metadata of an extracted wheel, if possible. diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index a9aea152d9f..436af6a9659 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -191,6 +191,33 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): assert messages == expected +def test_wheel_dist_info_dir_found(tmpdir): + expected = "simple-0.1.dist-info" + tmpdir.joinpath(expected).mkdir() + assert wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") == expected + + +def test_wheel_dist_info_dir_multiple(tmpdir): + tmpdir.joinpath("simple-0.1.dist-info").mkdir() + tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() + with pytest.raises(AssertionError) as e: + wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + assert "multiple .dist-info directories found" in str(e.value) + + +def test_wheel_dist_info_dir_none(tmpdir): + with pytest.raises(AssertionError) as e: + wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + assert "directory not found" in str(e.value) + + +def test_wheel_dist_info_dir_wrong_name(tmpdir): + tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + assert "does not start with 'simple'" in str(e.value) + + def test_wheel_version(tmpdir, data): future_wheel = 'futurewheel-1.9-py2.py3-none-any.whl' future_version = (1, 9) From a539c8dfe0e67181c60d19725ef7278876fb47b3 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 13:39:40 -0500 Subject: [PATCH 08/12] Normalize exception message for dist_info_dir check --- src/pip/_internal/operations/install/wheel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index ecba5e8bccb..b24b29874dc 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -312,9 +312,9 @@ def install_unpacked_wheel( # installation. source = wheeldir.rstrip(os.path.sep) + os.path.sep - info_dir = wheel_dist_info_dir(source, req_description, name) try: + info_dir = wheel_dist_info_dir(source, req_description, name) metadata = wheel_metadata(wheeldir) version = wheel_version(metadata) except UnsupportedWheel as e: @@ -650,8 +650,8 @@ def wheel_dist_info_dir(source, req_description, name): canonical_name = canonicalize_name(name) if not info_dir_name.startswith(canonical_name): raise UnsupportedWheel( - "{} .dist-info directory {!r} does not start with {!r}".format( - req_description, info_dir, canonical_name + ".dist-info directory {!r} does not start with {!r}".format( + info_dir, canonical_name ) ) From e1c7de8861687d93b11dba50e9cbeec066a2a13f Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 13:43:19 -0500 Subject: [PATCH 09/12] Raise UnsupportedWheel instead of AssertionError, and let caller add name --- src/pip/_internal/operations/install/wheel.py | 14 +++++++------- tests/unit/test_wheel.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index b24b29874dc..f6ca9629779 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -634,15 +634,15 @@ def wheel_dist_info_dir(source, req_description, name): subdirs = os.listdir(source) info_dirs = [s for s in subdirs if s.endswith('.dist-info')] - assert info_dirs, "{} .dist-info directory not found".format( - req_description - ) + if not info_dirs: + raise UnsupportedWheel(".dist-info directory not found") - assert len(info_dirs) == 1, ( - '{} multiple .dist-info directories found: {}'.format( - req_description, ', '.join(info_dirs) + if len(info_dirs) > 1: + raise UnsupportedWheel( + "multiple .dist-info directories found: {}".format( + ", ".join(info_dirs) + ) ) - ) info_dir = info_dirs[0] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 436af6a9659..3acf0fe635c 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -200,13 +200,13 @@ def test_wheel_dist_info_dir_found(tmpdir): def test_wheel_dist_info_dir_multiple(tmpdir): tmpdir.joinpath("simple-0.1.dist-info").mkdir() tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() - with pytest.raises(AssertionError) as e: + with pytest.raises(UnsupportedWheel) as e: wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") assert "multiple .dist-info directories found" in str(e.value) def test_wheel_dist_info_dir_none(tmpdir): - with pytest.raises(AssertionError) as e: + with pytest.raises(UnsupportedWheel) as e: wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") assert "directory not found" in str(e.value) From 6c56557fbedc7b978e3c2422e089149ca75c47a1 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 13:44:20 -0500 Subject: [PATCH 10/12] Remove unused req_description from wheel_dist_info_dir --- src/pip/_internal/operations/install/wheel.py | 6 +++--- tests/unit/test_wheel.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index f6ca9629779..99fc50cb47c 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -314,7 +314,7 @@ def install_unpacked_wheel( source = wheeldir.rstrip(os.path.sep) + os.path.sep try: - info_dir = wheel_dist_info_dir(source, req_description, name) + info_dir = wheel_dist_info_dir(source, name) metadata = wheel_metadata(wheeldir) version = wheel_version(metadata) except UnsupportedWheel as e: @@ -624,8 +624,8 @@ def install_wheel( ) -def wheel_dist_info_dir(source, req_description, name): - # type: (str, str, str) -> str +def wheel_dist_info_dir(source, name): + # type: (str, str) -> str """Returns the name of the contained .dist-info directory. Raises AssertionError or UnsupportedWheel if not found, >1 found, or diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 3acf0fe635c..31dc064fddd 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -194,27 +194,27 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): def test_wheel_dist_info_dir_found(tmpdir): expected = "simple-0.1.dist-info" tmpdir.joinpath(expected).mkdir() - assert wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") == expected + assert wheel.wheel_dist_info_dir(str(tmpdir), "simple") == expected def test_wheel_dist_info_dir_multiple(tmpdir): tmpdir.joinpath("simple-0.1.dist-info").mkdir() tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "multiple .dist-info directories found" in str(e.value) def test_wheel_dist_info_dir_none(tmpdir): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "directory not found" in str(e.value) def test_wheel_dist_info_dir_wrong_name(tmpdir): tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "does not start with 'simple'" in str(e.value) From fc48a172066a4aa3e56bafc6b303d87faaa17d03 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 13:56:19 -0500 Subject: [PATCH 11/12] Simplify positive wheel_version unit test --- tests/unit/test_wheel.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 31dc064fddd..88ca0edfb47 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -218,14 +218,10 @@ def test_wheel_dist_info_dir_wrong_name(tmpdir): assert "does not start with 'simple'" in str(e.value) -def test_wheel_version(tmpdir, data): - future_wheel = 'futurewheel-1.9-py2.py3-none-any.whl' - future_version = (1, 9) - - unpack_file(data.packages.joinpath(future_wheel), tmpdir + 'future') - - metadata = wheel.wheel_metadata(tmpdir + 'future') - assert wheel.wheel_version(metadata) == future_version +def test_wheel_version_ok(tmpdir, data): + assert wheel.wheel_version( + message_from_string("Wheel-Version: 1.9") + ) == (1, 9) def test_wheel_metadata_fails_on_error(monkeypatch): From 010c24d64c2f6efe1ad5d6d6ff888297549a0e0b Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 31 Dec 2019 14:00:16 -0500 Subject: [PATCH 12/12] Take .dist-info dir directly in wheel_metadata Since retrieval of the .dist-info dir already ensures that a distribution is found, this reduces responsibility on wheel_metadata and lets us remove a few tests already covered by the tests for test_wheel_dist_info_dir_*. --- src/pip/_internal/operations/install/wheel.py | 22 ++++--------------- tests/unit/test_wheel.py | 21 +++--------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 99fc50cb47c..200d7fe0ab7 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -315,7 +315,7 @@ def install_unpacked_wheel( try: info_dir = wheel_dist_info_dir(source, name) - metadata = wheel_metadata(wheeldir) + metadata = wheel_metadata(source, info_dir) version = wheel_version(metadata) except UnsupportedWheel as e: raise UnsupportedWheel( @@ -658,27 +658,13 @@ def wheel_dist_info_dir(source, name): return info_dir -def wheel_metadata(source_dir): - # type: (Optional[str]) -> Message +def wheel_metadata(source, dist_info_dir): + # type: (str, str) -> Message """Return the WHEEL metadata of an extracted wheel, if possible. Otherwise, raise UnsupportedWheel. """ try: - dists = [d for d in pkg_resources.find_on_path(None, source_dir)] - except Exception as e: - raise UnsupportedWheel( - "could not find a contained distribution due to: {!r}".format(e) - ) - - if not dists: - raise UnsupportedWheel("no contained distribution found") - - dist = dists[0] - - dist_info_dir = os.path.basename(dist.egg_info) - - try: - with open(os.path.join(source_dir, dist_info_dir, "WHEEL"), "rb") as f: + with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f: wheel_text = ensure_str(f.read()) except (IOError, OSError) as e: raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 88ca0edfb47..f6344b8ef04 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -6,8 +6,7 @@ from email import message_from_string import pytest -from mock import Mock, patch -from pip._vendor import pkg_resources +from mock import patch from pip._vendor.packaging.requirements import Requirement from pip._internal.exceptions import UnsupportedWheel @@ -224,27 +223,13 @@ def test_wheel_version_ok(tmpdir, data): ) == (1, 9) -def test_wheel_metadata_fails_on_error(monkeypatch): - err = RuntimeError("example") - monkeypatch.setattr(pkg_resources, "find_on_path", Mock(side_effect=err)) - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(".") - assert repr(err) in str(e.value) - - -def test_wheel_metadata_fails_no_dists(tmpdir): - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir)) - assert "no contained distribution found" in str(e.value) - - def test_wheel_metadata_fails_missing_wheel(tmpdir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir), dist_info_dir.name) assert "could not read WHEEL file" in str(e.value) @@ -256,7 +241,7 @@ def test_wheel_metadata_fails_on_bad_encoding(tmpdir): dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir), dist_info_dir.name) assert "error decoding WHEEL" in str(e.value)