Let us walk on the 3-isogeny graph
Loading...
Searching...
No Matches
candidates.py
Go to the documentation of this file.
1import logging
2import sys
3from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
4
5from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
6from pip._vendor.packaging.version import Version
7
8from pip._internal.exceptions import (
9 HashError,
10 InstallationSubprocessError,
11 MetadataInconsistent,
12)
13from pip._internal.metadata import BaseDistribution
14from pip._internal.models.link import Link, links_equivalent
15from pip._internal.models.wheel import Wheel
17 install_req_from_editable,
18 install_req_from_line,
19)
20from pip._internal.req.req_install import InstallRequirement
21from pip._internal.utils.direct_url_helpers import direct_url_from_link
22from pip._internal.utils.misc import normalize_version_info
23
24from .base import Candidate, CandidateVersion, Requirement, format_name
25
26if TYPE_CHECKING:
27 from .factory import Factory
28
29logger = logging.getLogger(__name__)
30
31BaseCandidate = Union[
32 "AlreadyInstalledCandidate",
33 "EditableCandidate",
34 "LinkCandidate",
35]
36
37# Avoid conflicting with the PyPI package "Python".
38REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, "<Python from Requires-Python>")
39
40
41def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]:
42 """The runtime version of BaseCandidate."""
43 base_candidate_classes = (
44 AlreadyInstalledCandidate,
45 EditableCandidate,
46 LinkCandidate,
47 )
48 if isinstance(candidate, base_candidate_classes):
49 return candidate
50 return None
51
52
54 link: Link, template: InstallRequirement
55) -> InstallRequirement:
56 assert not template.editable, "template is editable"
57 if template.req:
58 line = str(template.req)
59 else:
60 line = link.url
61 ireq = install_req_from_line(
62 line,
63 user_supplied=template.user_supplied,
64 comes_from=template.comes_from,
65 use_pep517=template.use_pep517,
66 isolated=template.isolated,
67 constraint=template.constraint,
68 global_options=template.global_options,
69 hash_options=template.hash_options,
70 config_settings=template.config_settings,
71 )
73 ireq.link = link
75 return ireq
76
77
79 link: Link, template: InstallRequirement
80) -> InstallRequirement:
81 assert template.editable, "template not editable"
82 ireq = install_req_from_editable(
84 user_supplied=template.user_supplied,
85 comes_from=template.comes_from,
86 use_pep517=template.use_pep517,
87 isolated=template.isolated,
88 constraint=template.constraint,
89 permit_editable_wheels=template.permit_editable_wheels,
90 global_options=template.global_options,
91 hash_options=template.hash_options,
92 config_settings=template.config_settings,
93 )
95 return ireq
96
97
99 dist: BaseDistribution, template: InstallRequirement
100) -> InstallRequirement:
101 if template.req:
102 line = str(template.req)
103 elif template.link:
104 line = f"{dist.canonical_name} @ {template.link.url}"
105 else:
106 line = f"{dist.canonical_name}=={dist.version}"
107 ireq = install_req_from_line(
108 line,
109 user_supplied=template.user_supplied,
110 comes_from=template.comes_from,
111 use_pep517=template.use_pep517,
112 isolated=template.isolated,
113 constraint=template.constraint,
114 global_options=template.global_options,
115 hash_options=template.hash_options,
116 config_settings=template.config_settings,
117 )
118 ireq.satisfied_by = dist
119 return ireq
120
121
123 """A candidate backed by an ``InstallRequirement``.
124
125 This represents a package request with the target not being already
126 in the environment, and needs to be fetched and installed. The backing
127 ``InstallRequirement`` is responsible for most of the leg work; this
128 class exposes appropriate information to the resolver.
129
130 :param link: The link passed to the ``InstallRequirement``. The backing
131 ``InstallRequirement`` will use this link to fetch the distribution.
132 :param source_link: The link this candidate "originates" from. This is
133 different from ``link`` when the link is found in the wheel cache.
134 ``link`` would point to the wheel cache, while this points to the
135 found remote link (e.g. from pypi.org).
136 """
137
138 dist: BaseDistribution
139 is_installed = False
140
142 self,
143 link: Link,
144 source_link: Link,
145 ireq: InstallRequirement,
146 factory: "Factory",
147 name: Optional[NormalizedName] = None,
148 version: Optional[CandidateVersion] = None,
149 ) -> None:
150 self._link = link
151 self._source_link = source_link
152 self._factory = factory
153 self._ireq = ireq
154 self._name = name
155 self._version = version
156 self.distdist = self._prepare()
157
158 def __str__(self) -> str:
159 return f"{self.name} {self.version}"
160
161 def __repr__(self) -> str:
162 return "{class_name}({link!r})".format(
163 class_name=self.__class__.__name__,
164 link=str(self._link),
165 )
166
167 def __hash__(self) -> int:
168 return hash((self.__class__, self._link))
169
170 def __eq__(self, other: Any) -> bool:
171 if isinstance(other, self.__class__):
172 return links_equivalent(self._link, other._link)
173 return False
174
175 @property
176 def source_link(self) -> Optional[Link]:
177 return self._source_link
178
179 @property
180 def project_name(self) -> NormalizedName:
181 """The normalised name of the project the candidate refers to"""
182 if self._name is None:
183 self._name = self.distdist.canonical_name
184 return self._name
185
186 @property
187 def name(self) -> str:
188 return self.project_nameproject_name
189
190 @property
191 def version(self) -> CandidateVersion:
192 if self._version is None:
193 self._version = self.distdist.version
194 return self._version
195
196 def format_for_error(self) -> str:
197 return "{} {} (from {})".format(
200 self._link.file_path if self._link.is_file else self._link,
201 )
202
203 def _prepare_distribution(self) -> BaseDistribution:
204 raise NotImplementedError("Override in subclass")
205
206 def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
207 """Check for consistency of project name and version of dist."""
208 if self._name is not None and self._name != dist.canonical_name:
210 self._ireq,
211 "name",
212 self._name,
214 )
215 if self._version is not None and self._version != dist.version:
217 self._ireq,
218 "version",
219 str(self._version),
220 str(dist.version),
221 )
222
223 def _prepare(self) -> BaseDistribution:
224 try:
225 dist = self._prepare_distribution()
226 except HashError as e:
227 # Provide HashError the underlying ireq that caused it. This
228 # provides context for the resulting error message to show the
229 # offending line to the user.
230 e.req = self._ireq
231 raise
232 except InstallationSubprocessError as exc:
233 # The output has been presented already, so don't duplicate it.
234 exc.context = "See above for output."
235 raise
236
238 return dist
239
240 def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
241 requires = self.distdist.iter_dependencies() if with_requires else ()
242 for r in requires:
243 yield self._factory.make_requirement_from_spec(str(r), self._ireq)
244 yield self._factory.make_requires_python_requirement(self.distdist.requires_python)
245
246 def get_install_requirement(self) -> Optional[InstallRequirement]:
247 return self._ireq
248
249
251 is_editable = False
252
254 self,
255 link: Link,
256 template: InstallRequirement,
257 factory: "Factory",
258 name: Optional[NormalizedName] = None,
259 version: Optional[CandidateVersion] = None,
260 ) -> None:
261 source_link = link
262 cache_entry = factory.get_wheel_cache_entry(source_link, name)
263 if cache_entry is not None:
264 logger.debug("Using cached wheel link: %s", cache_entry.link)
265 link = cache_entry.link
266 ireq = make_install_req_from_link(link, template)
267 assert ireq.link == link
270 wheel_name = canonicalize_name(wheel.name)
271 assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel"
272 # Version may not be present for PEP 508 direct URLs
273 if version is not None:
274 wheel_version = Version(wheel.version)
275 assert version == wheel_version, "{!r} != {!r} for wheel {}".format(
276 version, wheel_version, name
277 )
278
279 if cache_entry is not None:
280 assert ireq.link.is_wheel
281 assert ireq.link.is_file
284 if cache_entry.origin is not None:
286 else:
287 # Legacy cache entry that does not have origin.json.
288 # download_info may miss the archive_info.hashes field.
289 ireq.download_info = direct_url_from_link(
290 source_link, link_is_in_wheel_cache=cache_entry.persistent
291 )
292
293 super().__init__(
294 link=link,
295 source_link=source_link,
296 ireq=ireq,
297 factory=factory,
298 name=name,
299 version=version,
300 )
301
302 def _prepare_distribution(self) -> BaseDistribution:
303 preparer = self._factory.preparer
304 return preparer.prepare_linked_requirement(self._ireq_ireq, parallel_builds=True)
305
306
308 is_editable = True
309
311 self,
312 link: Link,
313 template: InstallRequirement,
314 factory: "Factory",
315 name: Optional[NormalizedName] = None,
316 version: Optional[CandidateVersion] = None,
317 ) -> None:
318 super().__init__(
319 link=link,
320 source_link=link,
321 ireq=make_install_req_from_editable(link, template),
322 factory=factory,
323 name=name,
324 version=version,
325 )
326
327 def _prepare_distribution(self) -> BaseDistribution:
329
330
332 is_installed = True
333 source_link = None
334
336 self,
337 dist: BaseDistribution,
338 template: InstallRequirement,
339 factory: "Factory",
340 ) -> None:
341 self.dist = dist
342 self._ireq = _make_install_req_from_dist(dist, template)
343 self._factory = factory
344 self._version = None
345
346 # This is just logging some messages, so we can do it eagerly.
347 # The returned dist would be exactly the same as self.dist because we
348 # set satisfied_by in _make_install_req_from_dist.
349 # TODO: Supply reason based on force_reinstall and upgrade_strategy.
350 skip_reason = "already satisfied"
352
353 def __str__(self) -> str:
354 return str(self.dist)
355
356 def __repr__(self) -> str:
357 return "{class_name}({distribution!r})".format(
358 class_name=self.__class__.__name__,
359 distribution=self.dist,
360 )
361
362 def __hash__(self) -> int:
363 return hash((self.__class__, self.namenamename, self.versionversionversion))
364
365 def __eq__(self, other: Any) -> bool:
366 if isinstance(other, self.__class__):
368 return False
369
370 @property
371 def project_name(self) -> NormalizedName:
372 return self.dist.canonical_name
373
374 @property
375 def name(self) -> str:
376 return self.project_nameproject_name
377
378 @property
379 def version(self) -> CandidateVersion:
380 if self._version is None:
381 self._version = self.dist.version
382 return self._version
383
384 @property
385 def is_editable(self) -> bool:
386 return self.dist.editable
387
388 def format_for_error(self) -> str:
389 return f"{self.name} {self.version} (Installed)"
390
391 def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
392 if not with_requires:
393 return
394 for r in self.dist.iter_dependencies():
395 yield self._factory.make_requirement_from_spec(str(r), self._ireq)
396
397 def get_install_requirement(self) -> Optional[InstallRequirement]:
398 return None
399
400
402 """A candidate that has 'extras', indicating additional dependencies.
403
404 Requirements can be for a project with dependencies, something like
405 foo[extra]. The extras don't affect the project/version being installed
406 directly, but indicate that we need additional dependencies. We model that
407 by having an artificial ExtrasCandidate that wraps the "base" candidate.
408
409 The ExtrasCandidate differs from the base in the following ways:
410
411 1. It has a unique name, of the form foo[extra]. This causes the resolver
412 to treat it as a separate node in the dependency graph.
413 2. When we're getting the candidate's dependencies,
414 a) We specify that we want the extra dependencies as well.
415 b) We add a dependency on the base candidate.
416 See below for why this is needed.
417 3. We return None for the underlying InstallRequirement, as the base
418 candidate will provide it, and we don't want to end up with duplicates.
419
420 The dependency on the base candidate is needed so that the resolver can't
421 decide that it should recommend foo[extra1] version 1.0 and foo[extra2]
422 version 2.0. Having those candidates depend on foo=1.0 and foo=2.0
423 respectively forces the resolver to recognise that this is a conflict.
424 """
425
427 self,
428 base: BaseCandidate,
429 extras: FrozenSet[str],
430 ) -> None:
431 self.base = base
432 self.extras = extras
433
434 def __str__(self) -> str:
435 name, rest = str(self.base).split(" ", 1)
436 return "{}[{}] {}".format(name, ",".join(self.extras), rest)
437
438 def __repr__(self) -> str:
439 return "{class_name}(base={base!r}, extras={extras!r})".format(
440 class_name=self.__class__.__name__,
441 base=self.base,
442 extras=self.extras,
443 )
444
445 def __hash__(self) -> int:
446 return hash((self.base, self.extras))
447
448 def __eq__(self, other: Any) -> bool:
449 if isinstance(other, self.__class__):
450 return self.base == other.base and self.extras == other.extras
451 return False
452
453 @property
454 def project_name(self) -> NormalizedName:
455 return self.base.project_name
456
457 @property
458 def name(self) -> str:
459 """The normalised name of the project the candidate refers to"""
460 return format_name(self.base.project_name, self.extras)
461
462 @property
463 def version(self) -> CandidateVersion:
464 return self.base.version
465
466 def format_for_error(self) -> str:
467 return "{} [{}]".format(
468 self.base.format_for_error(), ", ".join(sorted(self.extras))
469 )
470
471 @property
472 def is_installed(self) -> bool:
473 return self.base.is_installed
474
475 @property
476 def is_editable(self) -> bool:
477 return self.base.is_editable
478
479 @property
480 def source_link(self) -> Optional[Link]:
481 return self.base.source_link
482
483 def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
484 factory = self.base._factory
485
486 # Add a dependency on the exact base
487 # (See note 2b in the class docstring)
489 if not with_requires:
490 return
491
492 # The user may have specified extras that the candidate doesn't
493 # support. We ignore any unsupported extras here.
494 valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
495 invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
496 for extra in sorted(invalid_extras):
498 "%s %s does not provide the extra '%s'",
499 self.base.name,
501 extra,
502 )
503
504 for r in self.base.dist.iter_dependencies(valid_extras):
506 str(r), self.base._ireq, valid_extras
507 )
508 if requirement:
509 yield requirement
510
511 def get_install_requirement(self) -> Optional[InstallRequirement]:
512 # We don't return anything here, because we always
513 # depend on the base candidate, and we'll get the
514 # install requirement from that.
515 return None
516
517
519 is_installed = False
520 source_link = None
521
522 def __init__(self, py_version_info: Optional[Tuple[int, ...]]) -> None:
523 if py_version_info is not None:
524 version_info = normalize_version_info(py_version_info)
525 else:
526 version_info = sys.version_info[:3]
527 self._version = Version(".".join(str(c) for c in version_info))
528
529 # We don't need to implement __eq__() and __ne__() since there is always
530 # only one RequiresPythonCandidate in a resolution, i.e. the host Python.
531 # The built-in object.__eq__() and object.__ne__() do exactly what we want.
532
533 def __str__(self) -> str:
534 return f"Python {self._version}"
535
536 @property
537 def project_name(self) -> NormalizedName:
538 return REQUIRES_PYTHON_IDENTIFIER
539
540 @property
541 def name(self) -> str:
542 return REQUIRES_PYTHON_IDENTIFIER
543
544 @property
545 def version(self) -> CandidateVersion:
546 return self._version
547
548 def format_for_error(self) -> str:
549 return f"Python {self.version}"
550
551 def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
552 return ()
553
554 def get_install_requirement(self) -> Optional[InstallRequirement]:
555 return None
None __init__(self, BaseDistribution dist, InstallRequirement template, "Factory" factory)
Iterable[Optional[Requirement]] iter_dependencies(self, bool with_requires)
None __init__(self, Link link, InstallRequirement template, "Factory" factory, Optional[NormalizedName] name=None, Optional[CandidateVersion] version=None)
None __init__(self, BaseCandidate base, FrozenSet[str] extras)
Optional[InstallRequirement] get_install_requirement(self)
Iterable[Optional[Requirement]] iter_dependencies(self, bool with_requires)
None __init__(self, Link link, InstallRequirement template, "Factory" factory, Optional[NormalizedName] name=None, Optional[CandidateVersion] version=None)
Iterable[Optional[Requirement]] iter_dependencies(self, bool with_requires)
None __init__(self, Optional[Tuple[int,...]] py_version_info)
Iterable[Optional[Requirement]] iter_dependencies(self, bool with_requires)
None __init__(self, Link link, Link source_link, InstallRequirement ireq, "Factory" factory, Optional[NormalizedName] name=None, Optional[CandidateVersion] version=None)
InstallRequirement make_install_req_from_editable(Link link, InstallRequirement template)
Definition candidates.py:80
InstallRequirement _make_install_req_from_dist(BaseDistribution dist, InstallRequirement template)
InstallRequirement make_install_req_from_link(Link link, InstallRequirement template)
Definition candidates.py:55
for i