def _verify_one(req, wheel_path): # type: (InstallRequirement, str) -> None canonical_name = canonicalize_name(req.name or "") w = Wheel(os.path.basename(wheel_path)) if canonicalize_name(w.name) != canonical_name: raise InvalidWheelFilename( "Wheel has unexpected file name: expected {!r}, " "got {!r}".format(canonical_name, w.name), ) dist = get_wheel_distribution(wheel_path, canonical_name) dist_verstr = str(dist.version) if canonicalize_version(dist_verstr) != canonicalize_version(w.version): raise InvalidWheelFilename( "Wheel has unexpected file name: expected {!r}, " "got {!r}".format(dist_verstr, w.version), ) metadata_version_value = dist.metadata_version if metadata_version_value is None: raise UnsupportedWheel("Missing Metadata-Version") try: metadata_version = Version(metadata_version_value) except InvalidVersion: msg = f"Invalid Metadata-Version: {metadata_version_value}" raise UnsupportedWheel(msg) if (metadata_version >= Version("1.2") and not isinstance(dist.version, Version)): raise UnsupportedWheel( "Metadata 1.2 mandates PEP 440 version, " "but {!r} is not".format(dist_verstr) )
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 check_compatibility(version, name): # type: (Optional[Tuple[int, ...]], 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 not version: raise UnsupportedWheel("%s is in an unsupported or invalid wheel" % name) if version[0] > VERSION_COMPATIBLE[0]: raise UnsupportedWheel( "%s's Wheel-Version (%s) is not compatible with this version " "of pip" % (name, '.'.join(map(str, version)))) elif version > VERSION_COMPATIBLE: logger.warning( 'Installing from a newer Wheel-Version (%s)', '.'.join(map(str, version)), )
def wheel_dist_info_dir(source, name): # type: (ZipFile, 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 = list(set(p.split("/")[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)) # Zip file paths can be unicode or str depending on the zip entry flags, # so normalize it. return ensure_str(info_dir)
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 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 from_wheel(cls, wheel: Wheel, name: str) -> "Distribution": """Load the distribution from a given wheel. :raises InvalidWheel: Whenever loading of the wheel causes a :py:exc:`zipfile.BadZipFile` exception to be thrown. :raises UnsupportedWheel: If the wheel is a valid zip, but malformed internally. """ try: with wheel.as_zipfile() as zf: info_dir, _ = parse_wheel(zf, name) metadata_text = { path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path) for path in zf.namelist() if path.startswith(f"{info_dir}/") } except zipfile.BadZipFile as e: raise InvalidWheel(wheel.location, name) from e except UnsupportedWheel as e: raise UnsupportedWheel(f"{name} has an invalid wheel, {e}") dist = pkg_resources.DistInfoDistribution( location=wheel.location, metadata=WheelMetadata(metadata_text, wheel.location), project_name=name, ) return cls(dist)
def pkg_resources_distribution_for_wheel(wheel_zip, name, location): # type: (ZipFile, str, str) -> Distribution """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(f"{info_dir}/") ] metadata_text = {} # type: Dict[str, bytes] for path in metadata_files: _, metadata_name = path.split("/", 1) try: metadata_text[metadata_name] = read_wheel_metadata_file( wheel_zip, 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 pkg_resources_distribution_for_wheel(wheel_zip, name, location): # type: (ZipFile, str, str) -> Distribution """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 = {} # type: Dict[str, bytes] for path in metadata_files: # If a flag is set, namelist entries may be unicode in Python 2. # We coerce them to native str type to match the types used in the rest # of the code. This cannot fail because unicode can always be encoded # with UTF-8. 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 get_metadata(self, name: str) -> str: try: return self._metadata[name].decode() except UnicodeDecodeError as e: # Augment the default error with the origin of the file. raise UnsupportedWheel( f"Error decoding metadata for {self._wheel_name}: {e} in {name} file" )
def get_metadata(self, name): # type: (str) -> str try: return super(WheelMetadata, self).get_metadata(name) except UnicodeDecodeError as e: # Augment the default error with the origin of the file. raise UnsupportedWheel("Error decoding metadata for {}: {}".format( self._wheel_name, e))
def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None: if not link.is_wheel: return wheel = Wheel(link.filename) if wheel.supported(self._finder.target_python.get_tags()): return msg = f"{link.filename} is not a supported wheel on this platform." raise UnsupportedWheel(msg)
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_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 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)), )
def _candidate_sort_key(self, candidate, ignore_compatibility=True): """ Function used to generate link sort key for link tuples. The greater the return value, the more preferred it is. If not finding wheels, then sorted by version only. If finding wheels, then the sort order is by version, then: 1. existing installs 2. wheels ordered via Wheel.support_index_min(self.valid_tags) 3. source archives If prefer_binary was set, then all wheels are sorted above sources. Note: it was considered to embed this logic into the Link comparison operators, but then different sdist links with the same version, would have to be considered equal """ support_num = len(self.valid_tags) build_tag = tuple() binary_preference = 0 if candidate.location.is_wheel: # can raise InvalidWheelFilename wheel = Wheel(candidate.location.filename) if not wheel.supported( self.valid_tags) and not ignore_compatibility: raise UnsupportedWheel( "%s is not a supported wheel for this platform. It " "can't be sorted." % wheel.filename) if self.prefer_binary: binary_preference = 1 tags = self.valid_tags if not ignore_compatibility else None try: pri = -(wheel.support_index_min(tags=tags)) except TypeError: pri = -(support_num) if wheel.build_tag is not None: match = re.match(r'^(\d+)(.*)$', wheel.build_tag) build_tag_groups = match.groups() build_tag = (int(build_tag_groups[0]), build_tag_groups[1]) else: # sdist pri = -(support_num) return (binary_preference, candidate.version, build_tag, pri)
def _sort_key(self, candidate: InstallationCandidate, ignore_compatibility: bool = True) -> CandidateSortingKey: """ Function to pass as the `key` argument to a call to sorted() to sort InstallationCandidates by preference. Returns a tuple such that tuples sorting as greater using Python's default comparison operator are more preferred. The preference is as follows: First and foremost, candidates with allowed (matching) hashes are always preferred over candidates without matching hashes. This is because e.g. if the only candidate with an allowed hash is yanked, we still want to use that candidate. Second, excepting hash considerations, candidates that have been yanked (in the sense of PEP 592) are always less preferred than candidates that haven't been yanked. Then: If not finding wheels, they are sorted by version only. If finding wheels, then the sort order is by version, then: 1. existing installs 2. wheels ordered via Wheel.support_index_min(self._supported_tags) 3. source archives If prefer_binary was set, then all wheels are sorted above sources. Note: it was considered to embed this logic into the Link comparison operators, but then different sdist links with the same version, would have to be considered equal """ valid_tags = self._supported_tags support_num = len(valid_tags) build_tag: BuildTag = () binary_preference = 0 link = candidate.link if link.is_wheel: # can raise InvalidWheelFilename wheel = Wheel(link.filename) try: pri = -(wheel.find_most_preferred_tag( valid_tags, self._wheel_tag_preferences)) except ValueError: if not ignore_compatibility: raise UnsupportedWheel( "{} is not a supported wheel for this platform. It " "can't be sorted.".format(wheel.filename)) pri = -(support_num) if self._prefer_binary: binary_preference = 1 if wheel.build_tag is not None: match = re.match(r'^(\d+)(.*)$', wheel.build_tag) build_tag_groups = match.groups() build_tag = (int(build_tag_groups[0]), build_tag_groups[1]) else: # sdist pri = -(support_num) has_allowed_hash = int(link.is_hash_allowed(self._hashes)) yank_value = -1 * int(link.is_yanked) # -1 for yanked. return ( has_allowed_hash, yank_value, binary_preference, candidate.version, pri, build_tag, )