py_spec.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """A Python specification is an abstract requirement definition of an interpreter."""
  2. from __future__ import annotations
  3. import contextlib
  4. import os
  5. import re
  6. from virtualenv.util.specifier import SimpleSpecifierSet, SimpleVersion
  7. PATTERN = re.compile(r"^(?P<impl>[a-zA-Z]+)?(?P<version>[0-9.]+)?(?P<threaded>t)?(?:-(?P<arch>32|64))?$")
  8. SPECIFIER_PATTERN = re.compile(r"^(?:(?P<impl>[A-Za-z]+)\s*)?(?P<spec>(?:===|==|~=|!=|<=|>=|<|>).+)$")
  9. class PythonSpec:
  10. """Contains specification about a Python Interpreter."""
  11. def __init__( # noqa: PLR0913
  12. self,
  13. str_spec: str,
  14. implementation: str | None,
  15. major: int | None,
  16. minor: int | None,
  17. micro: int | None,
  18. architecture: int | None,
  19. path: str | None,
  20. *,
  21. free_threaded: bool | None = None,
  22. version_specifier: SpecifierSet | None = None,
  23. ) -> None:
  24. self.str_spec = str_spec
  25. self.implementation = implementation
  26. self.major = major
  27. self.minor = minor
  28. self.micro = micro
  29. self.free_threaded = free_threaded
  30. self.architecture = architecture
  31. self.path = path
  32. self.version_specifier = version_specifier
  33. @classmethod
  34. def from_string_spec(cls, string_spec: str): # noqa: C901, PLR0912
  35. impl, major, minor, micro, threaded, arch, path = None, None, None, None, None, None, None
  36. version_specifier = None
  37. if os.path.isabs(string_spec): # noqa: PLR1702
  38. path = string_spec
  39. else:
  40. ok = False
  41. match = re.match(PATTERN, string_spec)
  42. if match:
  43. def _int_or_none(val):
  44. return None if val is None else int(val)
  45. try:
  46. groups = match.groupdict()
  47. version = groups["version"]
  48. if version is not None:
  49. versions = tuple(int(i) for i in version.split(".") if i)
  50. if len(versions) > 3: # noqa: PLR2004
  51. raise ValueError # noqa: TRY301
  52. if len(versions) == 3: # noqa: PLR2004
  53. major, minor, micro = versions
  54. elif len(versions) == 2: # noqa: PLR2004
  55. major, minor = versions
  56. elif len(versions) == 1:
  57. version_data = versions[0]
  58. major = int(str(version_data)[0]) # first digit major
  59. if version_data > 9: # noqa: PLR2004
  60. minor = int(str(version_data)[1:])
  61. threaded = bool(groups["threaded"])
  62. ok = True
  63. except ValueError:
  64. pass
  65. else:
  66. impl = groups["impl"]
  67. if impl in {"py", "python"}:
  68. impl = None
  69. arch = _int_or_none(groups["arch"])
  70. if not ok:
  71. specifier_match = SPECIFIER_PATTERN.match(string_spec.strip())
  72. if specifier_match and SpecifierSet is not None:
  73. impl = specifier_match.group("impl")
  74. spec_text = specifier_match.group("spec").strip()
  75. try:
  76. version_specifier = SpecifierSet(spec_text)
  77. except InvalidSpecifier:
  78. pass
  79. else:
  80. if impl in {"py", "python"}:
  81. impl = None
  82. return cls(
  83. string_spec,
  84. impl,
  85. None,
  86. None,
  87. None,
  88. None,
  89. None,
  90. free_threaded=None,
  91. version_specifier=version_specifier,
  92. )
  93. path = string_spec
  94. return cls(
  95. string_spec,
  96. impl,
  97. major,
  98. minor,
  99. micro,
  100. arch,
  101. path,
  102. free_threaded=threaded,
  103. version_specifier=version_specifier,
  104. )
  105. def generate_re(self, *, windows: bool) -> re.Pattern:
  106. """Generate a regular expression for matching against a filename."""
  107. version = r"{}(\.{}(\.{})?)?".format(
  108. *(r"\d+" if v is None else v for v in (self.major, self.minor, self.micro))
  109. )
  110. impl = "python" if self.implementation is None else f"python|{re.escape(self.implementation)}"
  111. mod = "t?" if self.free_threaded else ""
  112. suffix = r"\.exe" if windows else ""
  113. version_conditional = (
  114. "?"
  115. # Windows Python executables are almost always unversioned
  116. if windows
  117. # Spec is an empty string
  118. or self.major is None
  119. else ""
  120. )
  121. # Try matching `direct` first, so the `direct` group is filled when possible.
  122. return re.compile(
  123. rf"(?P<impl>{impl})(?P<v>{version}{mod}){version_conditional}{suffix}$",
  124. flags=re.IGNORECASE,
  125. )
  126. @property
  127. def is_abs(self):
  128. return self.path is not None and os.path.isabs(self.path)
  129. def _check_version_specifier(self, spec):
  130. """Check if version specifier is satisfied."""
  131. components: list[int] = []
  132. for part in (self.major, self.minor, self.micro):
  133. if part is None:
  134. break
  135. components.append(part)
  136. if not components:
  137. return True
  138. version_str = ".".join(str(part) for part in components)
  139. with contextlib.suppress(InvalidVersion):
  140. Version(version_str)
  141. for item in spec.version_specifier:
  142. # Check precision requirements
  143. required_precision = self._get_required_precision(item)
  144. if required_precision is None or len(components) < required_precision:
  145. continue
  146. if not item.contains(version_str):
  147. return False
  148. return True
  149. @staticmethod
  150. def _get_required_precision(item):
  151. """Get the required precision for a specifier item."""
  152. with contextlib.suppress(AttributeError, ValueError):
  153. return len(item.version.release)
  154. return None
  155. def satisfies(self, spec): # noqa: PLR0911
  156. """Called when there's a candidate metadata spec to see if compatible - e.g. PEP-514 on Windows."""
  157. if spec.is_abs and self.is_abs and self.path != spec.path:
  158. return False
  159. if spec.implementation is not None and spec.implementation.lower() != self.implementation.lower():
  160. return False
  161. if spec.architecture is not None and spec.architecture != self.architecture:
  162. return False
  163. if spec.free_threaded is not None and spec.free_threaded != self.free_threaded:
  164. return False
  165. if spec.version_specifier is not None and not self._check_version_specifier(spec):
  166. return False
  167. for our, req in zip((self.major, self.minor, self.micro), (spec.major, spec.minor, spec.micro)):
  168. if req is not None and our is not None and our != req:
  169. return False
  170. return True
  171. def __repr__(self) -> str:
  172. name = type(self).__name__
  173. params = (
  174. "implementation",
  175. "major",
  176. "minor",
  177. "micro",
  178. "architecture",
  179. "path",
  180. "free_threaded",
  181. "version_specifier",
  182. )
  183. return f"{name}({', '.join(f'{k}={getattr(self, k)}' for k in params if getattr(self, k) is not None)})"
  184. # Create aliases for backward compatibility
  185. SpecifierSet = SimpleSpecifierSet
  186. Version = SimpleVersion
  187. InvalidSpecifier = ValueError
  188. InvalidVersion = ValueError
  189. __all__ = [
  190. "InvalidSpecifier",
  191. "InvalidVersion",
  192. "PythonSpec",
  193. "SpecifierSet",
  194. "Version",
  195. ]