Skip to content

Commit 5e2ce4c

Browse files
committed
Use pkg_resources.Distribution derived from wheel directly
We now extract all metadata files from the wheel directly into memory and make them available to the wrapping pkg_resources.Distribution via the DictMetadata introduced earlier.
1 parent 7d795af commit 5e2ce4c

File tree

4 files changed

+112
-9
lines changed

4 files changed

+112
-9
lines changed

src/pip/_internal/distributions/wheel.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from pip._vendor import pkg_resources
1+
from zipfile import ZipFile
22

33
from pip._internal.distributions.base import AbstractDistribution
44
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
5+
from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
56

67
if MYPY_CHECK_RUNNING:
78
from pip._vendor.pkg_resources import Distribution
@@ -16,8 +17,15 @@ class WheelDistribution(AbstractDistribution):
1617

1718
def get_pkg_resources_distribution(self):
1819
# type: () -> Distribution
19-
return list(pkg_resources.find_distributions(
20-
self.req.source_dir))[0]
20+
# Set as part of preparation during download.
21+
assert self.req.local_file_path
22+
# Wheels are never unnamed.
23+
assert self.req.name
24+
25+
with ZipFile(self.req.local_file_path, allowZip64=True) as z:
26+
return pkg_resources_distribution_for_wheel(
27+
z, self.req.name, self.req.local_file_path
28+
)
2129

2230
def prepare_distribution_metadata(self, finder, build_isolation):
2331
# type: (PackageFinder, bool) -> None

src/pip/_internal/utils/wheel.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
from zipfile import ZipFile
99

1010
from pip._vendor.packaging.utils import canonicalize_name
11+
from pip._vendor.pkg_resources import DistInfoDistribution
1112
from pip._vendor.six import PY2, ensure_str
1213

1314
from pip._internal.exceptions import UnsupportedWheel
15+
from pip._internal.utils.pkg_resources import DictMetadata
1416
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
1517

1618
if MYPY_CHECK_RUNNING:
1719
from email.message import Message
18-
from typing import Tuple
20+
from typing import Dict, Tuple
21+
22+
from pip._vendor.pkg_resources import Distribution
1923

2024
if PY2:
2125
from zipfile import BadZipfile as BadZipFile
@@ -29,6 +33,65 @@
2933
logger = logging.getLogger(__name__)
3034

3135

36+
class WheelMetadata(DictMetadata):
37+
"""Metadata provider that maps metadata decoding exceptions to our
38+
internal exception type.
39+
"""
40+
def __init__(self, metadata, wheel_name):
41+
# type: (Dict[str, bytes], str) -> None
42+
super(WheelMetadata, self).__init__(metadata)
43+
self._wheel_name = wheel_name
44+
45+
def get_metadata(self, name):
46+
# type: (str) -> str
47+
try:
48+
return super(WheelMetadata, self).get_metadata(name)
49+
except UnicodeDecodeError as e:
50+
# Augment the default error with the origin of the file.
51+
raise UnsupportedWheel(
52+
"Error decoding metadata for {}: {}".format(
53+
self._wheel_name, e
54+
)
55+
)
56+
57+
58+
def pkg_resources_distribution_for_wheel(wheel_zip, name, location):
59+
# type: (ZipFile, str, str) -> Distribution
60+
"""Get a pkg_resources distribution given a wheel.
61+
62+
:raises UnsupportedWheel: on any errors
63+
"""
64+
info_dir, _ = parse_wheel(wheel_zip, name)
65+
66+
metadata_files = [
67+
p for p in wheel_zip.namelist() if p.startswith("{}/".format(info_dir))
68+
]
69+
70+
metadata_text = {} # type: Dict[str, bytes]
71+
for path in metadata_files:
72+
# If a flag is set, namelist entries may be unicode in Python 2.
73+
# We coerce them to native str type to match the types used in the rest
74+
# of the code. This cannot fail because unicode can always be encoded
75+
# with UTF-8.
76+
full_path = ensure_str(path)
77+
_, metadata_name = full_path.split("/", 1)
78+
79+
try:
80+
metadata_text[metadata_name] = read_wheel_metadata_file(
81+
wheel_zip, full_path
82+
)
83+
except UnsupportedWheel as e:
84+
raise UnsupportedWheel(
85+
"{} has an invalid wheel, {}".format(name, str(e))
86+
)
87+
88+
metadata = WheelMetadata(metadata_text, location)
89+
90+
return DistInfoDistribution(
91+
location=location, metadata=metadata, project_name=name
92+
)
93+
94+
3295
def parse_wheel(wheel_zip, name):
3396
# type: (ZipFile, str) -> Tuple[str, Message]
3497
"""Extract information from the provided wheel, ensuring it meets basic

tests/functional/test_install_wheel.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,34 @@ def test_wheel_installs_ok_with_nested_dist_info(script):
534534
script.pip(
535535
"install", "--no-cache-dir", "--no-index", package
536536
)
537+
538+
539+
def test_wheel_installs_ok_with_badly_encoded_irrelevant_metadata(script):
540+
package = create_basic_wheel_for_package(
541+
script,
542+
"simple",
543+
"0.1.0",
544+
extra_files={
545+
"simple-0.1.0.dist-info/AUTHORS.txt": b"\xff"
546+
},
547+
)
548+
script.pip(
549+
"install", "--no-cache-dir", "--no-index", package
550+
)
551+
552+
553+
def test_wheel_install_fails_with_badly_encoded_metadata(script):
554+
package = create_basic_wheel_for_package(
555+
script,
556+
"simple",
557+
"0.1.0",
558+
extra_files={
559+
"simple-0.1.0.dist-info/METADATA": b"\xff"
560+
},
561+
)
562+
result = script.pip(
563+
"install", "--no-cache-dir", "--no-index", package, expect_error=True
564+
)
565+
assert "Error decoding metadata for" in result.stderr
566+
assert "simple-0.1.0-py2.py3-none-any.whl" in result.stderr
567+
assert "METADATA" in result.stderr

tests/lib/__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from zipfile import ZipFile
1616

1717
import pytest
18-
from pip._vendor.six import PY2
18+
from pip._vendor.six import PY2, ensure_binary
1919
from scripttest import FoundDir, TestFileEnvironment
2020

2121
from pip._internal.index.collector import LinkCollector
@@ -1018,9 +1018,6 @@ def hello():
10181018
"{dist_info}/RECORD": ""
10191019
}
10201020

1021-
if extra_files:
1022-
files.update(extra_files)
1023-
10241021
# Some useful shorthands
10251022
archive_name = "{name}-{version}-py2.py3-none-any.whl".format(
10261023
name=name, version=version
@@ -1046,10 +1043,14 @@ def hello():
10461043
name=name, version=version, requires_dist=requires_dist
10471044
).strip()
10481045

1046+
# Add new files after formatting
1047+
if extra_files:
1048+
files.update(extra_files)
1049+
10491050
for fname in files:
10501051
path = script.temp_path / fname
10511052
path.parent.mkdir(exist_ok=True, parents=True)
1052-
path.write_text(files[fname])
1053+
path.write_bytes(ensure_binary(files[fname]))
10531054

10541055
retval = script.scratch_path / archive_name
10551056
generated = shutil.make_archive(retval, 'zip', script.temp_path)

0 commit comments

Comments
 (0)