55
55
BuildTag = Tuple [Any , ...] # either empty tuple or Tuple[int, str]
56
56
CandidateSortingKey = Tuple [int , _BaseVersion , BuildTag , Optional [int ]]
57
57
58
- __all__ = ['FormatControl' , 'PackageFinder' ]
58
+
59
+ __all__ = ['FormatControl' , 'FoundCandidates' , 'PackageFinder' ]
59
60
60
61
61
62
SECURE_ORIGINS = [
@@ -254,6 +255,67 @@ def _get_html_page(link, session=None):
254
255
return None
255
256
256
257
258
+ class FoundCandidates (object ):
259
+ """A collection of candidates, returned by `PackageFinder.find_candidates`.
260
+
261
+ Arguments:
262
+
263
+ * `candidates`: A sequence of all available candidates found.
264
+ * `specifier`: Specifier to filter applicable versions.
265
+ * `prereleases`: Whether prereleases should be accounted. Pass None to
266
+ infer from the specifier.
267
+ * `sort_key`: A callable used as the key function when choosing the best
268
+ candidate.
269
+ """
270
+ def __init__ (
271
+ self ,
272
+ candidates , # type: List[InstallationCandidate]
273
+ specifier , # type: specifiers.BaseSpecifier
274
+ prereleases , # type: Optional[bool]
275
+ sort_key , # type: Callable[[InstallationCandidate], Any]
276
+ ):
277
+ # type: (...) -> None
278
+ self ._candidates = candidates
279
+ self ._specifier = specifier
280
+ self ._prereleases = prereleases
281
+ self ._sort_key = sort_key
282
+
283
+ def iter_all (self ):
284
+ # type: () -> Iterable[InstallationCandidate]
285
+ """Iterate through all candidates.
286
+ """
287
+ return iter (self ._candidates )
288
+
289
+ def iter_applicable (self ):
290
+ # type: () -> Iterable[InstallationCandidate]
291
+ """Iterate through candidates matching the given specifier.
292
+ """
293
+ # Filter out anything which doesn't match our specifier.
294
+ versions = set (self ._specifier .filter (
295
+ # We turn the version object into a str here because otherwise
296
+ # when we're debundled but setuptools isn't, Python will see
297
+ # packaging.version.Version and
298
+ # pkg_resources._vendor.packaging.version.Version as different
299
+ # types. This way we'll use a str as a common data interchange
300
+ # format. If we stop using the pkg_resources provided specifier
301
+ # and start using our own, we can drop the cast to str().
302
+ [str (c .version ) for c in self ._candidates ],
303
+ prereleases = self ._prereleases ,
304
+ ))
305
+ # Again, converting to str to deal with debundling.
306
+ return (c for c in self ._candidates if str (c .version ) in versions )
307
+
308
+ def get_best (self ):
309
+ # type: () -> Optional[InstallationCandidate]
310
+ """Return the best candidate available, or None if no applicable
311
+ candidates are found.
312
+ """
313
+ candidates = list (self .iter_applicable ())
314
+ if not candidates :
315
+ return None
316
+ return max (candidates , key = self ._sort_key )
317
+
318
+
257
319
class PackageFinder (object ):
258
320
"""This finds packages.
259
321
@@ -628,6 +690,25 @@ def find_all_candidates(self, project_name):
628
690
# This is an intentional priority ordering
629
691
return file_versions + find_links_versions + page_versions
630
692
693
+ def find_candidates (
694
+ self ,
695
+ project_name , # type: str
696
+ specifier = specifiers .SpecifierSet (), # type: specifiers.BaseSpecifier
697
+ ):
698
+ """Find matches for the given project and specifier.
699
+
700
+ If given, `specifier` should implement `filter` to allow version
701
+ filtering (e.g. ``packaging.specifiers.SpecifierSet``).
702
+
703
+ Returns a `FoundCandidates` instance.
704
+ """
705
+ return FoundCandidates (
706
+ self .find_all_candidates (project_name ),
707
+ specifier = specifier ,
708
+ prereleases = (self .allow_all_prereleases or None ),
709
+ sort_key = self ._candidate_sort_key ,
710
+ )
711
+
631
712
def find_requirement (self , req , upgrade ):
632
713
# type: (InstallRequirement, bool) -> Optional[Link]
633
714
"""Try to find a Link matching req
@@ -636,52 +717,28 @@ def find_requirement(self, req, upgrade):
636
717
Returns a Link if found,
637
718
Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
638
719
"""
639
- all_candidates = self .find_all_candidates (req .name )
640
-
641
- # Filter out anything which doesn't match our specifier
642
- compatible_versions = set (
643
- req .specifier .filter (
644
- # We turn the version object into a str here because otherwise
645
- # when we're debundled but setuptools isn't, Python will see
646
- # packaging.version.Version and
647
- # pkg_resources._vendor.packaging.version.Version as different
648
- # types. This way we'll use a str as a common data interchange
649
- # format. If we stop using the pkg_resources provided specifier
650
- # and start using our own, we can drop the cast to str().
651
- [str (c .version ) for c in all_candidates ],
652
- prereleases = (
653
- self .allow_all_prereleases
654
- if self .allow_all_prereleases else None
655
- ),
656
- )
657
- )
658
- applicable_candidates = [
659
- # Again, converting to str to deal with debundling.
660
- c for c in all_candidates if str (c .version ) in compatible_versions
661
- ]
662
-
663
- if applicable_candidates :
664
- best_candidate = max (applicable_candidates ,
665
- key = self ._candidate_sort_key )
666
- else :
667
- best_candidate = None
720
+ candidates = self .find_candidates (req .name , req .specifier )
721
+ best_candidate = candidates .get_best ()
668
722
723
+ installed_version = None # type: Optional[_BaseVersion]
669
724
if req .satisfied_by is not None :
670
725
installed_version = parse_version (req .satisfied_by .version )
671
- else :
672
- installed_version = None
726
+
727
+ def _format_versions (cand_iter ):
728
+ # This repeated parse_version and str() conversion is needed to
729
+ # handle different vendoring sources from pip and pkg_resources.
730
+ # If we stop using the pkg_resources provided specifier.
731
+ return ", " .join (sorted (
732
+ {str (c .version ) for c in cand_iter },
733
+ key = parse_version ,
734
+ )) or "none"
673
735
674
736
if installed_version is None and best_candidate is None :
675
737
logger .critical (
676
738
'Could not find a version that satisfies the requirement %s '
677
739
'(from versions: %s)' ,
678
740
req ,
679
- ', ' .join (
680
- sorted (
681
- {str (c .version ) for c in all_candidates },
682
- key = parse_version ,
683
- )
684
- )
741
+ _format_versions (candidates .iter_all ()),
685
742
)
686
743
687
744
raise DistributionNotFound (
@@ -716,15 +773,14 @@ def find_requirement(self, req, upgrade):
716
773
'Installed version (%s) is most up-to-date (past versions: '
717
774
'%s)' ,
718
775
installed_version ,
719
- ', ' .join (sorted (compatible_versions , key = parse_version )) or
720
- "none" ,
776
+ _format_versions (candidates .iter_applicable ()),
721
777
)
722
778
raise BestVersionAlreadyInstalled
723
779
724
780
logger .debug (
725
781
'Using version %s (newest of versions: %s)' ,
726
782
best_candidate .version ,
727
- ', ' . join ( sorted ( compatible_versions , key = parse_version ))
783
+ _format_versions ( candidates . iter_applicable ()),
728
784
)
729
785
return best_candidate .location
730
786
0 commit comments