specifier.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """Version specifier support using only standard library (PEP 440 compatible)."""
  2. from __future__ import annotations
  3. import contextlib
  4. import operator
  5. import re
  6. class SimpleVersion:
  7. """Simple PEP 440-like version parser using only standard library."""
  8. def __init__(self, version_str: str) -> None:
  9. self.version_str = version_str
  10. # Parse version string into components
  11. # Support formats like: "3.11", "3.11.0", "3.11.0a1", "3.11.0b2", "3.11.0rc1"
  12. match = re.match(
  13. r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:(a|b|rc)(\d+))?$",
  14. version_str.strip(),
  15. )
  16. if not match:
  17. msg = f"Invalid version: {version_str}"
  18. raise ValueError(msg)
  19. self.major = int(match.group(1))
  20. self.minor = int(match.group(2)) if match.group(2) else 0
  21. self.micro = int(match.group(3)) if match.group(3) else 0
  22. self.pre_type = match.group(4) # a, b, rc or None
  23. self.pre_num = int(match.group(5)) if match.group(5) else None
  24. self.release = (self.major, self.minor, self.micro)
  25. def __eq__(self, other):
  26. if not isinstance(other, SimpleVersion):
  27. return NotImplemented
  28. return self.release == other.release and self.pre_type == other.pre_type and self.pre_num == other.pre_num
  29. def __hash__(self):
  30. return hash((self.release, self.pre_type, self.pre_num))
  31. def __lt__(self, other):
  32. if not isinstance(other, SimpleVersion):
  33. return NotImplemented
  34. # Compare release tuples first
  35. if self.release != other.release:
  36. return self.release < other.release
  37. return self._compare_prerelease(other)
  38. def _compare_prerelease(self, other):
  39. """Compare pre-release versions."""
  40. # If releases are equal, compare pre-release
  41. # No pre-release is greater than any pre-release
  42. if self.pre_type is None and other.pre_type is None:
  43. return False
  44. if self.pre_type is None:
  45. return False # self is final, other is pre-release
  46. if other.pre_type is None:
  47. return True # self is pre-release, other is final
  48. # Both are pre-releases, compare type then number
  49. pre_order = {"a": 1, "b": 2, "rc": 3}
  50. if pre_order[self.pre_type] != pre_order[other.pre_type]:
  51. return pre_order[self.pre_type] < pre_order[other.pre_type]
  52. return (self.pre_num or 0) < (other.pre_num or 0)
  53. def __le__(self, other):
  54. return self == other or self < other
  55. def __gt__(self, other):
  56. if not isinstance(other, SimpleVersion):
  57. return NotImplemented
  58. return not self <= other
  59. def __ge__(self, other):
  60. return not self < other
  61. def __str__(self):
  62. return self.version_str
  63. def __repr__(self):
  64. return f"SimpleVersion('{self.version_str}')"
  65. class SimpleSpecifier:
  66. """Simple PEP 440-like version specifier using only standard library."""
  67. __slots__ = (
  68. "is_wildcard",
  69. "operator",
  70. "spec_str",
  71. "version",
  72. "version_str",
  73. "wildcard_precision",
  74. "wildcard_version",
  75. )
  76. def __init__(self, spec_str: str) -> None:
  77. self.spec_str = spec_str.strip()
  78. # Parse operator and version
  79. match = re.match(r"^(===|==|~=|!=|<=|>=|<|>)\s*(.+)$", self.spec_str)
  80. if not match:
  81. msg = f"Invalid specifier: {spec_str}"
  82. raise ValueError(msg)
  83. self.operator = match.group(1)
  84. self.version_str = match.group(2).strip()
  85. # Handle wildcard versions like "3.11.*"
  86. if self.version_str.endswith(".*"):
  87. self.is_wildcard = True
  88. self.wildcard_version = self.version_str[:-2]
  89. # Count the precision for wildcard matching
  90. self.wildcard_precision = len(self.wildcard_version.split("."))
  91. self.version_str = self.wildcard_version
  92. else:
  93. self.is_wildcard = False
  94. self.wildcard_precision = None
  95. try:
  96. self.version = SimpleVersion(self.version_str)
  97. except ValueError:
  98. # If version parsing fails, store as string for prefix matching
  99. self.version = None
  100. def contains(self, version_str: str) -> bool:
  101. """Check if a version string satisfies this specifier."""
  102. try:
  103. candidate = SimpleVersion(version_str) if isinstance(version_str, str) else version_str
  104. except ValueError:
  105. return False
  106. if self.version is None:
  107. return False
  108. if self.is_wildcard:
  109. return self._check_wildcard(candidate)
  110. return self._check_standard(candidate)
  111. def _check_wildcard(self, candidate):
  112. """Check wildcard version matching."""
  113. if self.operator == "==":
  114. return candidate.release[: self.wildcard_precision] == self.version.release[: self.wildcard_precision]
  115. if self.operator == "!=":
  116. return candidate.release[: self.wildcard_precision] != self.version.release[: self.wildcard_precision]
  117. # Other operators with wildcards are not standard
  118. return False
  119. def _check_standard(self, candidate):
  120. """Check standard version comparisons."""
  121. if self.operator == "===":
  122. return str(candidate) == str(self.version)
  123. if self.operator == "~=":
  124. return self._check_compatible_release(candidate)
  125. # Use operator module for comparisons
  126. cmp_ops = {
  127. "==": operator.eq,
  128. "!=": operator.ne,
  129. "<": operator.lt,
  130. "<=": operator.le,
  131. ">": operator.gt,
  132. ">=": operator.ge,
  133. }
  134. if self.operator in cmp_ops:
  135. return cmp_ops[self.operator](candidate, self.version)
  136. return False
  137. def _check_compatible_release(self, candidate):
  138. """Check compatible release version (~=)."""
  139. if candidate < self.version:
  140. return False
  141. if len(self.version.release) >= 2: # noqa: PLR2004
  142. upper_parts = list(self.version.release[:-1])
  143. upper_parts[-1] += 1
  144. upper = SimpleVersion(".".join(str(p) for p in upper_parts))
  145. return candidate < upper
  146. return True
  147. def __eq__(self, other):
  148. if not isinstance(other, SimpleSpecifier):
  149. return NotImplemented
  150. return self.spec_str == other.spec_str
  151. def __hash__(self):
  152. return hash(self.spec_str)
  153. def __str__(self):
  154. return self.spec_str
  155. def __repr__(self):
  156. return f"SimpleSpecifier('{self.spec_str}')"
  157. class SimpleSpecifierSet:
  158. """Simple PEP 440-like specifier set using only standard library."""
  159. __slots__ = ("specifiers", "specifiers_str")
  160. def __init__(self, specifiers_str: str = "") -> None:
  161. self.specifiers_str = specifiers_str.strip()
  162. self.specifiers = []
  163. if self.specifiers_str:
  164. # Split by comma for compound specifiers
  165. for spec_item in self.specifiers_str.split(","):
  166. stripped = spec_item.strip()
  167. if stripped:
  168. with contextlib.suppress(ValueError):
  169. self.specifiers.append(SimpleSpecifier(stripped))
  170. def contains(self, version_str: str) -> bool:
  171. """Check if a version satisfies all specifiers in the set."""
  172. if not self.specifiers:
  173. return True
  174. # All specifiers must be satisfied
  175. return all(spec.contains(version_str) for spec in self.specifiers)
  176. def __iter__(self):
  177. return iter(self.specifiers)
  178. def __eq__(self, other):
  179. if not isinstance(other, SimpleSpecifierSet):
  180. return NotImplemented
  181. return self.specifiers_str == other.specifiers_str
  182. def __hash__(self):
  183. return hash(self.specifiers_str)
  184. def __str__(self):
  185. return self.specifiers_str
  186. def __repr__(self):
  187. return f"SimpleSpecifierSet('{self.specifiers_str}')"
  188. __all__ = [
  189. "SimpleSpecifier",
  190. "SimpleSpecifierSet",
  191. "SimpleVersion",
  192. ]