|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from dataclasses import dataclass |
| 4 | +from typing import List, Tuple |
| 5 | + |
| 6 | + |
| 7 | +@dataclass(frozen=True, order=True) |
| 8 | +class Version: |
| 9 | + major: int |
| 10 | + minor: int |
| 11 | + patch: int |
| 12 | + |
| 13 | + @staticmethod |
| 14 | + def parse(version: str) -> "Version": |
| 15 | + """Parse a semver-like string 'MAJOR.MINOR.PATCH' ignoring pre-release/build. |
| 16 | +
|
| 17 | + Non-numeric or missing parts default to 0; extra parts are ignored. |
| 18 | + Examples: '1.16.0', '1.16', '1' -> (1,16,0)/(1,0,0). |
| 19 | + """ |
| 20 | + core = version.split("-")[0].split("+")[0] |
| 21 | + parts = core.split(".") |
| 22 | + nums: List[int] = [] |
| 23 | + for i in range(3): |
| 24 | + try: |
| 25 | + nums.append(int(parts[i])) |
| 26 | + except Exception: |
| 27 | + nums.append(0) |
| 28 | + return Version(nums[0], nums[1], nums[2]) |
| 29 | + |
| 30 | + |
| 31 | +def _parse_constraint_token(token: str) -> Tuple[str, Version]: |
| 32 | + token = token.strip() |
| 33 | + ops = ["<=", ">=", "==", "!=", "<", ">"] |
| 34 | + for op in ops: |
| 35 | + if token.startswith(op): |
| 36 | + return op, Version.parse(token[len(op) :].strip()) |
| 37 | + # default to == if no operator present |
| 38 | + return "==", Version.parse(token) |
| 39 | + |
| 40 | + |
| 41 | +def _satisfies(version: Version, op: str, bound: Version) -> bool: |
| 42 | + if op == "==": |
| 43 | + return version == bound |
| 44 | + if op == "!=": |
| 45 | + return version != bound |
| 46 | + if op == ">": |
| 47 | + return version > bound |
| 48 | + if op == ">=": |
| 49 | + return version >= bound |
| 50 | + if op == "<": |
| 51 | + return version < bound |
| 52 | + if op == "<=": |
| 53 | + return version <= bound |
| 54 | + raise ValueError(f"Unknown operator: {op}") |
| 55 | + |
| 56 | + |
| 57 | +def is_version_supported(version: str, constraints: str) -> bool: |
| 58 | + """Return True if the given version satisfies the constraints. |
| 59 | +
|
| 60 | + Constraints syntax: |
| 61 | + - Comma-separated items are ANDed: ">=1.16.0, <2.0.0" |
| 62 | + - Use '||' for OR groups: ">=1.16.0, <2.0.0 || ==0.0.0" |
| 63 | + - Each token supports operators: ==, !=, >=, <=, >, < |
| 64 | + - Missing operator defaults to == |
| 65 | + """ |
| 66 | + v = Version.parse(version) |
| 67 | + for group in constraints.split("||"): |
| 68 | + group = group.strip() |
| 69 | + if not group: |
| 70 | + continue |
| 71 | + tokens = [t for t in (tok.strip() for tok in group.split(",")) if t] |
| 72 | + if not tokens: |
| 73 | + continue |
| 74 | + if all(_satisfies(v, *_parse_constraint_token(tok)) for tok in tokens): |
| 75 | + return True |
| 76 | + return False |
0 commit comments