requirements.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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. # Pre-compute candidate lookup to avoid repeated specifier checks
  126. if specifier.contains(match.version, prereleases=True):
  127. self._candidate_lookup: CandidateLookup = (match, None)
  128. else:
  129. self._candidate_lookup = (None, None)
  130. def __str__(self) -> str:
  131. return f"Python {self.specifier}"
  132. def __repr__(self) -> str:
  133. return f"{self.__class__.__name__}({str(self.specifier)!r})"
  134. def __hash__(self) -> int:
  135. if self._hash is not None:
  136. return self._hash
  137. self._hash = hash((self._specifier_string, self._candidate))
  138. return self._hash
  139. def __eq__(self, other: Any) -> bool:
  140. if not isinstance(other, RequiresPythonRequirement):
  141. return False
  142. return (
  143. self._specifier_string == other._specifier_string
  144. and self._candidate == other._candidate
  145. )
  146. @property
  147. def project_name(self) -> NormalizedName:
  148. return self._candidate.project_name
  149. @property
  150. def name(self) -> str:
  151. return self._candidate.name
  152. def format_for_error(self) -> str:
  153. return str(self)
  154. def get_candidate_lookup(self) -> CandidateLookup:
  155. return self._candidate_lookup
  156. def is_satisfied_by(self, candidate: Candidate) -> bool:
  157. assert candidate.name == self._candidate.name, "Not Python candidate"
  158. # We can safely always allow prereleases here since PackageFinder
  159. # already implements the prerelease logic, and would have filtered out
  160. # prerelease candidates if the user does not expect them.
  161. return self.specifier.contains(candidate.version, prereleases=True)
  162. class UnsatisfiableRequirement(Requirement):
  163. """A requirement that cannot be satisfied."""
  164. def __init__(self, name: NormalizedName) -> None:
  165. self._name = name
  166. def __str__(self) -> str:
  167. return f"{self._name} (unavailable)"
  168. def __repr__(self) -> str:
  169. return f"{self.__class__.__name__}({str(self._name)!r})"
  170. def __eq__(self, other: object) -> bool:
  171. if not isinstance(other, UnsatisfiableRequirement):
  172. return NotImplemented
  173. return self._name == other._name
  174. def __hash__(self) -> int:
  175. return hash(self._name)
  176. @property
  177. def project_name(self) -> NormalizedName:
  178. return self._name
  179. @property
  180. def name(self) -> str:
  181. return self._name
  182. def format_for_error(self) -> str:
  183. return str(self)
  184. def get_candidate_lookup(self) -> CandidateLookup:
  185. return None, None
  186. def is_satisfied_by(self, candidate: Candidate) -> bool:
  187. return False