comparison lib/python3.8/site-packages/pip/_internal/index/package_finder.py @ 0:9e54283cc701 draft

"planemo upload commit d12c32a45bcd441307e632fca6d9af7d60289d44"
author guerler
date Mon, 27 Jul 2020 03:47:31 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:9e54283cc701
1 """Routines related to PyPI, indexes"""
2
3 # The following comment should be removed at some point in the future.
4 # mypy: strict-optional=False
5
6 from __future__ import absolute_import
7
8 import logging
9 import re
10
11 from pip._vendor.packaging import specifiers
12 from pip._vendor.packaging.utils import canonicalize_name
13 from pip._vendor.packaging.version import parse as parse_version
14
15 from pip._internal.exceptions import (
16 BestVersionAlreadyInstalled,
17 DistributionNotFound,
18 InvalidWheelFilename,
19 UnsupportedWheel,
20 )
21 from pip._internal.index.collector import parse_links
22 from pip._internal.models.candidate import InstallationCandidate
23 from pip._internal.models.format_control import FormatControl
24 from pip._internal.models.link import Link
25 from pip._internal.models.selection_prefs import SelectionPreferences
26 from pip._internal.models.target_python import TargetPython
27 from pip._internal.models.wheel import Wheel
28 from pip._internal.utils.filetypes import WHEEL_EXTENSION
29 from pip._internal.utils.logging import indent_log
30 from pip._internal.utils.misc import build_netloc
31 from pip._internal.utils.packaging import check_requires_python
32 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
33 from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
34 from pip._internal.utils.urls import url_to_path
35
36 if MYPY_CHECK_RUNNING:
37 from typing import (
38 FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union,
39 )
40
41 from pip._vendor.packaging.tags import Tag
42 from pip._vendor.packaging.version import _BaseVersion
43
44 from pip._internal.index.collector import LinkCollector
45 from pip._internal.models.search_scope import SearchScope
46 from pip._internal.req import InstallRequirement
47 from pip._internal.utils.hashes import Hashes
48
49 BuildTag = Union[Tuple[()], Tuple[int, str]]
50 CandidateSortingKey = (
51 Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]]
52 )
53
54
55 __all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder']
56
57
58 logger = logging.getLogger(__name__)
59
60
61 def _check_link_requires_python(
62 link, # type: Link
63 version_info, # type: Tuple[int, int, int]
64 ignore_requires_python=False, # type: bool
65 ):
66 # type: (...) -> bool
67 """
68 Return whether the given Python version is compatible with a link's
69 "Requires-Python" value.
70
71 :param version_info: A 3-tuple of ints representing the Python
72 major-minor-micro version to check.
73 :param ignore_requires_python: Whether to ignore the "Requires-Python"
74 value if the given Python version isn't compatible.
75 """
76 try:
77 is_compatible = check_requires_python(
78 link.requires_python, version_info=version_info,
79 )
80 except specifiers.InvalidSpecifier:
81 logger.debug(
82 "Ignoring invalid Requires-Python (%r) for link: %s",
83 link.requires_python, link,
84 )
85 else:
86 if not is_compatible:
87 version = '.'.join(map(str, version_info))
88 if not ignore_requires_python:
89 logger.debug(
90 'Link requires a different Python (%s not in: %r): %s',
91 version, link.requires_python, link,
92 )
93 return False
94
95 logger.debug(
96 'Ignoring failed Requires-Python check (%s not in: %r) '
97 'for link: %s',
98 version, link.requires_python, link,
99 )
100
101 return True
102
103
104 class LinkEvaluator(object):
105
106 """
107 Responsible for evaluating links for a particular project.
108 """
109
110 _py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
111
112 # Don't include an allow_yanked default value to make sure each call
113 # site considers whether yanked releases are allowed. This also causes
114 # that decision to be made explicit in the calling code, which helps
115 # people when reading the code.
116 def __init__(
117 self,
118 project_name, # type: str
119 canonical_name, # type: str
120 formats, # type: FrozenSet[str]
121 target_python, # type: TargetPython
122 allow_yanked, # type: bool
123 ignore_requires_python=None, # type: Optional[bool]
124 ):
125 # type: (...) -> None
126 """
127 :param project_name: The user supplied package name.
128 :param canonical_name: The canonical package name.
129 :param formats: The formats allowed for this package. Should be a set
130 with 'binary' or 'source' or both in it.
131 :param target_python: The target Python interpreter to use when
132 evaluating link compatibility. This is used, for example, to
133 check wheel compatibility, as well as when checking the Python
134 version, e.g. the Python version embedded in a link filename
135 (or egg fragment) and against an HTML link's optional PEP 503
136 "data-requires-python" attribute.
137 :param allow_yanked: Whether files marked as yanked (in the sense
138 of PEP 592) are permitted to be candidates for install.
139 :param ignore_requires_python: Whether to ignore incompatible
140 PEP 503 "data-requires-python" values in HTML links. Defaults
141 to False.
142 """
143 if ignore_requires_python is None:
144 ignore_requires_python = False
145
146 self._allow_yanked = allow_yanked
147 self._canonical_name = canonical_name
148 self._ignore_requires_python = ignore_requires_python
149 self._formats = formats
150 self._target_python = target_python
151
152 self.project_name = project_name
153
154 def evaluate_link(self, link):
155 # type: (Link) -> Tuple[bool, Optional[Text]]
156 """
157 Determine whether a link is a candidate for installation.
158
159 :return: A tuple (is_candidate, result), where `result` is (1) a
160 version string if `is_candidate` is True, and (2) if
161 `is_candidate` is False, an optional string to log the reason
162 the link fails to qualify.
163 """
164 version = None
165 if link.is_yanked and not self._allow_yanked:
166 reason = link.yanked_reason or '<none given>'
167 # Mark this as a unicode string to prevent "UnicodeEncodeError:
168 # 'ascii' codec can't encode character" in Python 2 when
169 # the reason contains non-ascii characters.
170 return (False, u'yanked for reason: {}'.format(reason))
171
172 if link.egg_fragment:
173 egg_info = link.egg_fragment
174 ext = link.ext
175 else:
176 egg_info, ext = link.splitext()
177 if not ext:
178 return (False, 'not a file')
179 if ext not in SUPPORTED_EXTENSIONS:
180 return (False, 'unsupported archive format: %s' % ext)
181 if "binary" not in self._formats and ext == WHEEL_EXTENSION:
182 reason = 'No binaries permitted for %s' % self.project_name
183 return (False, reason)
184 if "macosx10" in link.path and ext == '.zip':
185 return (False, 'macosx10 one')
186 if ext == WHEEL_EXTENSION:
187 try:
188 wheel = Wheel(link.filename)
189 except InvalidWheelFilename:
190 return (False, 'invalid wheel filename')
191 if canonicalize_name(wheel.name) != self._canonical_name:
192 reason = 'wrong project name (not %s)' % self.project_name
193 return (False, reason)
194
195 supported_tags = self._target_python.get_tags()
196 if not wheel.supported(supported_tags):
197 # Include the wheel's tags in the reason string to
198 # simplify troubleshooting compatibility issues.
199 file_tags = wheel.get_formatted_file_tags()
200 reason = (
201 "none of the wheel's tags match: {}".format(
202 ', '.join(file_tags)
203 )
204 )
205 return (False, reason)
206
207 version = wheel.version
208
209 # This should be up by the self.ok_binary check, but see issue 2700.
210 if "source" not in self._formats and ext != WHEEL_EXTENSION:
211 return (False, 'No sources permitted for %s' % self.project_name)
212
213 if not version:
214 version = _extract_version_from_fragment(
215 egg_info, self._canonical_name,
216 )
217 if not version:
218 return (
219 False, 'Missing project version for %s' % self.project_name,
220 )
221
222 match = self._py_version_re.search(version)
223 if match:
224 version = version[:match.start()]
225 py_version = match.group(1)
226 if py_version != self._target_python.py_version:
227 return (False, 'Python version is incorrect')
228
229 supports_python = _check_link_requires_python(
230 link, version_info=self._target_python.py_version_info,
231 ignore_requires_python=self._ignore_requires_python,
232 )
233 if not supports_python:
234 # Return None for the reason text to suppress calling
235 # _log_skipped_link().
236 return (False, None)
237
238 logger.debug('Found link %s, version: %s', link, version)
239
240 return (True, version)
241
242
243 def filter_unallowed_hashes(
244 candidates, # type: List[InstallationCandidate]
245 hashes, # type: Hashes
246 project_name, # type: str
247 ):
248 # type: (...) -> List[InstallationCandidate]
249 """
250 Filter out candidates whose hashes aren't allowed, and return a new
251 list of candidates.
252
253 If at least one candidate has an allowed hash, then all candidates with
254 either an allowed hash or no hash specified are returned. Otherwise,
255 the given candidates are returned.
256
257 Including the candidates with no hash specified when there is a match
258 allows a warning to be logged if there is a more preferred candidate
259 with no hash specified. Returning all candidates in the case of no
260 matches lets pip report the hash of the candidate that would otherwise
261 have been installed (e.g. permitting the user to more easily update
262 their requirements file with the desired hash).
263 """
264 if not hashes:
265 logger.debug(
266 'Given no hashes to check %s links for project %r: '
267 'discarding no candidates',
268 len(candidates),
269 project_name,
270 )
271 # Make sure we're not returning back the given value.
272 return list(candidates)
273
274 matches_or_no_digest = []
275 # Collect the non-matches for logging purposes.
276 non_matches = []
277 match_count = 0
278 for candidate in candidates:
279 link = candidate.link
280 if not link.has_hash:
281 pass
282 elif link.is_hash_allowed(hashes=hashes):
283 match_count += 1
284 else:
285 non_matches.append(candidate)
286 continue
287
288 matches_or_no_digest.append(candidate)
289
290 if match_count:
291 filtered = matches_or_no_digest
292 else:
293 # Make sure we're not returning back the given value.
294 filtered = list(candidates)
295
296 if len(filtered) == len(candidates):
297 discard_message = 'discarding no candidates'
298 else:
299 discard_message = 'discarding {} non-matches:\n {}'.format(
300 len(non_matches),
301 '\n '.join(str(candidate.link) for candidate in non_matches)
302 )
303
304 logger.debug(
305 'Checked %s links for project %r against %s hashes '
306 '(%s matches, %s no digest): %s',
307 len(candidates),
308 project_name,
309 hashes.digest_count,
310 match_count,
311 len(matches_or_no_digest) - match_count,
312 discard_message
313 )
314
315 return filtered
316
317
318 class CandidatePreferences(object):
319
320 """
321 Encapsulates some of the preferences for filtering and sorting
322 InstallationCandidate objects.
323 """
324
325 def __init__(
326 self,
327 prefer_binary=False, # type: bool
328 allow_all_prereleases=False, # type: bool
329 ):
330 # type: (...) -> None
331 """
332 :param allow_all_prereleases: Whether to allow all pre-releases.
333 """
334 self.allow_all_prereleases = allow_all_prereleases
335 self.prefer_binary = prefer_binary
336
337
338 class BestCandidateResult(object):
339 """A collection of candidates, returned by `PackageFinder.find_best_candidate`.
340
341 This class is only intended to be instantiated by CandidateEvaluator's
342 `compute_best_candidate()` method.
343 """
344
345 def __init__(
346 self,
347 candidates, # type: List[InstallationCandidate]
348 applicable_candidates, # type: List[InstallationCandidate]
349 best_candidate, # type: Optional[InstallationCandidate]
350 ):
351 # type: (...) -> None
352 """
353 :param candidates: A sequence of all available candidates found.
354 :param applicable_candidates: The applicable candidates.
355 :param best_candidate: The most preferred candidate found, or None
356 if no applicable candidates were found.
357 """
358 assert set(applicable_candidates) <= set(candidates)
359
360 if best_candidate is None:
361 assert not applicable_candidates
362 else:
363 assert best_candidate in applicable_candidates
364
365 self._applicable_candidates = applicable_candidates
366 self._candidates = candidates
367
368 self.best_candidate = best_candidate
369
370 def iter_all(self):
371 # type: () -> Iterable[InstallationCandidate]
372 """Iterate through all candidates.
373 """
374 return iter(self._candidates)
375
376 def iter_applicable(self):
377 # type: () -> Iterable[InstallationCandidate]
378 """Iterate through the applicable candidates.
379 """
380 return iter(self._applicable_candidates)
381
382
383 class CandidateEvaluator(object):
384
385 """
386 Responsible for filtering and sorting candidates for installation based
387 on what tags are valid.
388 """
389
390 @classmethod
391 def create(
392 cls,
393 project_name, # type: str
394 target_python=None, # type: Optional[TargetPython]
395 prefer_binary=False, # type: bool
396 allow_all_prereleases=False, # type: bool
397 specifier=None, # type: Optional[specifiers.BaseSpecifier]
398 hashes=None, # type: Optional[Hashes]
399 ):
400 # type: (...) -> CandidateEvaluator
401 """Create a CandidateEvaluator object.
402
403 :param target_python: The target Python interpreter to use when
404 checking compatibility. If None (the default), a TargetPython
405 object will be constructed from the running Python.
406 :param specifier: An optional object implementing `filter`
407 (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
408 versions.
409 :param hashes: An optional collection of allowed hashes.
410 """
411 if target_python is None:
412 target_python = TargetPython()
413 if specifier is None:
414 specifier = specifiers.SpecifierSet()
415
416 supported_tags = target_python.get_tags()
417
418 return cls(
419 project_name=project_name,
420 supported_tags=supported_tags,
421 specifier=specifier,
422 prefer_binary=prefer_binary,
423 allow_all_prereleases=allow_all_prereleases,
424 hashes=hashes,
425 )
426
427 def __init__(
428 self,
429 project_name, # type: str
430 supported_tags, # type: List[Tag]
431 specifier, # type: specifiers.BaseSpecifier
432 prefer_binary=False, # type: bool
433 allow_all_prereleases=False, # type: bool
434 hashes=None, # type: Optional[Hashes]
435 ):
436 # type: (...) -> None
437 """
438 :param supported_tags: The PEP 425 tags supported by the target
439 Python in order of preference (most preferred first).
440 """
441 self._allow_all_prereleases = allow_all_prereleases
442 self._hashes = hashes
443 self._prefer_binary = prefer_binary
444 self._project_name = project_name
445 self._specifier = specifier
446 self._supported_tags = supported_tags
447
448 def get_applicable_candidates(
449 self,
450 candidates, # type: List[InstallationCandidate]
451 ):
452 # type: (...) -> List[InstallationCandidate]
453 """
454 Return the applicable candidates from a list of candidates.
455 """
456 # Using None infers from the specifier instead.
457 allow_prereleases = self._allow_all_prereleases or None
458 specifier = self._specifier
459 versions = {
460 str(v) for v in specifier.filter(
461 # We turn the version object into a str here because otherwise
462 # when we're debundled but setuptools isn't, Python will see
463 # packaging.version.Version and
464 # pkg_resources._vendor.packaging.version.Version as different
465 # types. This way we'll use a str as a common data interchange
466 # format. If we stop using the pkg_resources provided specifier
467 # and start using our own, we can drop the cast to str().
468 (str(c.version) for c in candidates),
469 prereleases=allow_prereleases,
470 )
471 }
472
473 # Again, converting version to str to deal with debundling.
474 applicable_candidates = [
475 c for c in candidates if str(c.version) in versions
476 ]
477
478 filtered_applicable_candidates = filter_unallowed_hashes(
479 candidates=applicable_candidates,
480 hashes=self._hashes,
481 project_name=self._project_name,
482 )
483
484 return sorted(filtered_applicable_candidates, key=self._sort_key)
485
486 def _sort_key(self, candidate):
487 # type: (InstallationCandidate) -> CandidateSortingKey
488 """
489 Function to pass as the `key` argument to a call to sorted() to sort
490 InstallationCandidates by preference.
491
492 Returns a tuple such that tuples sorting as greater using Python's
493 default comparison operator are more preferred.
494
495 The preference is as follows:
496
497 First and foremost, candidates with allowed (matching) hashes are
498 always preferred over candidates without matching hashes. This is
499 because e.g. if the only candidate with an allowed hash is yanked,
500 we still want to use that candidate.
501
502 Second, excepting hash considerations, candidates that have been
503 yanked (in the sense of PEP 592) are always less preferred than
504 candidates that haven't been yanked. Then:
505
506 If not finding wheels, they are sorted by version only.
507 If finding wheels, then the sort order is by version, then:
508 1. existing installs
509 2. wheels ordered via Wheel.support_index_min(self._supported_tags)
510 3. source archives
511 If prefer_binary was set, then all wheels are sorted above sources.
512
513 Note: it was considered to embed this logic into the Link
514 comparison operators, but then different sdist links
515 with the same version, would have to be considered equal
516 """
517 valid_tags = self._supported_tags
518 support_num = len(valid_tags)
519 build_tag = () # type: BuildTag
520 binary_preference = 0
521 link = candidate.link
522 if link.is_wheel:
523 # can raise InvalidWheelFilename
524 wheel = Wheel(link.filename)
525 if not wheel.supported(valid_tags):
526 raise UnsupportedWheel(
527 "%s is not a supported wheel for this platform. It "
528 "can't be sorted." % wheel.filename
529 )
530 if self._prefer_binary:
531 binary_preference = 1
532 pri = -(wheel.support_index_min(valid_tags))
533 if wheel.build_tag is not None:
534 match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
535 build_tag_groups = match.groups()
536 build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
537 else: # sdist
538 pri = -(support_num)
539 has_allowed_hash = int(link.is_hash_allowed(self._hashes))
540 yank_value = -1 * int(link.is_yanked) # -1 for yanked.
541 return (
542 has_allowed_hash, yank_value, binary_preference, candidate.version,
543 build_tag, pri,
544 )
545
546 def sort_best_candidate(
547 self,
548 candidates, # type: List[InstallationCandidate]
549 ):
550 # type: (...) -> Optional[InstallationCandidate]
551 """
552 Return the best candidate per the instance's sort order, or None if
553 no candidate is acceptable.
554 """
555 if not candidates:
556 return None
557
558 best_candidate = max(candidates, key=self._sort_key)
559
560 # Log a warning per PEP 592 if necessary before returning.
561 link = best_candidate.link
562 if link.is_yanked:
563 reason = link.yanked_reason or '<none given>'
564 msg = (
565 # Mark this as a unicode string to prevent
566 # "UnicodeEncodeError: 'ascii' codec can't encode character"
567 # in Python 2 when the reason contains non-ascii characters.
568 u'The candidate selected for download or install is a '
569 'yanked version: {candidate}\n'
570 'Reason for being yanked: {reason}'
571 ).format(candidate=best_candidate, reason=reason)
572 logger.warning(msg)
573
574 return best_candidate
575
576 def compute_best_candidate(
577 self,
578 candidates, # type: List[InstallationCandidate]
579 ):
580 # type: (...) -> BestCandidateResult
581 """
582 Compute and return a `BestCandidateResult` instance.
583 """
584 applicable_candidates = self.get_applicable_candidates(candidates)
585
586 best_candidate = self.sort_best_candidate(applicable_candidates)
587
588 return BestCandidateResult(
589 candidates,
590 applicable_candidates=applicable_candidates,
591 best_candidate=best_candidate,
592 )
593
594
595 class PackageFinder(object):
596 """This finds packages.
597
598 This is meant to match easy_install's technique for looking for
599 packages, by reading pages and looking for appropriate links.
600 """
601
602 def __init__(
603 self,
604 link_collector, # type: LinkCollector
605 target_python, # type: TargetPython
606 allow_yanked, # type: bool
607 format_control=None, # type: Optional[FormatControl]
608 candidate_prefs=None, # type: CandidatePreferences
609 ignore_requires_python=None, # type: Optional[bool]
610 ):
611 # type: (...) -> None
612 """
613 This constructor is primarily meant to be used by the create() class
614 method and from tests.
615
616 :param format_control: A FormatControl object, used to control
617 the selection of source packages / binary packages when consulting
618 the index and links.
619 :param candidate_prefs: Options to use when creating a
620 CandidateEvaluator object.
621 """
622 if candidate_prefs is None:
623 candidate_prefs = CandidatePreferences()
624
625 format_control = format_control or FormatControl(set(), set())
626
627 self._allow_yanked = allow_yanked
628 self._candidate_prefs = candidate_prefs
629 self._ignore_requires_python = ignore_requires_python
630 self._link_collector = link_collector
631 self._target_python = target_python
632
633 self.format_control = format_control
634
635 # These are boring links that have already been logged somehow.
636 self._logged_links = set() # type: Set[Link]
637
638 # Don't include an allow_yanked default value to make sure each call
639 # site considers whether yanked releases are allowed. This also causes
640 # that decision to be made explicit in the calling code, which helps
641 # people when reading the code.
642 @classmethod
643 def create(
644 cls,
645 link_collector, # type: LinkCollector
646 selection_prefs, # type: SelectionPreferences
647 target_python=None, # type: Optional[TargetPython]
648 ):
649 # type: (...) -> PackageFinder
650 """Create a PackageFinder.
651
652 :param selection_prefs: The candidate selection preferences, as a
653 SelectionPreferences object.
654 :param target_python: The target Python interpreter to use when
655 checking compatibility. If None (the default), a TargetPython
656 object will be constructed from the running Python.
657 """
658 if target_python is None:
659 target_python = TargetPython()
660
661 candidate_prefs = CandidatePreferences(
662 prefer_binary=selection_prefs.prefer_binary,
663 allow_all_prereleases=selection_prefs.allow_all_prereleases,
664 )
665
666 return cls(
667 candidate_prefs=candidate_prefs,
668 link_collector=link_collector,
669 target_python=target_python,
670 allow_yanked=selection_prefs.allow_yanked,
671 format_control=selection_prefs.format_control,
672 ignore_requires_python=selection_prefs.ignore_requires_python,
673 )
674
675 @property
676 def search_scope(self):
677 # type: () -> SearchScope
678 return self._link_collector.search_scope
679
680 @search_scope.setter
681 def search_scope(self, search_scope):
682 # type: (SearchScope) -> None
683 self._link_collector.search_scope = search_scope
684
685 @property
686 def find_links(self):
687 # type: () -> List[str]
688 return self._link_collector.find_links
689
690 @property
691 def index_urls(self):
692 # type: () -> List[str]
693 return self.search_scope.index_urls
694
695 @property
696 def trusted_hosts(self):
697 # type: () -> Iterable[str]
698 for host_port in self._link_collector.session.pip_trusted_origins:
699 yield build_netloc(*host_port)
700
701 @property
702 def allow_all_prereleases(self):
703 # type: () -> bool
704 return self._candidate_prefs.allow_all_prereleases
705
706 def set_allow_all_prereleases(self):
707 # type: () -> None
708 self._candidate_prefs.allow_all_prereleases = True
709
710 def make_link_evaluator(self, project_name):
711 # type: (str) -> LinkEvaluator
712 canonical_name = canonicalize_name(project_name)
713 formats = self.format_control.get_allowed_formats(canonical_name)
714
715 return LinkEvaluator(
716 project_name=project_name,
717 canonical_name=canonical_name,
718 formats=formats,
719 target_python=self._target_python,
720 allow_yanked=self._allow_yanked,
721 ignore_requires_python=self._ignore_requires_python,
722 )
723
724 def _sort_links(self, links):
725 # type: (Iterable[Link]) -> List[Link]
726 """
727 Returns elements of links in order, non-egg links first, egg links
728 second, while eliminating duplicates
729 """
730 eggs, no_eggs = [], []
731 seen = set() # type: Set[Link]
732 for link in links:
733 if link not in seen:
734 seen.add(link)
735 if link.egg_fragment:
736 eggs.append(link)
737 else:
738 no_eggs.append(link)
739 return no_eggs + eggs
740
741 def _log_skipped_link(self, link, reason):
742 # type: (Link, Text) -> None
743 if link not in self._logged_links:
744 # Mark this as a unicode string to prevent "UnicodeEncodeError:
745 # 'ascii' codec can't encode character" in Python 2 when
746 # the reason contains non-ascii characters.
747 # Also, put the link at the end so the reason is more visible
748 # and because the link string is usually very long.
749 logger.debug(u'Skipping link: %s: %s', reason, link)
750 self._logged_links.add(link)
751
752 def get_install_candidate(self, link_evaluator, link):
753 # type: (LinkEvaluator, Link) -> Optional[InstallationCandidate]
754 """
755 If the link is a candidate for install, convert it to an
756 InstallationCandidate and return it. Otherwise, return None.
757 """
758 is_candidate, result = link_evaluator.evaluate_link(link)
759 if not is_candidate:
760 if result:
761 self._log_skipped_link(link, reason=result)
762 return None
763
764 return InstallationCandidate(
765 name=link_evaluator.project_name,
766 link=link,
767 # Convert the Text result to str since InstallationCandidate
768 # accepts str.
769 version=str(result),
770 )
771
772 def evaluate_links(self, link_evaluator, links):
773 # type: (LinkEvaluator, Iterable[Link]) -> List[InstallationCandidate]
774 """
775 Convert links that are candidates to InstallationCandidate objects.
776 """
777 candidates = []
778 for link in self._sort_links(links):
779 candidate = self.get_install_candidate(link_evaluator, link)
780 if candidate is not None:
781 candidates.append(candidate)
782
783 return candidates
784
785 def process_project_url(self, project_url, link_evaluator):
786 # type: (Link, LinkEvaluator) -> List[InstallationCandidate]
787 logger.debug(
788 'Fetching project page and analyzing links: %s', project_url,
789 )
790 html_page = self._link_collector.fetch_page(project_url)
791 if html_page is None:
792 return []
793
794 page_links = list(parse_links(html_page))
795
796 with indent_log():
797 package_links = self.evaluate_links(
798 link_evaluator,
799 links=page_links,
800 )
801
802 return package_links
803
804 def find_all_candidates(self, project_name):
805 # type: (str) -> List[InstallationCandidate]
806 """Find all available InstallationCandidate for project_name
807
808 This checks index_urls and find_links.
809 All versions found are returned as an InstallationCandidate list.
810
811 See LinkEvaluator.evaluate_link() for details on which files
812 are accepted.
813 """
814 collected_links = self._link_collector.collect_links(project_name)
815
816 link_evaluator = self.make_link_evaluator(project_name)
817
818 find_links_versions = self.evaluate_links(
819 link_evaluator,
820 links=collected_links.find_links,
821 )
822
823 page_versions = []
824 for project_url in collected_links.project_urls:
825 package_links = self.process_project_url(
826 project_url, link_evaluator=link_evaluator,
827 )
828 page_versions.extend(package_links)
829
830 file_versions = self.evaluate_links(
831 link_evaluator,
832 links=collected_links.files,
833 )
834 if file_versions:
835 file_versions.sort(reverse=True)
836 logger.debug(
837 'Local files found: %s',
838 ', '.join([
839 url_to_path(candidate.link.url)
840 for candidate in file_versions
841 ])
842 )
843
844 # This is an intentional priority ordering
845 return file_versions + find_links_versions + page_versions
846
847 def make_candidate_evaluator(
848 self,
849 project_name, # type: str
850 specifier=None, # type: Optional[specifiers.BaseSpecifier]
851 hashes=None, # type: Optional[Hashes]
852 ):
853 # type: (...) -> CandidateEvaluator
854 """Create a CandidateEvaluator object to use.
855 """
856 candidate_prefs = self._candidate_prefs
857 return CandidateEvaluator.create(
858 project_name=project_name,
859 target_python=self._target_python,
860 prefer_binary=candidate_prefs.prefer_binary,
861 allow_all_prereleases=candidate_prefs.allow_all_prereleases,
862 specifier=specifier,
863 hashes=hashes,
864 )
865
866 def find_best_candidate(
867 self,
868 project_name, # type: str
869 specifier=None, # type: Optional[specifiers.BaseSpecifier]
870 hashes=None, # type: Optional[Hashes]
871 ):
872 # type: (...) -> BestCandidateResult
873 """Find matches for the given project and specifier.
874
875 :param specifier: An optional object implementing `filter`
876 (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
877 versions.
878
879 :return: A `BestCandidateResult` instance.
880 """
881 candidates = self.find_all_candidates(project_name)
882 candidate_evaluator = self.make_candidate_evaluator(
883 project_name=project_name,
884 specifier=specifier,
885 hashes=hashes,
886 )
887 return candidate_evaluator.compute_best_candidate(candidates)
888
889 def find_requirement(self, req, upgrade):
890 # type: (InstallRequirement, bool) -> Optional[Link]
891 """Try to find a Link matching req
892
893 Expects req, an InstallRequirement and upgrade, a boolean
894 Returns a Link if found,
895 Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
896 """
897 hashes = req.hashes(trust_internet=False)
898 best_candidate_result = self.find_best_candidate(
899 req.name, specifier=req.specifier, hashes=hashes,
900 )
901 best_candidate = best_candidate_result.best_candidate
902
903 installed_version = None # type: Optional[_BaseVersion]
904 if req.satisfied_by is not None:
905 installed_version = parse_version(req.satisfied_by.version)
906
907 def _format_versions(cand_iter):
908 # type: (Iterable[InstallationCandidate]) -> str
909 # This repeated parse_version and str() conversion is needed to
910 # handle different vendoring sources from pip and pkg_resources.
911 # If we stop using the pkg_resources provided specifier and start
912 # using our own, we can drop the cast to str().
913 return ", ".join(sorted(
914 {str(c.version) for c in cand_iter},
915 key=parse_version,
916 )) or "none"
917
918 if installed_version is None and best_candidate is None:
919 logger.critical(
920 'Could not find a version that satisfies the requirement %s '
921 '(from versions: %s)',
922 req,
923 _format_versions(best_candidate_result.iter_all()),
924 )
925
926 raise DistributionNotFound(
927 'No matching distribution found for %s' % req
928 )
929
930 best_installed = False
931 if installed_version and (
932 best_candidate is None or
933 best_candidate.version <= installed_version):
934 best_installed = True
935
936 if not upgrade and installed_version is not None:
937 if best_installed:
938 logger.debug(
939 'Existing installed version (%s) is most up-to-date and '
940 'satisfies requirement',
941 installed_version,
942 )
943 else:
944 logger.debug(
945 'Existing installed version (%s) satisfies requirement '
946 '(most up-to-date version is %s)',
947 installed_version,
948 best_candidate.version,
949 )
950 return None
951
952 if best_installed:
953 # We have an existing version, and its the best version
954 logger.debug(
955 'Installed version (%s) is most up-to-date (past versions: '
956 '%s)',
957 installed_version,
958 _format_versions(best_candidate_result.iter_applicable()),
959 )
960 raise BestVersionAlreadyInstalled
961
962 logger.debug(
963 'Using version %s (newest of versions: %s)',
964 best_candidate.version,
965 _format_versions(best_candidate_result.iter_applicable()),
966 )
967 return best_candidate.link
968
969
970 def _find_name_version_sep(fragment, canonical_name):
971 # type: (str, str) -> int
972 """Find the separator's index based on the package's canonical name.
973
974 :param fragment: A <package>+<version> filename "fragment" (stem) or
975 egg fragment.
976 :param canonical_name: The package's canonical name.
977
978 This function is needed since the canonicalized name does not necessarily
979 have the same length as the egg info's name part. An example::
980
981 >>> fragment = 'foo__bar-1.0'
982 >>> canonical_name = 'foo-bar'
983 >>> _find_name_version_sep(fragment, canonical_name)
984 8
985 """
986 # Project name and version must be separated by one single dash. Find all
987 # occurrences of dashes; if the string in front of it matches the canonical
988 # name, this is the one separating the name and version parts.
989 for i, c in enumerate(fragment):
990 if c != "-":
991 continue
992 if canonicalize_name(fragment[:i]) == canonical_name:
993 return i
994 raise ValueError("{} does not match {}".format(fragment, canonical_name))
995
996
997 def _extract_version_from_fragment(fragment, canonical_name):
998 # type: (str, str) -> Optional[str]
999 """Parse the version string from a <package>+<version> filename
1000 "fragment" (stem) or egg fragment.
1001
1002 :param fragment: The string to parse. E.g. foo-2.1
1003 :param canonical_name: The canonicalized name of the package this
1004 belongs to.
1005 """
1006 try:
1007 version_start = _find_name_version_sep(fragment, canonical_name) + 1
1008 except ValueError:
1009 return None
1010 version = fragment[version_start:]
1011 if not version:
1012 return None
1013 return version