Skip to content

Commit b2f596b

Browse files
authored
Merge pull request #7536 from chrahunt/refactor/extract-wheel-info-functions
Refactor wheel info extraction during install
2 parents 68e49b9 + 010c24d commit b2f596b

File tree

6 files changed

+100
-123
lines changed
  • src/pip/_internal/operations/install
  • tests
    • data/packages
      • plat_wheel-1.7/plat_wheel-1.7.dist-info
      • plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info
      • pure_wheel-1.7/pure_wheel-1.7.dist-info
      • pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info
    • unit

6 files changed

+100
-123
lines changed

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

+53-56
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pip._vendor.distlib.scripts import ScriptMaker
2424
from pip._vendor.distlib.util import get_export_entry
2525
from pip._vendor.packaging.utils import canonicalize_name
26-
from pip._vendor.six import StringIO
26+
from pip._vendor.six import StringIO, ensure_str
2727

2828
from pip._internal.exceptions import InstallationError, UnsupportedWheel
2929
from pip._internal.locations import get_major_minor_version
@@ -33,6 +33,7 @@
3333
from pip._internal.utils.unpacking import unpack_file
3434

3535
if MYPY_CHECK_RUNNING:
36+
from email.message import Message
3637
from typing import (
3738
Dict, List, Optional, Sequence, Tuple, IO, Text, Any,
3839
Iterable, Callable, Set,
@@ -97,23 +98,9 @@ def fix_script(path):
9798
return None
9899

99100

100-
dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>.+?))?)
101-
\.dist-info$""", re.VERBOSE)
102-
103-
104-
def root_is_purelib(name, wheeldir):
105-
# type: (str, str) -> bool
106-
"""True if the extracted wheel in wheeldir should go into purelib."""
107-
name_folded = name.replace("-", "_")
108-
for item in os.listdir(wheeldir):
109-
match = dist_info_re.match(item)
110-
if match and match.group('name') == name_folded:
111-
with open(os.path.join(wheeldir, item, 'WHEEL')) as wheel:
112-
for line in wheel:
113-
line = line.lower().rstrip()
114-
if line == "root-is-purelib: true":
115-
return True
116-
return False
101+
def wheel_root_is_purelib(metadata):
102+
# type: (Message) -> bool
103+
return metadata.get("Root-Is-Purelib", "").lower() == "true"
117104

118105

119106
def get_entrypoints(filename):
@@ -324,23 +311,25 @@ def install_unpacked_wheel(
324311
# TODO: Look into moving this into a dedicated class for representing an
325312
# installation.
326313

314+
source = wheeldir.rstrip(os.path.sep) + os.path.sep
315+
327316
try:
328-
version = wheel_version(wheeldir)
317+
info_dir = wheel_dist_info_dir(source, name)
318+
metadata = wheel_metadata(source, info_dir)
319+
version = wheel_version(metadata)
329320
except UnsupportedWheel as e:
330321
raise UnsupportedWheel(
331322
"{} has an invalid wheel, {}".format(name, str(e))
332323
)
333324

334325
check_compatibility(version, name)
335326

336-
if root_is_purelib(name, wheeldir):
327+
if wheel_root_is_purelib(metadata):
337328
lib_dir = scheme.purelib
338329
else:
339330
lib_dir = scheme.platlib
340331

341-
source = wheeldir.rstrip(os.path.sep) + os.path.sep
342332
subdirs = os.listdir(source)
343-
info_dirs = [s for s in subdirs if s.endswith('.dist-info')]
344333
data_dirs = [s for s in subdirs if s.endswith('.data')]
345334

346335
# Record details of the files moved
@@ -434,27 +423,6 @@ def clobber(
434423

435424
clobber(source, lib_dir, True)
436425

437-
assert info_dirs, "{} .dist-info directory not found".format(
438-
req_description
439-
)
440-
441-
assert len(info_dirs) == 1, (
442-
'{} multiple .dist-info directories found: {}'.format(
443-
req_description, ', '.join(info_dirs)
444-
)
445-
)
446-
447-
info_dir = info_dirs[0]
448-
449-
info_dir_name = canonicalize_name(info_dir)
450-
canonical_name = canonicalize_name(name)
451-
if not info_dir_name.startswith(canonical_name):
452-
raise UnsupportedWheel(
453-
"{} .dist-info directory {!r} does not start with {!r}".format(
454-
req_description, info_dir, canonical_name
455-
)
456-
)
457-
458426
dest_info_dir = os.path.join(lib_dir, info_dir)
459427

460428
# Get the defined entry points
@@ -656,25 +624,48 @@ def install_wheel(
656624
)
657625

658626

659-
def wheel_version(source_dir):
660-
# type: (Optional[str]) -> Tuple[int, ...]
661-
"""Return the Wheel-Version of an extracted wheel, if possible.
662-
Otherwise, raise UnsupportedWheel if we couldn't parse / extract it.
627+
def wheel_dist_info_dir(source, name):
628+
# type: (str, str) -> str
629+
"""Returns the name of the contained .dist-info directory.
630+
631+
Raises AssertionError or UnsupportedWheel if not found, >1 found, or
632+
it doesn't match the provided name.
663633
"""
664-
try:
665-
dists = [d for d in pkg_resources.find_on_path(None, source_dir)]
666-
except Exception as e:
634+
subdirs = os.listdir(source)
635+
info_dirs = [s for s in subdirs if s.endswith('.dist-info')]
636+
637+
if not info_dirs:
638+
raise UnsupportedWheel(".dist-info directory not found")
639+
640+
if len(info_dirs) > 1:
641+
raise UnsupportedWheel(
642+
"multiple .dist-info directories found: {}".format(
643+
", ".join(info_dirs)
644+
)
645+
)
646+
647+
info_dir = info_dirs[0]
648+
649+
info_dir_name = canonicalize_name(info_dir)
650+
canonical_name = canonicalize_name(name)
651+
if not info_dir_name.startswith(canonical_name):
667652
raise UnsupportedWheel(
668-
"could not find a contained distribution due to: {!r}".format(e)
653+
".dist-info directory {!r} does not start with {!r}".format(
654+
info_dir, canonical_name
655+
)
669656
)
670657

671-
if not dists:
672-
raise UnsupportedWheel("no contained distribution found")
658+
return info_dir
673659

674-
dist = dists[0]
675660

661+
def wheel_metadata(source, dist_info_dir):
662+
# type: (str, str) -> Message
663+
"""Return the WHEEL metadata of an extracted wheel, if possible.
664+
Otherwise, raise UnsupportedWheel.
665+
"""
676666
try:
677-
wheel_text = dist.get_metadata('WHEEL')
667+
with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f:
668+
wheel_text = ensure_str(f.read())
678669
except (IOError, OSError) as e:
679670
raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e))
680671
except UnicodeDecodeError as e:
@@ -683,8 +674,14 @@ def wheel_version(source_dir):
683674
# FeedParser (used by Parser) does not raise any exceptions. The returned
684675
# message may have .defects populated, but for backwards-compatibility we
685676
# currently ignore them.
686-
wheel_data = Parser().parsestr(wheel_text)
677+
return Parser().parsestr(wheel_text)
687678

679+
680+
def wheel_version(wheel_data):
681+
# type: (Message) -> Tuple[int, ...]
682+
"""Given WHEEL metadata, return the parsed Wheel-Version.
683+
Otherwise, raise UnsupportedWheel.
684+
"""
688685
version_text = wheel_data["Wheel-Version"]
689686
if version_text is None:
690687
raise UnsupportedWheel("WHEEL is missing Wheel-Version")

tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL

-4
This file was deleted.

tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL

-4
This file was deleted.

tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL

-4
This file was deleted.

tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL

-4
This file was deleted.

tests/unit/test_wheel.py

+47-51
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import logging
44
import os
55
import textwrap
6+
from email import message_from_string
67

78
import pytest
8-
from mock import Mock, patch
9-
from pip._vendor import pkg_resources
9+
from mock import patch
1010
from pip._vendor.packaging.requirements import Requirement
1111

1212
from pip._internal.exceptions import UnsupportedWheel
@@ -190,59 +190,64 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog):
190190
assert messages == expected
191191

192192

193-
def test_wheel_version(tmpdir, data):
194-
future_wheel = 'futurewheel-1.9-py2.py3-none-any.whl'
195-
future_version = (1, 9)
193+
def test_wheel_dist_info_dir_found(tmpdir):
194+
expected = "simple-0.1.dist-info"
195+
tmpdir.joinpath(expected).mkdir()
196+
assert wheel.wheel_dist_info_dir(str(tmpdir), "simple") == expected
196197

197-
unpack_file(data.packages.joinpath(future_wheel), tmpdir + 'future')
198198

199-
assert wheel.wheel_version(tmpdir + 'future') == future_version
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()
202+
with pytest.raises(UnsupportedWheel) as e:
203+
wheel.wheel_dist_info_dir(str(tmpdir), "simple")
204+
assert "multiple .dist-info directories found" in str(e.value)
200205

201206

202-
def test_wheel_version_fails_on_error(monkeypatch):
203-
err = RuntimeError("example")
204-
monkeypatch.setattr(pkg_resources, "find_on_path", Mock(side_effect=err))
207+
def test_wheel_dist_info_dir_none(tmpdir):
205208
with pytest.raises(UnsupportedWheel) as e:
206-
wheel.wheel_version(".")
207-
assert repr(err) in str(e.value)
209+
wheel.wheel_dist_info_dir(str(tmpdir), "simple")
210+
assert "directory not found" in str(e.value)
208211

209212

210-
def test_wheel_version_fails_no_dists(tmpdir):
213+
def test_wheel_dist_info_dir_wrong_name(tmpdir):
214+
tmpdir.joinpath("unrelated-0.1.dist-info").mkdir()
211215
with pytest.raises(UnsupportedWheel) as e:
212-
wheel.wheel_version(str(tmpdir))
213-
assert "no contained distribution found" in str(e.value)
216+
wheel.wheel_dist_info_dir(str(tmpdir), "simple")
217+
assert "does not start with 'simple'" in str(e.value)
218+
219+
220+
def test_wheel_version_ok(tmpdir, data):
221+
assert wheel.wheel_version(
222+
message_from_string("Wheel-Version: 1.9")
223+
) == (1, 9)
214224

215225

216-
def test_wheel_version_fails_missing_wheel(tmpdir):
226+
def test_wheel_metadata_fails_missing_wheel(tmpdir):
217227
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
218228
dist_info_dir.mkdir()
219229
dist_info_dir.joinpath("METADATA").touch()
220230

221231
with pytest.raises(UnsupportedWheel) as e:
222-
wheel.wheel_version(str(tmpdir))
232+
wheel.wheel_metadata(str(tmpdir), dist_info_dir.name)
223233
assert "could not read WHEEL file" in str(e.value)
224234

225235

226236
@skip_if_python2
227-
def test_wheel_version_fails_on_bad_encoding(tmpdir):
237+
def test_wheel_metadata_fails_on_bad_encoding(tmpdir):
228238
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
229239
dist_info_dir.mkdir()
230240
dist_info_dir.joinpath("METADATA").touch()
231241
dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff")
232242

233243
with pytest.raises(UnsupportedWheel) as e:
234-
wheel.wheel_version(str(tmpdir))
244+
wheel.wheel_metadata(str(tmpdir), dist_info_dir.name)
235245
assert "error decoding WHEEL" in str(e.value)
236246

237247

238-
def test_wheel_version_fails_on_no_wheel_version(tmpdir):
239-
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
240-
dist_info_dir.mkdir()
241-
dist_info_dir.joinpath("METADATA").touch()
242-
dist_info_dir.joinpath("WHEEL").touch()
243-
248+
def test_wheel_version_fails_on_no_wheel_version():
244249
with pytest.raises(UnsupportedWheel) as e:
245-
wheel.wheel_version(str(tmpdir))
250+
wheel.wheel_version(message_from_string(""))
246251
assert "missing Wheel-Version" in str(e.value)
247252

248253

@@ -251,19 +256,26 @@ def test_wheel_version_fails_on_no_wheel_version(tmpdir):
251256
("1.b",),
252257
("1.",),
253258
])
254-
def test_wheel_version_fails_on_bad_wheel_version(tmpdir, version):
255-
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
256-
dist_info_dir.mkdir()
257-
dist_info_dir.joinpath("METADATA").touch()
258-
dist_info_dir.joinpath("WHEEL").write_text(
259-
"Wheel-Version: {}".format(version)
260-
)
261-
259+
def test_wheel_version_fails_on_bad_wheel_version(version):
262260
with pytest.raises(UnsupportedWheel) as e:
263-
wheel.wheel_version(str(tmpdir))
261+
wheel.wheel_version(
262+
message_from_string("Wheel-Version: {}".format(version))
263+
)
264264
assert "invalid Wheel-Version" in str(e.value)
265265

266266

267+
@pytest.mark.parametrize("text,expected", [
268+
("Root-Is-Purelib: true", True),
269+
("Root-Is-Purelib: false", False),
270+
("Root-Is-Purelib: hello", False),
271+
("", False),
272+
("root-is-purelib: true", True),
273+
("root-is-purelib: True", True),
274+
])
275+
def test_wheel_root_is_purelib(text, expected):
276+
assert wheel.wheel_root_is_purelib(message_from_string(text)) == expected
277+
278+
267279
def test_check_compatibility():
268280
name = 'test'
269281
vc = wheel.VERSION_COMPATIBLE
@@ -296,22 +308,6 @@ def test_unpack_wheel_no_flatten(self, tmpdir):
296308
unpack_file(filepath, tmpdir)
297309
assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info'))
298310

299-
def test_purelib_platlib(self, data):
300-
"""
301-
Test the "wheel is purelib/platlib" code.
302-
"""
303-
packages = [
304-
("pure_wheel", data.packages.joinpath("pure_wheel-1.7"), True),
305-
("plat_wheel", data.packages.joinpath("plat_wheel-1.7"), False),
306-
("pure_wheel", data.packages.joinpath(
307-
"pure_wheel-_invalidversion_"), True),
308-
("plat_wheel", data.packages.joinpath(
309-
"plat_wheel-_invalidversion_"), False),
310-
]
311-
312-
for name, path, expected in packages:
313-
assert wheel.root_is_purelib(name, path) == expected
314-
315311

316312
class TestInstallUnpackedWheel(object):
317313
"""

0 commit comments

Comments
 (0)