You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					137 lines
				
				4.4 KiB
			
		
		
			
		
	
	
					137 lines
				
				4.4 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								"""Support functions for working with wheel files.
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import logging
							 | 
						||
| 
								 | 
							
								from email.message import Message
							 | 
						||
| 
								 | 
							
								from email.parser import Parser
							 | 
						||
| 
								 | 
							
								from typing import Tuple
							 | 
						||
| 
								 | 
							
								from zipfile import BadZipFile, ZipFile
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from pip._vendor.packaging.utils import canonicalize_name
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from pip._internal.exceptions import UnsupportedWheel
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								VERSION_COMPATIBLE = (1, 0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								logger = logging.getLogger(__name__)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def parse_wheel(wheel_zip: ZipFile, name: str) -> Tuple[str, Message]:
							 | 
						||
| 
								 | 
							
								    """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: ZipFile, name: 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.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    # Zip file path separators must be /
							 | 
						||
| 
								 | 
							
								    subdirs = {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 info_dir
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def read_wheel_metadata_file(source: ZipFile, path: str) -> bytes:
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        return source.read(path)
							 | 
						||
| 
								 | 
							
								        # BadZipFile for general corruption, KeyError for missing entry,
							 | 
						||
| 
								 | 
							
								        # and RuntimeError for password-protected files
							 | 
						||
| 
								 | 
							
								    except (BadZipFile, KeyError, RuntimeError) as e:
							 | 
						||
| 
								 | 
							
								        raise UnsupportedWheel(f"could not read {path!r} file: {e!r}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def wheel_metadata(source: ZipFile, dist_info_dir: str) -> Message:
							 | 
						||
| 
								 | 
							
								    """Return the WHEEL metadata of an extracted wheel, if possible.
							 | 
						||
| 
								 | 
							
								    Otherwise, raise UnsupportedWheel.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    path = f"{dist_info_dir}/WHEEL"
							 | 
						||
| 
								 | 
							
								    # Zip file path separators must be /
							 | 
						||
| 
								 | 
							
								    wheel_contents = read_wheel_metadata_file(source, path)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        wheel_text = wheel_contents.decode()
							 | 
						||
| 
								 | 
							
								    except UnicodeDecodeError as e:
							 | 
						||
| 
								 | 
							
								        raise UnsupportedWheel(f"error decoding {path!r}: {e!r}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # FeedParser (used by Parser) does not raise any exceptions. The returned
							 | 
						||
| 
								 | 
							
								    # message may have .defects populated, but for backwards-compatibility we
							 | 
						||
| 
								 | 
							
								    # currently ignore them.
							 | 
						||
| 
								 | 
							
								    return Parser().parsestr(wheel_text)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def wheel_version(wheel_data: 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")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    version = version_text.strip()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        return tuple(map(int, version.split(".")))
							 | 
						||
| 
								 | 
							
								    except ValueError:
							 | 
						||
| 
								 | 
							
								        raise UnsupportedWheel(f"invalid Wheel-Version: {version!r}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def check_compatibility(version: Tuple[int, ...], name: str) -> None:
							 | 
						||
| 
								 | 
							
								    """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)),
							 | 
						||
| 
								 | 
							
								        )
							 |