save progress

This commit is contained in:
StellaOps Bot
2025-12-28 01:40:35 +02:00
parent 3bfbbae115
commit cec4265a40
694 changed files with 88052 additions and 24718 deletions

View File

@@ -0,0 +1,237 @@
"""
Version parsing and comparison utilities for NuGet packages.
Handles SemVer versions with prerelease suffixes.
"""
import re
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class ParsedVersion:
"""Parsed semantic version."""
major: int
minor: int
patch: int
prerelease: Optional[str] = None
build_metadata: Optional[str] = None
original: str = ""
def is_stable(self) -> bool:
"""Returns True if this is a stable (non-prerelease) version."""
return self.prerelease is None
def __lt__(self, other: "ParsedVersion") -> bool:
"""Compare versions following SemVer rules."""
# Compare major.minor.patch first
self_tuple = (self.major, self.minor, self.patch)
other_tuple = (other.major, other.minor, other.patch)
if self_tuple != other_tuple:
return self_tuple < other_tuple
# If equal, prerelease versions are less than stable
if self.prerelease is None and other.prerelease is None:
return False
if self.prerelease is None:
return False # stable > prerelease
if other.prerelease is None:
return True # prerelease < stable
# Both have prerelease - compare alphanumerically
return self._compare_prerelease(self.prerelease, other.prerelease) < 0
def __le__(self, other: "ParsedVersion") -> bool:
return self == other or self < other
def __gt__(self, other: "ParsedVersion") -> bool:
return other < self
def __ge__(self, other: "ParsedVersion") -> bool:
return self == other or self > other
@staticmethod
def _compare_prerelease(a: str, b: str) -> int:
"""Compare prerelease strings according to SemVer."""
a_parts = a.split(".")
b_parts = b.split(".")
for i in range(max(len(a_parts), len(b_parts))):
if i >= len(a_parts):
return -1
if i >= len(b_parts):
return 1
a_part = a_parts[i]
b_part = b_parts[i]
# Try numeric comparison first
a_is_num = a_part.isdigit()
b_is_num = b_part.isdigit()
if a_is_num and b_is_num:
diff = int(a_part) - int(b_part)
if diff != 0:
return diff
elif a_is_num:
return -1 # Numeric < string
elif b_is_num:
return 1 # String > numeric
else:
# Both strings - compare lexically
if a_part < b_part:
return -1
if a_part > b_part:
return 1
return 0
# Regex for parsing NuGet versions
# Matches: 1.2.3, 1.2.3-beta, 1.2.3-beta.1, 1.2.3-rc.1+build, [1.2.3]
VERSION_PATTERN = re.compile(
r"^\[?" # Optional opening bracket
r"(\d+)" # Major (required)
r"(?:\.(\d+))?" # Minor (optional)
r"(?:\.(\d+))?" # Patch (optional)
r"(?:-([a-zA-Z0-9][a-zA-Z0-9.-]*))?" # Prerelease (optional)
r"(?:\+([a-zA-Z0-9][a-zA-Z0-9.-]*))?" # Build metadata (optional)
r"\]?$" # Optional closing bracket
)
# Pattern for wildcard versions (e.g., 1.0.*)
WILDCARD_PATTERN = re.compile(r"\*")
def parse_version(version_str: str) -> Optional[ParsedVersion]:
"""
Parse a NuGet version string.
Args:
version_str: Version string like "1.2.3", "1.2.3-beta.1", "[1.2.3]"
Returns:
ParsedVersion if valid, None if invalid or wildcard
"""
if not version_str:
return None
version_str = version_str.strip()
# Skip wildcard versions
if WILDCARD_PATTERN.search(version_str):
return None
match = VERSION_PATTERN.match(version_str)
if not match:
return None
major = int(match.group(1))
minor = int(match.group(2)) if match.group(2) else 0
patch = int(match.group(3)) if match.group(3) else 0
prerelease = match.group(4)
build_metadata = match.group(5)
return ParsedVersion(
major=major,
minor=minor,
patch=patch,
prerelease=prerelease,
build_metadata=build_metadata,
original=version_str,
)
def is_stable(version_str: str) -> bool:
"""
Check if a version string represents a stable release.
Args:
version_str: Version string to check
Returns:
True if stable (no prerelease suffix), False otherwise
"""
parsed = parse_version(version_str)
if parsed is None:
return False
return parsed.is_stable()
def compare_versions(v1: str, v2: str) -> int:
"""
Compare two version strings.
Args:
v1: First version string
v2: Second version string
Returns:
-1 if v1 < v2, 0 if equal, 1 if v1 > v2
Returns 0 if either version is unparseable
"""
parsed_v1 = parse_version(v1)
parsed_v2 = parse_version(v2)
if parsed_v1 is None or parsed_v2 is None:
return 0
if parsed_v1 < parsed_v2:
return -1
if parsed_v1 > parsed_v2:
return 1
return 0
def select_latest_stable(versions: list[str]) -> Optional[str]:
"""
Select the latest stable version from a list.
Args:
versions: List of version strings
Returns:
Latest stable version string, or None if no stable versions exist
"""
stable_versions: list[tuple[ParsedVersion, str]] = []
for v in versions:
parsed = parse_version(v)
if parsed is not None and parsed.is_stable():
stable_versions.append((parsed, v))
if not stable_versions:
return None
# Sort by parsed version and return the original string of the max
stable_versions.sort(key=lambda x: x[0], reverse=True)
return stable_versions[0][1]
def normalize_version_string(version_str: str) -> str:
"""
Normalize a version string to a canonical form.
Strips brackets, whitespace, and normalizes format.
Args:
version_str: Version string to normalize
Returns:
Normalized version string
"""
parsed = parse_version(version_str)
if parsed is None:
return version_str.strip()
# Rebuild canonical form
result = f"{parsed.major}.{parsed.minor}.{parsed.patch}"
if parsed.prerelease:
result += f"-{parsed.prerelease}"
if parsed.build_metadata:
result += f"+{parsed.build_metadata}"
return result