import dataclasses
import typing
from enum import Enum
from pathlib import Path
from typing import Self
[docs]
class VersionType(Enum):
"""SemVer part types and their index in the version tuple.
Attributes:
MAJOR (VersionType): Major version index 0.
MINOR (VersionType): Minor version index 1.
PATCH (VersionType): Patch version index 2.
"""
MAJOR = 0
MINOR = 1
PATCH = 2
[docs]
def __init__(self, part_index: int):
"""Store the index corresponding to this version part."""
self.part_index: int = part_index
[docs]
class VersionTag(Enum):
"""Optional tags appended to version strings.
Attributes:
TEST (VersionTag): "test" tag.
PROD (VersionTag): "prod" tag.
"""
TEST = "test"
PROD = "prod"
SemverParts = tuple[int, int, int]
[docs]
@dataclasses.dataclass(frozen=True)
class Version:
"""Semantic version representation with optional tag.
Attributes:
version_parts (tuple[int, int, int]): (major, minor, patch).
version_tag (VersionTag | None): Optional tag to append.
"""
VERSION_SPLIT = "."
VERSION_FILE_SPLIT = "_"
VERSION_FILE_PREFIX = "v"
VERSION_LIMIT = 100
VERSION_PARTS = 3
version_parts: SemverParts
version_tag: VersionTag | None = None
[docs]
@staticmethod
def from_string(version_string: str) -> "Version":
"""Parse a dotted version string into a Version, padding missing parts with zero."""
split = version_string.split(Version.VERSION_SPLIT)
split += [0] * (Version.VERSION_PARTS - len(split))
return Version(typing.cast(SemverParts, tuple(map(int, split))))
[docs]
def with_tag(self, version_tag: VersionTag) -> "Version":
"""Return a new Version with the given tag."""
return dataclasses.replace(self, version_tag=version_tag)
[docs]
def as_file_version(self) -> str:
"""Generate the file-friendly version string, e.g. '1_2_3-test'."""
version_string = self.VERSION_FILE_SPLIT.join(map(str, self.version_parts))
if self.version_tag is None:
return version_string
return f"{version_string}-{self.version_tag.value}"
[docs]
def is_to_high(self) -> bool:
"""Check if any version part exceeds the limit."""
return any(part >= self.VERSION_LIMIT for part in self.version_parts)
[docs]
def next_version(self, version_type: VersionType) -> "Version":
"""Increment the specified part and return a new Version."""
parts = list(self.version_parts)
parts[version_type.part_index] += 1
return Version(typing.cast(SemverParts, tuple(parts)))
def __str__(self) -> str:
"""Return the dotted version string, e.g. '1.2.3'."""
return self.VERSION_SPLIT.join(map(str, self.version_parts))