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