Skip to content

Commit a71086e

Browse files
authored
Merge pull request #7538 from chrahunt/refactor/get-metadata-from-zip
Read wheel metadata from wheel directly
2 parents b2f596b + 2f92826 commit a71086e

File tree

2 files changed

+102
-35
lines changed

2 files changed

+102
-35
lines changed

src/pip/_internal/operations/install/wheel.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
import warnings
1919
from base64 import urlsafe_b64encode
2020
from email.parser import Parser
21+
from zipfile import ZipFile
2122

2223
from pip._vendor import pkg_resources
2324
from pip._vendor.distlib.scripts import ScriptMaker
2425
from pip._vendor.distlib.util import get_export_entry
2526
from pip._vendor.packaging.utils import canonicalize_name
26-
from pip._vendor.six import StringIO, ensure_str
27+
from pip._vendor.six import PY2, StringIO, ensure_str
2728

2829
from pip._internal.exceptions import InstallationError, UnsupportedWheel
2930
from pip._internal.locations import get_major_minor_version
@@ -43,6 +44,11 @@
4344

4445
InstalledCSVRow = Tuple[str, ...]
4546

47+
if PY2:
48+
from zipfile import BadZipfile as BadZipFile
49+
else:
50+
from zipfile import BadZipFile
51+
4652

4753
VERSION_COMPATIBLE = (1, 0)
4854

@@ -286,6 +292,7 @@ def make(self, specification, options=None):
286292
def install_unpacked_wheel(
287293
name, # type: str
288294
wheeldir, # type: str
295+
wheel_zip, # type: ZipFile
289296
scheme, # type: Scheme
290297
req_description, # type: str
291298
pycompile=True, # type: bool
@@ -296,6 +303,7 @@ def install_unpacked_wheel(
296303
297304
:param name: Name of the project to install
298305
:param wheeldir: Base directory of the unpacked wheel
306+
:param wheel_zip: open ZipFile for wheel being installed
299307
:param scheme: Distutils scheme dictating the install directories
300308
:param req_description: String used in place of the requirement, for
301309
logging
@@ -313,16 +321,7 @@ def install_unpacked_wheel(
313321

314322
source = wheeldir.rstrip(os.path.sep) + os.path.sep
315323

316-
try:
317-
info_dir = wheel_dist_info_dir(source, name)
318-
metadata = wheel_metadata(source, info_dir)
319-
version = wheel_version(metadata)
320-
except UnsupportedWheel as e:
321-
raise UnsupportedWheel(
322-
"{} has an invalid wheel, {}".format(name, str(e))
323-
)
324-
325-
check_compatibility(version, name)
324+
info_dir, metadata = parse_wheel(wheel_zip, name)
326325

327326
if wheel_root_is_purelib(metadata):
328327
lib_dir = scheme.purelib
@@ -612,26 +611,50 @@ def install_wheel(
612611
# type: (...) -> None
613612
with TempDirectory(
614613
path=_temp_dir_for_testing, kind="unpacked-wheel"
615-
) as unpacked_dir:
614+
) as unpacked_dir, ZipFile(wheel_path, allowZip64=True) as z:
616615
unpack_file(wheel_path, unpacked_dir.path)
617616
install_unpacked_wheel(
618617
name=name,
619618
wheeldir=unpacked_dir.path,
619+
wheel_zip=z,
620620
scheme=scheme,
621621
req_description=req_description,
622622
pycompile=pycompile,
623623
warn_script_location=warn_script_location,
624624
)
625625

626626

627+
def parse_wheel(wheel_zip, name):
628+
# type: (ZipFile, str) -> Tuple[str, Message]
629+
"""Extract information from the provided wheel, ensuring it meets basic
630+
standards.
631+
632+
Returns the name of the .dist-info directory and the parsed WHEEL metadata.
633+
"""
634+
try:
635+
info_dir = wheel_dist_info_dir(wheel_zip, name)
636+
metadata = wheel_metadata(wheel_zip, info_dir)
637+
version = wheel_version(metadata)
638+
except UnsupportedWheel as e:
639+
raise UnsupportedWheel(
640+
"{} has an invalid wheel, {}".format(name, str(e))
641+
)
642+
643+
check_compatibility(version, name)
644+
645+
return info_dir, metadata
646+
647+
627648
def wheel_dist_info_dir(source, name):
628-
# type: (str, str) -> str
649+
# type: (ZipFile, str) -> str
629650
"""Returns the name of the contained .dist-info directory.
630651
631652
Raises AssertionError or UnsupportedWheel if not found, >1 found, or
632653
it doesn't match the provided name.
633654
"""
634-
subdirs = os.listdir(source)
655+
# Zip file path separators must be /
656+
subdirs = list(set(p.split("/")[0] for p in source.namelist()))
657+
635658
info_dirs = [s for s in subdirs if s.endswith('.dist-info')]
636659

637660
if not info_dirs:
@@ -655,19 +678,26 @@ def wheel_dist_info_dir(source, name):
655678
)
656679
)
657680

658-
return info_dir
681+
# Zip file paths can be unicode or str depending on the zip entry flags,
682+
# so normalize it.
683+
return ensure_str(info_dir)
659684

660685

661686
def wheel_metadata(source, dist_info_dir):
662-
# type: (str, str) -> Message
687+
# type: (ZipFile, str) -> Message
663688
"""Return the WHEEL metadata of an extracted wheel, if possible.
664689
Otherwise, raise UnsupportedWheel.
665690
"""
666691
try:
667-
with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f:
668-
wheel_text = ensure_str(f.read())
669-
except (IOError, OSError) as e:
692+
# Zip file path separators must be /
693+
wheel_contents = source.read("{}/WHEEL".format(dist_info_dir))
694+
# BadZipFile for general corruption, KeyError for missing entry,
695+
# and RuntimeError for password-protected files
696+
except (BadZipFile, KeyError, RuntimeError) as e:
670697
raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e))
698+
699+
try:
700+
wheel_text = ensure_str(wheel_contents)
671701
except UnicodeDecodeError as e:
672702
raise UnsupportedWheel("error decoding WHEEL: {!r}".format(e))
673703

tests/unit/test_wheel.py

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
import os
55
import textwrap
66
from email import message_from_string
7+
from io import BytesIO
8+
from zipfile import ZipFile
79

810
import pytest
911
from mock import patch
12+
from pip._vendor.contextlib2 import ExitStack
1013
from pip._vendor.packaging.requirements import Requirement
1114

1215
from pip._internal.exceptions import UnsupportedWheel
@@ -22,9 +25,13 @@
2225
)
2326
from pip._internal.utils.compat import WINDOWS
2427
from pip._internal.utils.misc import hash_file
28+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
2529
from pip._internal.utils.unpacking import unpack_file
2630
from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2
2731

32+
if MYPY_CHECK_RUNNING:
33+
from tests.lib.path import Path
34+
2835

2936
def call_get_legacy_build_wheel_path(caplog, names):
3037
wheel_path = get_legacy_build_wheel_path(
@@ -190,30 +197,60 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog):
190197
assert messages == expected
191198

192199

193-
def test_wheel_dist_info_dir_found(tmpdir):
200+
@pytest.fixture
201+
def zip_dir():
202+
def make_zip(path):
203+
# type: (Path) -> ZipFile
204+
buf = BytesIO()
205+
with ZipFile(buf, "w", allowZip64=True) as z:
206+
for dirpath, dirnames, filenames in os.walk(path):
207+
for filename in filenames:
208+
file_path = os.path.join(path, dirpath, filename)
209+
# Zip files must always have / as path separator
210+
archive_path = os.path.relpath(file_path, path).replace(
211+
os.pathsep, "/"
212+
)
213+
z.write(file_path, archive_path)
214+
215+
return stack.enter_context(ZipFile(buf, "r", allowZip64=True))
216+
217+
stack = ExitStack()
218+
with stack:
219+
yield make_zip
220+
221+
222+
def test_wheel_dist_info_dir_found(tmpdir, zip_dir):
194223
expected = "simple-0.1.dist-info"
195-
tmpdir.joinpath(expected).mkdir()
196-
assert wheel.wheel_dist_info_dir(str(tmpdir), "simple") == expected
224+
dist_info_dir = tmpdir / expected
225+
dist_info_dir.mkdir()
226+
dist_info_dir.joinpath("WHEEL").touch()
227+
assert wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") == expected
197228

198229

199-
def test_wheel_dist_info_dir_multiple(tmpdir):
200-
tmpdir.joinpath("simple-0.1.dist-info").mkdir()
201-
tmpdir.joinpath("unrelated-0.1.dist-info").mkdir()
230+
def test_wheel_dist_info_dir_multiple(tmpdir, zip_dir):
231+
dist_info_dir_1 = tmpdir / "simple-0.1.dist-info"
232+
dist_info_dir_1.mkdir()
233+
dist_info_dir_1.joinpath("WHEEL").touch()
234+
dist_info_dir_2 = tmpdir / "unrelated-0.1.dist-info"
235+
dist_info_dir_2.mkdir()
236+
dist_info_dir_2.joinpath("WHEEL").touch()
202237
with pytest.raises(UnsupportedWheel) as e:
203-
wheel.wheel_dist_info_dir(str(tmpdir), "simple")
238+
wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple")
204239
assert "multiple .dist-info directories found" in str(e.value)
205240

206241

207-
def test_wheel_dist_info_dir_none(tmpdir):
242+
def test_wheel_dist_info_dir_none(tmpdir, zip_dir):
208243
with pytest.raises(UnsupportedWheel) as e:
209-
wheel.wheel_dist_info_dir(str(tmpdir), "simple")
244+
wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple")
210245
assert "directory not found" in str(e.value)
211246

212247

213-
def test_wheel_dist_info_dir_wrong_name(tmpdir):
214-
tmpdir.joinpath("unrelated-0.1.dist-info").mkdir()
248+
def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_dir):
249+
dist_info_dir = tmpdir / "unrelated-0.1.dist-info"
250+
dist_info_dir.mkdir()
251+
dist_info_dir.joinpath("WHEEL").touch()
215252
with pytest.raises(UnsupportedWheel) as e:
216-
wheel.wheel_dist_info_dir(str(tmpdir), "simple")
253+
wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple")
217254
assert "does not start with 'simple'" in str(e.value)
218255

219256

@@ -223,25 +260,25 @@ def test_wheel_version_ok(tmpdir, data):
223260
) == (1, 9)
224261

225262

226-
def test_wheel_metadata_fails_missing_wheel(tmpdir):
263+
def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir):
227264
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
228265
dist_info_dir.mkdir()
229266
dist_info_dir.joinpath("METADATA").touch()
230267

231268
with pytest.raises(UnsupportedWheel) as e:
232-
wheel.wheel_metadata(str(tmpdir), dist_info_dir.name)
269+
wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name)
233270
assert "could not read WHEEL file" in str(e.value)
234271

235272

236273
@skip_if_python2
237-
def test_wheel_metadata_fails_on_bad_encoding(tmpdir):
274+
def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir):
238275
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
239276
dist_info_dir.mkdir()
240277
dist_info_dir.joinpath("METADATA").touch()
241278
dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff")
242279

243280
with pytest.raises(UnsupportedWheel) as e:
244-
wheel.wheel_metadata(str(tmpdir), dist_info_dir.name)
281+
wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name)
245282
assert "error decoding WHEEL" in str(e.value)
246283

247284

0 commit comments

Comments
 (0)