requirements.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. from __future__ import annotations
  2. from typing import Any
  3. from pip._vendor.packaging.specifiers import SpecifierSet
  4. from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
  5. from pip._internal.req.constructors import install_req_drop_extras
  6. from pip._internal.req.req_install import InstallRequirement
  7. from .base import Candidate, CandidateLookup, Requirement, format_name
  8. class ExplicitRequirement(Requirement):
  9. def __init__(self, candidate: Candidate) -> None:
  10. self.candidate = candidate
  11. def __str__(self) -> str:
  12. return str(self.candidate)
  13. def __repr__(self) -> str:
  14. return f"{self.__class__.__name__}({self.candidate!r})"
  15. def __hash__(self) -> int:
  16. return hash(self.candidate)
  17. def __eq__(self, other: Any) -> bool:
  18. if not isinstance(other, ExplicitRequirement):
  19. return False
  20. return self.candidate == other.candidate
  21. @property
  22. def project_name(self) -> NormalizedName:
  23. # No need to canonicalize - the candidate did this
  24. return self.candidate.project_name
  25. @property
  26. def name(self) -> str:
  27. # No need to canonicalize - the candidate did this
  28. return self.candidate.name
  29. def format_for_error(self) -> str:
  30. return self.candidate.format_for_error()
  31. def get_candidate_lookup(self) -> CandidateLookup:
  32. return self.candidate, None
  33. def is_satisfied_by(self, candidate: Candidate) -> bool:
  34. return candidate == self.candidate
  35. class SpecifierRequirement(Requirement):
  36. def __init__(self, ireq: InstallRequirement) -> None:
  37. assert ireq.link is None, "This is a link, not a specifier"
  38. self._ireq = ireq
  39. self._equal_cache: str | None = None
  40. self._hash: int | None = None
  41. self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
  42. @property
  43. def _equal(self) -> str:
  44. if self._equal_cache is not None:
  45. return self._equal_cache
  46. self._equal_cache = str(self._ireq)
  47. return self._equal_cache
  48. def __str__(self) -> str:
  49. return str(self._ireq.req)
  50. def __repr__(self) -> str:
  51. return f"{self.__class__.__name__}({str(self._ireq.req)!r})"
  52. def __eq__(self, other: object) -> bool:
  53. if not isinstance(other, SpecifierRequirement):
  54. return NotImplemented
  55. return self._equal == other._equal
  56. def __hash__(self) -> int:
  57. if self._hash is not None:
  58. return self._hash
  59. self._hash = hash(self._equal)
  60. return self._hash
  61. @property
  62. def project_name(self) -> NormalizedName:
  63. assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
  64. return canonicalize_name(self._ireq.req.name)
  65. @property
  66. def name(self) -> str:
  67. return format_name(self.project_name, self._extras)
  68. def format_for_error(self) -> str:
  69. # Convert comma-separated specifiers into "A, B, ..., F and G"
  70. # This makes the specifier a bit more "human readable", without
  71. # risking a change in meaning. (Hopefully! Not all edge cases have
  72. # been checked)
  73. parts = [s.strip() for s in str(self).split(",")]
  74. if len(parts) == 0:
  75. return ""
  76. elif len(parts) == 1:
  77. return parts[0]
  78. return ", ".join(parts[:-1]) + " and " + parts[-1]
  79. def get_candidate_lookup(self) -> CandidateLookup:
  80. return None, self._ireq
  81. def is_satisfied_by(self, candidate: Candidate) -> bool:
  82. assert candidate.name == self.name, (
  83. f"Internal issue: Candidate is not for this requirement "
  84. f"{candidate.name} vs {self.name}"
  85. )
  86. # We can safely always allow prereleases here since PackageFinder
  87. # already implements the prerelease logic, and would have filtered out
  88. # prerelease candidates if the user does not expect them.
  89. assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
  90. spec = self._ireq.req.specifier
  91. return spec.contains(candidate.version, prereleases=True)
  92. class SpecifierWithoutExtrasRequirement(SpecifierRequirement):
  93. """
  94. Requirement backed by an install requirement on a base package.
  95. Trims extras from its install requirement if there are any.
  96. """
  97. def __init__(self, ireq: InstallRequirement) -> None:
  98. assert ireq.link is None, "This is a link, not a specifier"
  99. self._ireq = install_req_drop_extras(ireq)
  100. self._equal_cache: str | None = None
  101. self._hash: int | None = None
  102. self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
  103. @property
  104. def _equal(self) -> str:
  105. if self._equal_cache is not None:
  106. return self._equal_cache
  107. self._equal_cache = str(self._ireq)
  108. return self._equal_cache
  109. def __eq__(self, other: object) -> bool:
  110. if not isinstance(other, SpecifierWithoutExtrasRequirement):
  111. return NotImplemented
  112. return self._equal == other._equal
  113. def __hash__(self) -> int:
  114. if self._hash is not None:
  115. return self._hash
  116. self._hash = hash(self._equal)
  117. return self._hash
  118. class RequiresPythonRequirement(Requirement):
  119. """A requirement representing Requires-Python metadata."""
  120. def __init__(self, specifier: SpecifierSet, match: Candidate) -> None:
  121. self.specifier = specifier
  122. self._specifier_string = str(specifier) # for faster __eq__
  123. self._hash: int | None = None
  124. self._candidate = match
  125. def __str__(self) -> str:
  126. return f"Python {self.specifier}"
  127. def __repr__(self) -> str:
  128. return f"{self.__class__.__name__}({str(self.specifier)!r})"
  129. def __hash__(self) -> int:
  130. if self._hash is not None:
  131. return self._hash
  132. self._hash = hash((self._specifier_string, self._candidate))
  133. return self._hash
  134. def __eq__(self, other: Any) -> bool:
  135. if not isinstance(other, RequiresPythonRequirement):
  136. return False
  137. return (
  138. self._specifier_string == other._specifier_string
  139. and self._candidate == other._candidate
  140. )
  141. @property
  142. def project_name(self) -> NormalizedName:
  143. return self._candidate.project_name
  144. @property
  145. def name(self) -> str:
  146. return self._candidate.name
  147. def format_for_error(self) -> str:
  148. return str(self)
  149. def get_candidate_lookup(self) -> CandidateLookup:
  150. if self.specifier.contains(self._candidate.version, prereleases=True):
  151. return self._candidate, None
  152. return None, None
  153. def is_satisfied_by(self, candidate: Candidate) -> bool:
  154. assert candidate.name == self._candidate.name, "Not Python candidate"
  155. # We can safely always allow prereleases here since PackageFinder
  156. # already implements the prerelease logic, and would have filtered out
  157. # prerelease candidates if the user does not expect them.
  158. return self.specifier.contains(candidate.version, prereleases=True)
  159. class UnsatisfiableRequirement(Requirement):
  160. """A requirement that cannot be satisfied."""
  161. def __init__(self, name: NormalizedName) -> None:
  162. self._name = name
  163. def __str__(self) -> str:
  164. return f"{self._name} (unavailable)"
  165. def __repr__(self) -> str:
  166. return f"{self.__class__.__name__}({str(self._name)!r})"
  167. def __eq__(self, other: object) -> bool:
  168. if not isinstance(other, UnsatisfiableRequirement):
  169. return NotImplemented
  170. return self._name == other._name
  171. def __hash__(self) -> int:
  172. return hash(self._name)
  173. @property
  174. def project_name(self) -> NormalizedName:
  175. return self._name
  176. @property
  177. def name(self) -> str:
  178. return self._name
  179. def format_for_error(self) -> str:
  180. return str(self)
  181. def get_candidate_lookup(self) -> CandidateLookup:
  182. return None, None
  183. def is_satisfied_by(self, candidate: Candidate) -> bool:
  184. return False