| | """Support functions for working with wheel files. |
| | """ |
| |
|
| | from __future__ import absolute_import |
| |
|
| | import logging |
| | from email.parser import Parser |
| | from zipfile import ZipFile |
| |
|
| | from pip._vendor.packaging.utils import canonicalize_name |
| | from pip._vendor.pkg_resources import DistInfoDistribution |
| | from pip._vendor.six import PY2, ensure_str |
| |
|
| | from pip._internal.exceptions import UnsupportedWheel |
| | from pip._internal.utils.pkg_resources import DictMetadata |
| | from pip._internal.utils.typing import MYPY_CHECK_RUNNING |
| |
|
| | if MYPY_CHECK_RUNNING: |
| | from email.message import Message |
| | from typing import Dict, Tuple |
| |
|
| | from pip._vendor.pkg_resources import Distribution |
| |
|
| | if PY2: |
| | from zipfile import BadZipfile as BadZipFile |
| | else: |
| | from zipfile import BadZipFile |
| |
|
| |
|
| | VERSION_COMPATIBLE = (1, 0) |
| |
|
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | class WheelMetadata(DictMetadata): |
| | """Metadata provider that maps metadata decoding exceptions to our |
| | internal exception type. |
| | """ |
| | def __init__(self, metadata, wheel_name): |
| | |
| | super(WheelMetadata, self).__init__(metadata) |
| | self._wheel_name = wheel_name |
| |
|
| | def get_metadata(self, name): |
| | |
| | try: |
| | return super(WheelMetadata, self).get_metadata(name) |
| | except UnicodeDecodeError as e: |
| | |
| | raise UnsupportedWheel( |
| | "Error decoding metadata for {}: {}".format( |
| | self._wheel_name, e |
| | ) |
| | ) |
| |
|
| |
|
| | def pkg_resources_distribution_for_wheel(wheel_zip, name, location): |
| | |
| | """Get a pkg_resources distribution given a wheel. |
| | |
| | :raises UnsupportedWheel: on any errors |
| | """ |
| | info_dir, _ = parse_wheel(wheel_zip, name) |
| |
|
| | metadata_files = [ |
| | p for p in wheel_zip.namelist() if p.startswith("{}/".format(info_dir)) |
| | ] |
| |
|
| | metadata_text = {} |
| | for path in metadata_files: |
| | |
| | |
| | |
| | |
| | full_path = ensure_str(path) |
| | _, metadata_name = full_path.split("/", 1) |
| |
|
| | try: |
| | metadata_text[metadata_name] = read_wheel_metadata_file( |
| | wheel_zip, full_path |
| | ) |
| | except UnsupportedWheel as e: |
| | raise UnsupportedWheel( |
| | "{} has an invalid wheel, {}".format(name, str(e)) |
| | ) |
| |
|
| | metadata = WheelMetadata(metadata_text, location) |
| |
|
| | return DistInfoDistribution( |
| | location=location, metadata=metadata, project_name=name |
| | ) |
| |
|
| |
|
| | def parse_wheel(wheel_zip, name): |
| | |
| | """Extract information from the provided wheel, ensuring it meets basic |
| | standards. |
| | |
| | Returns the name of the .dist-info directory and the parsed WHEEL metadata. |
| | """ |
| | try: |
| | info_dir = wheel_dist_info_dir(wheel_zip, name) |
| | metadata = wheel_metadata(wheel_zip, info_dir) |
| | version = wheel_version(metadata) |
| | except UnsupportedWheel as e: |
| | raise UnsupportedWheel( |
| | "{} has an invalid wheel, {}".format(name, str(e)) |
| | ) |
| |
|
| | check_compatibility(version, name) |
| |
|
| | return info_dir, metadata |
| |
|
| |
|
| | def wheel_dist_info_dir(source, name): |
| | |
| | """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 = set(p.split("/", 1)[0] for p in source.namelist()) |
| |
|
| | info_dirs = [s for s in subdirs if s.endswith('.dist-info')] |
| |
|
| | if not info_dirs: |
| | raise UnsupportedWheel(".dist-info directory not found") |
| |
|
| | if len(info_dirs) > 1: |
| | raise UnsupportedWheel( |
| | "multiple .dist-info directories found: {}".format( |
| | ", ".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( |
| | info_dir, canonical_name |
| | ) |
| | ) |
| |
|
| | |
| | |
| | return ensure_str(info_dir) |
| |
|
| |
|
| | def read_wheel_metadata_file(source, path): |
| | |
| | try: |
| | return source.read(path) |
| | |
| | |
| | except (BadZipFile, KeyError, RuntimeError) as e: |
| | raise UnsupportedWheel( |
| | "could not read {!r} file: {!r}".format(path, e) |
| | ) |
| |
|
| |
|
| | def wheel_metadata(source, dist_info_dir): |
| | |
| | """Return the WHEEL metadata of an extracted wheel, if possible. |
| | Otherwise, raise UnsupportedWheel. |
| | """ |
| | path = "{}/WHEEL".format(dist_info_dir) |
| | |
| | wheel_contents = read_wheel_metadata_file(source, path) |
| |
|
| | try: |
| | wheel_text = ensure_str(wheel_contents) |
| | except UnicodeDecodeError as e: |
| | raise UnsupportedWheel("error decoding {!r}: {!r}".format(path, e)) |
| |
|
| | |
| | |
| | |
| | return Parser().parsestr(wheel_text) |
| |
|
| |
|
| | def wheel_version(wheel_data): |
| | |
| | """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") |
| |
|
| | version = version_text.strip() |
| |
|
| | try: |
| | return tuple(map(int, version.split('.'))) |
| | except ValueError: |
| | raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version)) |
| |
|
| |
|
| | def check_compatibility(version, name): |
| | |
| | """Raises errors or warns if called with an incompatible Wheel-Version. |
| | |
| | pip should refuse to install a Wheel-Version that's a major series |
| | ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when |
| | installing a version only minor version ahead (e.g 1.2 > 1.1). |
| | |
| | version: a 2-tuple representing a Wheel-Version (Major, Minor) |
| | name: name of wheel or package to raise exception about |
| | |
| | :raises UnsupportedWheel: when an incompatible Wheel-Version is given |
| | """ |
| | if version[0] > VERSION_COMPATIBLE[0]: |
| | raise UnsupportedWheel( |
| | "{}'s Wheel-Version ({}) is not compatible with this version " |
| | "of pip".format(name, '.'.join(map(str, version))) |
| | ) |
| | elif version > VERSION_COMPATIBLE: |
| | logger.warning( |
| | 'Installing from a newer Wheel-Version (%s)', |
| | '.'.join(map(str, version)), |
| | ) |
| |
|