tsemekwes.version

  1# Copyright (c) 2017-2026 Juancarlo AƱez (apalala@gmail.com)
  2# SPDX-License-Identifier: BSD-4-Clause
  3
  4# NOTE
  5#   PEP 440: Version Identification and Dependency Specification
  6#   https://peps.python.org/pep-0440/
  7#   https://github.com/pypa/packaging
  8
  9from __future__ import annotations
 10
 11import re
 12from collections import namedtuple
 13from dataclasses import asdict, dataclass
 14from itertools import takewhile
 15from typing import Any
 16
 17from .abctools import rowselect
 18
 19__all__ = ["Version"]
 20
 21
 22STRIC_VERSION_RE = r"""(?x)
 23    ^v?
 24    (?P<epoch>\d+!)?
 25    (?P<release>\d+(\.\d+)*)
 26    (?P<pre>[-._]?(a|b|rc)\d*)?
 27    (?P<post>[-._]?post\d*)?
 28    (?P<dev>[-._]?dev\d*)?
 29    (?P<local>\+.*)?
 30    $
 31"""
 32
 33VERSION_RE = r"""(?x)
 34    ^[vV]?
 35    (?:(?P<epoch>\d+)!)?
 36    (?P<release>\d+(\.\d+)*)
 37    (?:[-._]?(?P<pre>(?!post|dev)(\w+)\d+))?
 38    (?:[-._]?(?P<post>post\d+))?
 39    (?:[-._]?(?P<dev>dev\d+))?
 40    (?:\+(?P<local>[\w\.]+))?
 41    $
 42"""
 43
 44LETTER_NORMALIZATION = {
 45    "alpha": "a",
 46    "beta": "b",
 47    "c": "rc",
 48    "pre": "rc",
 49    "preview": "rc",
 50    "rev": "post",
 51    "r": "post",
 52}
 53
 54
 55@dataclass(slots=True, kw_only=True)
 56class Version:
 57    epoch: Any = None
 58    major: int | None = None
 59    minor: int | None = None
 60    micro: int | None = None
 61    nano: tuple[int, ...] | None = None
 62    level: str | None = None
 63    serial: int | str | None = None
 64    post: Any = None
 65    dev: Any = None
 66    local: Any = None
 67
 68    def __str__(self):
 69        return str(self.astuple())
 70
 71    def astuple(self):
 72        notnone = {
 73            name: value for name, value in asdict(self).items() if value is not None
 74        }
 75        return namedtuple("version_info", notnone.keys())(*notnone.values())  # type: ignore
 76
 77    @staticmethod
 78    def parse(versionstr: str) -> Version:
 79        match = re.match(VERSION_RE, versionstr)
 80        if not match:
 81            raise ValueError(f"Invalid version string: {versionstr!r}")
 82
 83        def alphadigit_split(s: str) -> tuple[str, int | str]:
 84            if not s:
 85                return None, None  # type: ignore
 86
 87            alpha = "".join(takewhile(str.isalpha, s))
 88            digits = s[len(alpha) :]
 89            if digits.isdigit():
 90                digits = int(digits)  # type: ignore
 91            return alpha, digits
 92
 93        parts = match.groupdict()
 94        release = tuple(int(d) for d in parts["release"].split("."))
 95        parts["release"] = release
 96
 97        pre = parts["pre"] or ""
 98        pre, num = alphadigit_split(pre.lstrip("_-."))
 99        pre = LETTER_NORMALIZATION.get(pre, pre)
100        pre = (pre, num)
101        parts["pre"] = pre
102        level, serial = pre
103        serial = int(serial) if serial else None  # type: ignore
104
105        major, minor, micro, *nano = release + (None,) * 3
106        nano = tuple(int(n) for n in nano if n is not None) or None  # type: ignore
107
108        for key in ("epoch", "post", "dev", "local"):
109            parts[key] = alphadigit_split(parts[key])[1]
110
111        return Version(
112            major=major,
113            minor=minor,
114            micro=micro,
115            nano=nano,  # type: ignore
116            level=level,
117            serial=serial,
118            **rowselect({"epoch", "post", "dev", "local"}, parts),
119        )
@dataclass(slots=True, kw_only=True)
class Version:
 56@dataclass(slots=True, kw_only=True)
 57class Version:
 58    epoch: Any = None
 59    major: int | None = None
 60    minor: int | None = None
 61    micro: int | None = None
 62    nano: tuple[int, ...] | None = None
 63    level: str | None = None
 64    serial: int | str | None = None
 65    post: Any = None
 66    dev: Any = None
 67    local: Any = None
 68
 69    def __str__(self):
 70        return str(self.astuple())
 71
 72    def astuple(self):
 73        notnone = {
 74            name: value for name, value in asdict(self).items() if value is not None
 75        }
 76        return namedtuple("version_info", notnone.keys())(*notnone.values())  # type: ignore
 77
 78    @staticmethod
 79    def parse(versionstr: str) -> Version:
 80        match = re.match(VERSION_RE, versionstr)
 81        if not match:
 82            raise ValueError(f"Invalid version string: {versionstr!r}")
 83
 84        def alphadigit_split(s: str) -> tuple[str, int | str]:
 85            if not s:
 86                return None, None  # type: ignore
 87
 88            alpha = "".join(takewhile(str.isalpha, s))
 89            digits = s[len(alpha) :]
 90            if digits.isdigit():
 91                digits = int(digits)  # type: ignore
 92            return alpha, digits
 93
 94        parts = match.groupdict()
 95        release = tuple(int(d) for d in parts["release"].split("."))
 96        parts["release"] = release
 97
 98        pre = parts["pre"] or ""
 99        pre, num = alphadigit_split(pre.lstrip("_-."))
100        pre = LETTER_NORMALIZATION.get(pre, pre)
101        pre = (pre, num)
102        parts["pre"] = pre
103        level, serial = pre
104        serial = int(serial) if serial else None  # type: ignore
105
106        major, minor, micro, *nano = release + (None,) * 3
107        nano = tuple(int(n) for n in nano if n is not None) or None  # type: ignore
108
109        for key in ("epoch", "post", "dev", "local"):
110            parts[key] = alphadigit_split(parts[key])[1]
111
112        return Version(
113            major=major,
114            minor=minor,
115            micro=micro,
116            nano=nano,  # type: ignore
117            level=level,
118            serial=serial,
119            **rowselect({"epoch", "post", "dev", "local"}, parts),
120        )
Version( *, epoch: Any = None, major: int | None = None, minor: int | None = None, micro: int | None = None, nano: tuple[int, ...] | None = None, level: str | None = None, serial: int | str | None = None, post: Any = None, dev: Any = None, local: Any = None)
epoch: Any
major: int | None
minor: int | None
micro: int | None
nano: tuple[int, ...] | None
level: str | None
serial: int | str | None
post: Any
dev: Any
local: Any
def astuple(self):
72    def astuple(self):
73        notnone = {
74            name: value for name, value in asdict(self).items() if value is not None
75        }
76        return namedtuple("version_info", notnone.keys())(*notnone.values())  # type: ignore
@staticmethod
def parse(versionstr: str) -> Version:
 78    @staticmethod
 79    def parse(versionstr: str) -> Version:
 80        match = re.match(VERSION_RE, versionstr)
 81        if not match:
 82            raise ValueError(f"Invalid version string: {versionstr!r}")
 83
 84        def alphadigit_split(s: str) -> tuple[str, int | str]:
 85            if not s:
 86                return None, None  # type: ignore
 87
 88            alpha = "".join(takewhile(str.isalpha, s))
 89            digits = s[len(alpha) :]
 90            if digits.isdigit():
 91                digits = int(digits)  # type: ignore
 92            return alpha, digits
 93
 94        parts = match.groupdict()
 95        release = tuple(int(d) for d in parts["release"].split("."))
 96        parts["release"] = release
 97
 98        pre = parts["pre"] or ""
 99        pre, num = alphadigit_split(pre.lstrip("_-."))
100        pre = LETTER_NORMALIZATION.get(pre, pre)
101        pre = (pre, num)
102        parts["pre"] = pre
103        level, serial = pre
104        serial = int(serial) if serial else None  # type: ignore
105
106        major, minor, micro, *nano = release + (None,) * 3
107        nano = tuple(int(n) for n in nano if n is not None) or None  # type: ignore
108
109        for key in ("epoch", "post", "dev", "local"):
110            parts[key] = alphadigit_split(parts[key])[1]
111
112        return Version(
113            major=major,
114            minor=minor,
115            micro=micro,
116            nano=nano,  # type: ignore
117            level=level,
118            serial=serial,
119            **rowselect({"epoch", "post", "dev", "local"}, parts),
120        )