1"""Meta related things."""
2from __future__ import annotations
3from collections import namedtuple
4import re
5
6RE_VER = re.compile(
7 r'''(?x)
8 (?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<micro>\d+))?
9 (?:(?P<type>a|b|rc)(?P<pre>\d+))?
10 (?:\.post(?P<post>\d+))?
11 (?:\.dev(?P<dev>\d+))?
12 '''
13)
14
15REL_MAP = {
16 ".dev": "",
17 ".dev-alpha": "a",
18 ".dev-beta": "b",
19 ".dev-candidate": "rc",
20 "alpha": "a",
21 "beta": "b",
22 "candidate": "rc",
23 "final": ""
24}
25
26DEV_STATUS = {
27 ".dev": "2 - Pre-Alpha",
28 ".dev-alpha": "2 - Pre-Alpha",
29 ".dev-beta": "2 - Pre-Alpha",
30 ".dev-candidate": "2 - Pre-Alpha",
31 "alpha": "3 - Alpha",
32 "beta": "4 - Beta",
33 "candidate": "4 - Beta",
34 "final": "5 - Production/Stable"
35}
36
37PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}
38
39
40class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
41 """
42 Get the version (PEP 440).
43
44 A biased approach to the PEP 440 semantic version.
45
46 Provides a tuple structure which is sorted for comparisons `v1 > v2` etc.
47 (major, minor, micro, release type, pre-release build, post-release build, development release build)
48 Release types are named in is such a way they are comparable with ease.
49 Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get
50 development status for setup files.
51
52 How it works (currently):
53
54 - You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`.
55 - To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`.
56 The dot is used to ensure all development specifiers are sorted before `alpha`.
57 You can specify a `dev` number for development builds, but do not have to as implicit development releases
58 are allowed.
59 - You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not
60 allow implicit prereleases.
61 - You can optionally set `post` to a value greater than zero to make the build a post release. While post releases
62 are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be
63 noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically
64 does not allow implicit post releases.
65 - It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`.
66
67 Acceptable version releases:
68
69 ```
70 Version(1, 0, 0, "final") 1.0
71 Version(1, 2, 0, "final") 1.2
72 Version(1, 2, 3, "final") 1.2.3
73 Version(1, 2, 0, ".dev-alpha", pre=4) 1.2a4
74 Version(1, 2, 0, ".dev-beta", pre=4) 1.2b4
75 Version(1, 2, 0, ".dev-candidate", pre=4) 1.2rc4
76 Version(1, 2, 0, "final", post=1) 1.2.post1
77 Version(1, 2, 3, ".dev") 1.2.3.dev0
78 Version(1, 2, 3, ".dev", dev=1) 1.2.3.dev1
79 ```
80
81 """
82
83 def __new__(
84 cls,
85 major: int, minor: int, micro: int, release: str = "final",
86 pre: int = 0, post: int = 0, dev: int = 0
87 ) -> Version:
88 """Validate version info."""
89
90 # Ensure all parts are positive integers.
91 for value in (major, minor, micro, pre, post):
92 if not (isinstance(value, int) and value >= 0):
93 raise ValueError("All version parts except 'release' should be integers.")
94
95 if release not in REL_MAP:
96 raise ValueError(f"'{release}' is not a valid release type.")
97
98 # Ensure valid pre-release (we do not allow implicit pre-releases).
99 if ".dev-candidate" < release < "final":
100 if pre == 0:
101 raise ValueError("Implicit pre-releases not allowed.")
102 elif dev:
103 raise ValueError("Version is not a development release.")
104 elif post:
105 raise ValueError("Post-releases are not allowed with pre-releases.")
106
107 # Ensure valid development or development/pre release
108 elif release < "alpha":
109 if release > ".dev" and pre == 0:
110 raise ValueError("Implicit pre-release not allowed.")
111 elif post:
112 raise ValueError("Post-releases are not allowed with pre-releases.")
113
114 # Ensure a valid normal release
115 else:
116 if pre:
117 raise ValueError("Version is not a pre-release.")
118 elif dev:
119 raise ValueError("Version is not a development release.")
120
121 return super().__new__(cls, major, minor, micro, release, pre, post, dev)
122
123 def _is_pre(self) -> bool:
124 """Is prerelease."""
125
126 return bool(self.pre > 0)
127
128 def _is_dev(self) -> bool:
129 """Is development."""
130
131 return bool(self.release < "alpha")
132
133 def _is_post(self) -> bool:
134 """Is post."""
135
136 return bool(self.post > 0)
137
138 def _get_dev_status(self) -> str: # pragma: no cover
139 """Get development status string."""
140
141 return DEV_STATUS[self.release]
142
143 def _get_canonical(self) -> str:
144 """Get the canonical output string."""
145
146 # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
147 if self.micro == 0:
148 ver = f"{self.major}.{self.minor}"
149 else:
150 ver = f"{self.major}.{self.minor}.{self.micro}"
151 if self._is_pre():
152 ver += f'{REL_MAP[self.release]}{self.pre}'
153 if self._is_post():
154 ver += f".post{self.post}"
155 if self._is_dev():
156 ver += f".dev{self.dev}"
157
158 return ver
159
160
161def parse_version(ver: str) -> Version:
162 """Parse version into a comparable Version tuple."""
163
164 m = RE_VER.match(ver)
165
166 if m is None:
167 raise ValueError(f"'{ver}' is not a valid version")
168
169 # Handle major, minor, micro
170 major = int(m.group('major'))
171 minor = int(m.group('minor')) if m.group('minor') else 0
172 micro = int(m.group('micro')) if m.group('micro') else 0
173
174 # Handle pre releases
175 if m.group('type'):
176 release = PRE_REL_MAP[m.group('type')]
177 pre = int(m.group('pre'))
178 else:
179 release = "final"
180 pre = 0
181
182 # Handle development releases
183 dev = m.group('dev') if m.group('dev') else 0
184 if m.group('dev'):
185 dev = int(m.group('dev'))
186 release = '.dev-' + release if pre else '.dev'
187 else:
188 dev = 0
189
190 # Handle post
191 post = int(m.group('post')) if m.group('post') else 0
192
193 return Version(major, minor, micro, release, pre, post, dev)
194
195
196__version_info__ = Version(2, 6, 0, "final")
197__version__ = __version_info__._get_canonical()