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