| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- """Version specifier support using only standard library (PEP 440 compatible)."""
- from __future__ import annotations
- import contextlib
- import operator
- import re
- class SimpleVersion:
- """Simple PEP 440-like version parser using only standard library."""
- def __init__(self, version_str: str) -> None:
- self.version_str = version_str
- # Parse version string into components
- # Support formats like: "3.11", "3.11.0", "3.11.0a1", "3.11.0b2", "3.11.0rc1"
- match = re.match(
- r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:(a|b|rc)(\d+))?$",
- version_str.strip(),
- )
- if not match:
- msg = f"Invalid version: {version_str}"
- raise ValueError(msg)
- self.major = int(match.group(1))
- self.minor = int(match.group(2)) if match.group(2) else 0
- self.micro = int(match.group(3)) if match.group(3) else 0
- self.pre_type = match.group(4) # a, b, rc or None
- self.pre_num = int(match.group(5)) if match.group(5) else None
- self.release = (self.major, self.minor, self.micro)
- def __eq__(self, other):
- if not isinstance(other, SimpleVersion):
- return NotImplemented
- return self.release == other.release and self.pre_type == other.pre_type and self.pre_num == other.pre_num
- def __hash__(self):
- return hash((self.release, self.pre_type, self.pre_num))
- def __lt__(self, other):
- if not isinstance(other, SimpleVersion):
- return NotImplemented
- # Compare release tuples first
- if self.release != other.release:
- return self.release < other.release
- return self._compare_prerelease(other)
- def _compare_prerelease(self, other):
- """Compare pre-release versions."""
- # If releases are equal, compare pre-release
- # No pre-release is greater than any pre-release
- if self.pre_type is None and other.pre_type is None:
- return False
- if self.pre_type is None:
- return False # self is final, other is pre-release
- if other.pre_type is None:
- return True # self is pre-release, other is final
- # Both are pre-releases, compare type then number
- pre_order = {"a": 1, "b": 2, "rc": 3}
- if pre_order[self.pre_type] != pre_order[other.pre_type]:
- return pre_order[self.pre_type] < pre_order[other.pre_type]
- return (self.pre_num or 0) < (other.pre_num or 0)
- def __le__(self, other):
- return self == other or self < other
- def __gt__(self, other):
- if not isinstance(other, SimpleVersion):
- return NotImplemented
- return not self <= other
- def __ge__(self, other):
- return not self < other
- def __str__(self):
- return self.version_str
- def __repr__(self):
- return f"SimpleVersion('{self.version_str}')"
- class SimpleSpecifier:
- """Simple PEP 440-like version specifier using only standard library."""
- __slots__ = (
- "is_wildcard",
- "operator",
- "spec_str",
- "version",
- "version_str",
- "wildcard_precision",
- "wildcard_version",
- )
- def __init__(self, spec_str: str) -> None:
- self.spec_str = spec_str.strip()
- # Parse operator and version
- match = re.match(r"^(===|==|~=|!=|<=|>=|<|>)\s*(.+)$", self.spec_str)
- if not match:
- msg = f"Invalid specifier: {spec_str}"
- raise ValueError(msg)
- self.operator = match.group(1)
- self.version_str = match.group(2).strip()
- # Handle wildcard versions like "3.11.*"
- if self.version_str.endswith(".*"):
- self.is_wildcard = True
- self.wildcard_version = self.version_str[:-2]
- # Count the precision for wildcard matching
- self.wildcard_precision = len(self.wildcard_version.split("."))
- self.version_str = self.wildcard_version
- else:
- self.is_wildcard = False
- self.wildcard_precision = None
- try:
- self.version = SimpleVersion(self.version_str)
- except ValueError:
- # If version parsing fails, store as string for prefix matching
- self.version = None
- def contains(self, version_str: str) -> bool:
- """Check if a version string satisfies this specifier."""
- try:
- candidate = SimpleVersion(version_str) if isinstance(version_str, str) else version_str
- except ValueError:
- return False
- if self.version is None:
- return False
- if self.is_wildcard:
- return self._check_wildcard(candidate)
- return self._check_standard(candidate)
- def _check_wildcard(self, candidate):
- """Check wildcard version matching."""
- if self.operator == "==":
- return candidate.release[: self.wildcard_precision] == self.version.release[: self.wildcard_precision]
- if self.operator == "!=":
- return candidate.release[: self.wildcard_precision] != self.version.release[: self.wildcard_precision]
- # Other operators with wildcards are not standard
- return False
- def _check_standard(self, candidate):
- """Check standard version comparisons."""
- if self.operator == "===":
- return str(candidate) == str(self.version)
- if self.operator == "~=":
- return self._check_compatible_release(candidate)
- # Use operator module for comparisons
- cmp_ops = {
- "==": operator.eq,
- "!=": operator.ne,
- "<": operator.lt,
- "<=": operator.le,
- ">": operator.gt,
- ">=": operator.ge,
- }
- if self.operator in cmp_ops:
- return cmp_ops[self.operator](candidate, self.version)
- return False
- def _check_compatible_release(self, candidate):
- """Check compatible release version (~=)."""
- if candidate < self.version:
- return False
- if len(self.version.release) >= 2: # noqa: PLR2004
- upper_parts = list(self.version.release[:-1])
- upper_parts[-1] += 1
- upper = SimpleVersion(".".join(str(p) for p in upper_parts))
- return candidate < upper
- return True
- def __eq__(self, other):
- if not isinstance(other, SimpleSpecifier):
- return NotImplemented
- return self.spec_str == other.spec_str
- def __hash__(self):
- return hash(self.spec_str)
- def __str__(self):
- return self.spec_str
- def __repr__(self):
- return f"SimpleSpecifier('{self.spec_str}')"
- class SimpleSpecifierSet:
- """Simple PEP 440-like specifier set using only standard library."""
- __slots__ = ("specifiers", "specifiers_str")
- def __init__(self, specifiers_str: str = "") -> None:
- self.specifiers_str = specifiers_str.strip()
- self.specifiers = []
- if self.specifiers_str:
- # Split by comma for compound specifiers
- for spec_item in self.specifiers_str.split(","):
- stripped = spec_item.strip()
- if stripped:
- with contextlib.suppress(ValueError):
- self.specifiers.append(SimpleSpecifier(stripped))
- def contains(self, version_str: str) -> bool:
- """Check if a version satisfies all specifiers in the set."""
- if not self.specifiers:
- return True
- # All specifiers must be satisfied
- return all(spec.contains(version_str) for spec in self.specifiers)
- def __iter__(self):
- return iter(self.specifiers)
- def __eq__(self, other):
- if not isinstance(other, SimpleSpecifierSet):
- return NotImplemented
- return self.specifiers_str == other.specifiers_str
- def __hash__(self):
- return hash(self.specifiers_str)
- def __str__(self):
- return self.specifiers_str
- def __repr__(self):
- return f"SimpleSpecifierSet('{self.specifiers_str}')"
- __all__ = [
- "SimpleSpecifier",
- "SimpleSpecifierSet",
- "SimpleVersion",
- ]
|