File: version.py

package info (click to toggle)
python-tatsu 5.17.1%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,516 kB
  • sloc: python: 13,185; makefile: 127
file content (119 lines) | stat: -rw-r--r-- 3,117 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# Copyright (c) 2017-2026 Juancarlo AƱez (apalala@gmail.com)
# SPDX-License-Identifier: BSD-4-Clause

# NOTE
#   PEP 440: Version Identification and Dependency Specification
#   https://peps.python.org/pep-0440/
#   https://github.com/pypa/packaging

from __future__ import annotations

import re
from collections import namedtuple
from dataclasses import asdict, dataclass
from itertools import takewhile
from typing import Any

from .abctools import rowselect

__all__ = ['Version']


STRIC_VERSION_RE = r'''(?x)
    ^v?
    (?P<epoch>\d+!)?
    (?P<release>\d+(\.\d+)*)
    (?P<pre>[-._]?(a|b|rc)\d*)?
    (?P<post>[-._]?post\d*)?
    (?P<dev>[-._]?dev\d*)?
    (?P<local>\+.*)?
    $
'''

VERSION_RE = r'''(?x)
    ^[vV]?
    (?:(?P<epoch>\d+)!)?
    (?P<release>\d+(\.\d+)*)
    (?:[-._]?(?P<pre>(?!post|dev)(\w+)\d+))?
    (?:[-._]?(?P<post>post\d+))?
    (?:[-._]?(?P<dev>dev\d+))?
    (?:\+(?P<local>[\w\.]+))?
    $
'''

LETTER_NORMALIZATION = {
    'alpha': 'a',
    'beta': 'b',
    'c': 'rc',
    'pre': 'rc',
    'preview': 'rc',
    'rev': 'post',
    'r': 'post',
}


@dataclass(slots=True, kw_only=True)
class 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 | None = None
    post: Any = None
    dev: Any = None
    local: Any = None

    def __str__(self):
        return str(self.astuple())

    def astuple(self):
        notnone = {
            name: value for name, value in asdict(self).items() if value is not None
        }
        return namedtuple('version_info', notnone.keys())(*notnone.values())

    @staticmethod
    def parse(versionstr: str) -> Version:
        match = re.match(VERSION_RE, versionstr)
        if not match:
            raise ValueError(f'Invalid version string: {versionstr!r}')

        def alphadigit_split(s: str) -> tuple[str, int | str]:
            if not s:
                return None, None  # type: ignore

            alpha = ''.join(takewhile(str.isalpha, s))
            digits = s[len(alpha) :]
            if digits.isdigit():
                digits = int(digits)
            return alpha, digits

        parts = match.groupdict()
        release = tuple(int(d) for d in parts['release'].split('.'))
        parts['release'] = release

        pre = parts['pre'] or ''
        pre, num = alphadigit_split(pre.lstrip('_-.'))
        pre = LETTER_NORMALIZATION.get(pre, pre)
        pre = (pre, num)
        parts['pre'] = pre
        level, serial = pre
        serial = int(serial) if serial else None

        major, minor, micro, *nano = release + (None,) * 3
        nano = tuple(int(n) for n in nano if n is not None) or None

        for key in ('epoch', 'post', 'dev', 'local'):
            parts[key] = alphadigit_split(parts[key])[1]

        return Version(
            major=major,
            minor=minor,
            micro=micro,
            nano=nano,
            level=level,
            serial=serial,
            **rowselect({'epoch', 'post', 'dev', 'local'}, parts),
        )