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 _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 _fetch_metadata_using_lazy_wheel( self, link: Link, ) -> Optional[BaseDistribution]: """Fetch metadata using lazy wheel, if possible.""" if not self.use_lazy_wheel: return None if self.require_hashes: logger.debug("Lazy wheel is not used as hash checking is required") return None if link.is_file or not link.is_wheel: logger.debug( "Lazy wheel is not used as %r does not points to a remote wheel", link, ) return None wheel = Wheel(link.filename) name = canonicalize_name(wheel.name) logger.info( "Obtaining dependency information from %s %s", name, wheel.version, ) url = link.url.split("#", 1)[0] try: return dist_from_wheel_url(name, url, self._session) except HTTPRangeRequestUnsupported: logger.debug("%s does not support range requests", url) return None
def get( self, link, # type: Link package_name, # type: Optional[str] supported_tags, # type: List[Tag] ): # type: (...) -> Link candidates = [] if not package_name: return link canonical_package_name = canonicalize_name(package_name) for wheel_name, wheel_dir in self._get_candidates( link, canonical_package_name ): try: wheel = Wheel(wheel_name) except InvalidWheelFilename: continue if canonicalize_name(wheel.name) != canonical_package_name: logger.debug( "Ignoring cached wheel %s for %s as it " "does not match the expected distribution name %s.", wheel_name, link, package_name, ) continue if not wheel.supported(supported_tags): # Built for a different python/arch/etc continue candidates.append( ( wheel.support_index_min(supported_tags), wheel_name, wheel_dir, ) ) if not candidates: return link _, wheel_name, wheel_dir = min(candidates) return Link(path_to_url(os.path.join(wheel_dir, wheel_name)))
def __init__( self, link: Link, template: InstallRequirement, factory: "Factory", name: Optional[NormalizedName] = None, version: Optional[CandidateVersion] = None, ) -> None: source_link = link cache_entry = factory.get_wheel_cache_entry(link, name) if cache_entry is not None: logger.debug("Using cached wheel link: %s", cache_entry.link) link = cache_entry.link ireq = make_install_req_from_link(link, template) assert ireq.link == link if ireq.link.is_wheel and not ireq.link.is_file: wheel = Wheel(ireq.link.filename) wheel_name = canonicalize_name(wheel.name) assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel" # Version may not be present for PEP 508 direct URLs if version is not None: wheel_version = Version(wheel.version) assert version == wheel_version, "{!r} != {!r} for wheel {}".format( version, wheel_version, name) if (cache_entry is not None and cache_entry.persistent and template.link is template.original_link): ireq.original_link_is_in_wheel_cache = True super().__init__( link=link, source_link=source_link, ireq=ireq, factory=factory, name=name, version=version, )
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, )
def evaluate_link(self, link: Link) -> Tuple[bool, Optional[str]]: """ Determine whether a link is a candidate for installation. :return: A tuple (is_candidate, result), where `result` is (1) a version string if `is_candidate` is True, and (2) if `is_candidate` is False, an optional string to log the reason the link fails to qualify. """ version = None if link.is_yanked and not self._allow_yanked: reason = link.yanked_reason or '<none given>' return (False, f'yanked for reason: {reason}') if link.egg_fragment: egg_info = link.egg_fragment ext = link.ext else: egg_info, ext = link.splitext() if not ext: return (False, 'not a file') if ext not in SUPPORTED_EXTENSIONS: return (False, f'unsupported archive format: {ext}') if "binary" not in self._formats and ext == WHEEL_EXTENSION and not self._ignore_compatibility: reason = 'No binaries permitted for {}'.format( self.project_name) return (False, reason) if "macosx10" in link.path and ext == '.zip' and not self._ignore_compatibility: return (False, 'macosx10 one') if ext == WHEEL_EXTENSION: try: wheel = Wheel(link.filename) except InvalidWheelFilename: return (False, 'invalid wheel filename') if canonicalize_name(wheel.name) != self._canonical_name: reason = 'wrong project name (not {})'.format( self.project_name) return (False, reason) supported_tags = self._target_python.get_tags() if not wheel.supported( supported_tags) and not self._ignore_compatibility: # Include the wheel's tags in the reason string to # simplify troubleshooting compatibility issues. file_tags = wheel.get_formatted_file_tags() reason = ( "none of the wheel's tags ({}) are compatible " "(run pip debug --verbose to show compatible tags)". format(', '.join(file_tags))) return (False, reason) version = wheel.version # This should be up by the self.ok_binary check, but see issue 2700. if "source" not in self._formats and ext != WHEEL_EXTENSION: reason = f'No sources permitted for {self.project_name}' return (False, reason) if not version: version = _extract_version_from_fragment( egg_info, self._canonical_name, ) if not version: reason = f'Missing project version for {self.project_name}' return (False, reason) match = self._py_version_re.search(version) if match: version = version[:match.start()] py_version = match.group(1) if py_version != self._target_python.py_version: return (False, 'Python version is incorrect') supports_python = _check_link_requires_python( link, version_info=self._target_python.py_version_info, ignore_requires_python=self._ignore_requires_python, ) if not supports_python and not self._ignore_compatibility: # Return None for the reason text to suppress calling # _log_skipped_link(). return (False, None) logger.debug('Found link %s, version: %s', link, version) return (True, version)
def add_requirement( self, install_req: InstallRequirement, parent_req_name: Optional[str] = None, extras_requested: Optional[Iterable[str]] = None ) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]: """Add install_req as a requirement to install. :param parent_req_name: The name of the requirement that needed this added. The name is used because when multiple unnamed requirements resolve to the same name, we could otherwise end up with dependency links that point outside the Requirements set. parent_req must already be added. Note that None implies that this is a user supplied requirement, vs an inferred one. :param extras_requested: an iterable of extras used to evaluate the environment markers. :return: Additional requirements to scan. That is either [] if the requirement is not applicable, or [install_req] if the requirement is applicable and has just been added. """ # If the markers do not match, ignore this requirement. if not install_req.match_markers(extras_requested): logger.info( "Ignoring %s: markers '%s' don't match your environment", install_req.name, install_req.markers, ) return [], None # If the wheel is not supported, raise an error. # Should check this after filtering out based on environment markers to # allow specifying different wheels based on the environment/OS, in a # single requirements file. if install_req.link and install_req.link.is_wheel: wheel = Wheel(install_req.link.filename) tags = compatibility_tags.get_supported() if (self.check_supported_wheels and not wheel.supported(tags)): raise InstallationError( "{} is not a supported wheel on this platform.".format( wheel.filename) ) # This next bit is really a sanity check. assert not install_req.user_supplied or parent_req_name is None, ( "a user supplied req shouldn't have a parent" ) # Unnamed requirements are scanned again and the requirement won't be # added as a dependency until after scanning. if not install_req.name: self.add_unnamed_requirement(install_req) return [install_req], None try: existing_req: Optional[InstallRequirement] = self.get_requirement( install_req.name) except KeyError: existing_req = None has_conflicting_requirement = ( parent_req_name is None and existing_req and not existing_req.constraint and existing_req.extras == install_req.extras and existing_req.req and install_req.req and existing_req.req.specifier != install_req.req.specifier ) if has_conflicting_requirement: raise InstallationError( "Double requirement given: {} (already in {}, name={!r})" .format(install_req, existing_req, install_req.name) ) # When no existing requirement exists, add the requirement as a # dependency and it will be scanned again after. if not existing_req: self.add_named_requirement(install_req) # We'd want to rescan this requirement later return [install_req], install_req # Assume there's no need to scan, and that we've already # encountered this for scanning. if install_req.constraint or not existing_req.constraint: return [], existing_req does_not_satisfy_constraint = ( install_req.link and not ( existing_req.link and install_req.link.path == existing_req.link.path ) ) if does_not_satisfy_constraint: raise InstallationError( "Could not satisfy constraints for '{}': " "installation from path or url cannot be " "constrained to a version".format(install_req.name) ) # If we're now installing a constraint, mark the existing # object for real installation. existing_req.constraint = False # If we're now installing a user supplied requirement, # mark the existing object as such. if install_req.user_supplied: existing_req.user_supplied = True existing_req.extras = tuple(sorted( set(existing_req.extras) | set(install_req.extras) )) logger.debug( "Setting %s extras to: %s", existing_req, existing_req.extras, ) # Return the existing requirement for addition to the parent and # scanning again. return [existing_req], existing_req
def parse_req_from_line(name, line_source): # type: (str, Optional[str]) -> RequirementParts if is_url(name): marker_sep = '; ' else: marker_sep = ';' if marker_sep in name: name, markers_as_string = name.split(marker_sep, 1) markers_as_string = markers_as_string.strip() if not markers_as_string: markers = None else: markers = Marker(markers_as_string) else: markers = None name = name.strip() req_as_string = None path = os.path.normpath(os.path.abspath(name)) link = None extras_as_string = None if is_url(name): link = Link(name) else: p, extras_as_string = _strip_extras(path) url = _get_url_from_path(p, name) if url is not None: link = Link(url) # it's a local file, dir, or url if link: # Handle relative file URLs if link.scheme == 'file' and re.search(r'\.\./', link.url): link = Link( path_to_url(os.path.normpath(os.path.abspath(link.path)))) # wheel file if link.is_wheel: wheel = Wheel(link.filename) # can raise InvalidWheelFilename req_as_string = "%s==%s" % (wheel.name, wheel.version) else: # set the req to the egg fragment. when it's not there, this # will become an 'unnamed' requirement req_as_string = link.egg_fragment # a requirement specifier else: req_as_string = name extras = convert_extras(extras_as_string) def with_source(text): # type: (str) -> str if not line_source: return text return '{} (from {})'.format(text, line_source) if req_as_string is not None: try: req = Requirement(req_as_string) except InvalidRequirement: if os.path.sep in req_as_string: add_msg = "It looks like a path." add_msg += deduce_helpful_msg(req_as_string) elif ('=' in req_as_string and not any(op in req_as_string for op in operators)): add_msg = "= is not a valid operator. Did you mean == ?" else: add_msg = '' msg = with_source( 'Invalid requirement: {!r}'.format(req_as_string) ) if add_msg: msg += '\nHint: {}'.format(add_msg) raise InstallationError(msg) else: req = None return RequirementParts(req, link, markers, extras)
def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts: if is_url(name): marker_sep = "; " else: marker_sep = ";" if marker_sep in name: name, markers_as_string = name.split(marker_sep, 1) markers_as_string = markers_as_string.strip() if not markers_as_string: markers = None else: markers = Marker(markers_as_string) else: markers = None name = name.strip() req_as_string = None path = os.path.normpath(os.path.abspath(name)) link = None extras_as_string = None if is_url(name): link = Link(name) else: p, extras_as_string = _strip_extras(path) url = _get_url_from_path(p, name) if url is not None: link = Link(url) # it's a local file, dir, or url if link: # Handle relative file URLs if link.scheme == "file" and re.search(r"\.\./", link.url): link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path)))) # wheel file if link.is_wheel: wheel = Wheel(link.filename) # can raise InvalidWheelFilename req_as_string = f"{wheel.name}=={wheel.version}" else: # set the req to the egg fragment. when it's not there, this # will become an 'unnamed' requirement req_as_string = link.egg_fragment # a requirement specifier else: req_as_string = name extras = convert_extras(extras_as_string) def with_source(text: str) -> str: if not line_source: return text return f"{text} (from {line_source})" def _parse_req_string(req_as_string: str) -> Requirement: try: req = get_requirement(req_as_string) except InvalidRequirement: if os.path.sep in req_as_string: add_msg = "It looks like a path." add_msg += deduce_helpful_msg(req_as_string) elif "=" in req_as_string and not any( op in req_as_string for op in operators ): add_msg = "= is not a valid operator. Did you mean == ?" else: add_msg = "" msg = with_source(f"Invalid requirement: {req_as_string!r}") if add_msg: msg += f"\nHint: {add_msg}" raise InstallationError(msg) else: # Deprecate extras after specifiers: "name>=1.0[extras]" # This currently works by accident because _strip_extras() parses # any extras in the end of the string and those are saved in # RequirementParts for spec in req.specifier: spec_str = str(spec) if spec_str.endswith("]"): msg = f"Extras after version '{spec_str}'." raise InstallationError(msg) return req if req_as_string is not None: req: Optional[Requirement] = _parse_req_string(req_as_string) else: req = None return RequirementParts(req, link, markers, extras)