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)
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 )