blob: 97e405f66c64f432c34edc2deabdc4b4460669c1 [file] [log] [blame]
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import functools
from typing import Any, Final, Iterable
class VersionParseError(ValueError):
def __init__(self, name: str, msg: str, version_str: str) -> None:
self._version_str = version_str
super().__init__(f"Invalid {name} {repr(version_str)}: {msg}")
@property
def version_str(self) -> str:
return self._version_str
@functools.total_ordering
class Version:
_MAX_PART_VALUE: Final[int] = 0xFFFF
@classmethod
def parse_error(cls, msg: str, version: str) -> VersionParseError:
return VersionParseError(cls.__name__, msg, version)
def __init__(self, parts: Iterable[int], version_str: str = "") -> None:
self._parts: Final[tuple[int, ...]] = self._validate_parts(
parts, version_str or repr(parts))
self._version_str: Final[str] = version_str
@classmethod
def _validate_parts(cls, parts: Iterable[int], value: str) -> tuple[int, ...]:
if parts is None:
raise cls.parse_error("Invalid version format", value)
parts_tuple = tuple(parts)
for part in parts_tuple:
if part < 0:
raise cls.parse_error("Version parts must be positive", value)
return parts_tuple
@property
def parts(self) -> tuple[int, ...]:
return self._parts
@property
def version_str(self) -> str:
return self._version_str
@property
def parts_str(self) -> str:
return ".".join(map(str, self._parts))
def comparable_parts(self, padded_len: int) -> tuple[int, ...]:
if self.is_complete:
return self._parts
padding = (self._MAX_PART_VALUE,) * (padded_len - len(self._parts))
return self._parts + padding
@property
def is_complete(self) -> bool:
return True
def is_compatible_type(self, other: object) -> bool:
if isinstance(other, type(self)):
return True
other_type = type(other)
return issubclass(other_type, Version) and isinstance(self, other_type)
def __hash__(self) -> int:
return hash(self.key)
def __eq__(self, other: object) -> bool:
if not self.is_compatible_type(other):
return False
assert isinstance(other, Version)
return self.key == other.key
def __le__(self, other: Any) -> bool:
if not self.is_compatible_type(other):
raise TypeError("Cannot compare unrelated versions : "
f"{type(self).__name__} vs. "
f"{type(other).__name__}.")
return self.key <= other.key
@property
def key(self) -> tuple[tuple[int, ...], Any]:
return (self._parts, None)