ps-version 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: ps-version
3
+ Version: 0.2.8
4
+ Summary:
5
+ Requires-Python: >=3.10,<3.14
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "ps-version"
3
+ description = ""
4
+ requires-python = ">=3.10,<3.14"
5
+ version = "0.2.8"
6
+
7
+ [tool.poetry]
8
+ packages = [ { include = "ps/version", from = "src" } ]
9
+
10
+ [tool.ps-plugin]
11
+ host-project = "../.."
12
+
13
+ [build-system]
14
+ requires = ["poetry-core>=1.0.0"]
15
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,20 @@
1
+ from ._version import Version, VersionFormatter
2
+ from ._version_constraint import VersionConstraint
3
+ from ._version_metadata import VersionMetadata
4
+ from ._version_prerelease import VersionPreRelease
5
+ from ._version_standard import VersionStandard
6
+ from .parsers import CalVerParser, LooseParser, NuGetParser, PEP440Parser, SemVerParser
7
+
8
+ __all__ = [
9
+ "Version",
10
+ "VersionFormatter",
11
+ "VersionConstraint",
12
+ "VersionMetadata",
13
+ "VersionPreRelease",
14
+ "VersionStandard",
15
+ "CalVerParser",
16
+ "LooseParser",
17
+ "NuGetParser",
18
+ "PEP440Parser",
19
+ "SemVerParser",
20
+ ]
@@ -0,0 +1,287 @@
1
+ # ruff: noqa: PLC0415
2
+ from dataclasses import dataclass
3
+ from functools import lru_cache, total_ordering
4
+ from typing import Optional
5
+
6
+ from ._version_metadata import VersionMetadata
7
+ from ._version_prerelease import VersionPreRelease
8
+ from ._version_constraint import VersionConstraint
9
+ from ._version_standard import VersionStandard
10
+
11
+
12
+ _PEP440_CANONICAL_LABELS: dict[str, str] = {
13
+ "alpha": "a",
14
+ "beta": "b",
15
+ "c": "rc",
16
+ "pre": "rc",
17
+ "preview": "rc",
18
+ }
19
+
20
+
21
+ @lru_cache
22
+ def _get_parsers() -> list:
23
+ from .parsers import CalVerParser, LooseParser, NuGetParser, PEP440Parser, SemVerParser
24
+
25
+ return [
26
+ PEP440Parser(),
27
+ SemVerParser(),
28
+ NuGetParser(),
29
+ CalVerParser(),
30
+ LooseParser(),
31
+ ]
32
+
33
+
34
+ @total_ordering
35
+ @dataclass(slots=True)
36
+ class Version:
37
+ major: int = 0
38
+ minor: Optional[int] = None
39
+ patch: Optional[int] = None
40
+ rev: Optional[int] = None
41
+ pre: Optional[VersionPreRelease] = None
42
+ post: Optional[int] = None
43
+ dev: Optional[int] = None
44
+ metadata: Optional[VersionMetadata] = None
45
+
46
+ def __post_init__(self) -> None:
47
+ if self.major < 0:
48
+ raise ValueError(f"major must be non-negative, got {self.major}")
49
+ if self.minor is not None and self.minor < 0:
50
+ raise ValueError(f"minor must be non-negative, got {self.minor}")
51
+ if self.patch is not None and self.patch < 0:
52
+ raise ValueError(f"patch must be non-negative, got {self.patch}")
53
+ if self.rev is not None and self.rev < 0:
54
+ raise ValueError(f"rev must be non-negative, got {self.rev}")
55
+ if self.post is not None and self.post < 0:
56
+ raise ValueError(f"post must be non-negative, got {self.post}")
57
+ if self.dev is not None and self.dev < 0:
58
+ raise ValueError(f"dev must be non-negative, got {self.dev}")
59
+
60
+ @property
61
+ def core(self) -> str:
62
+ parts = [str(self.major)]
63
+ if self.minor is not None:
64
+ parts.append(str(self.minor))
65
+ if self.patch is not None:
66
+ parts.append(str(self.patch))
67
+ if self.rev is not None and self.rev > 0:
68
+ parts.append(str(self.rev))
69
+ return ".".join(parts)
70
+
71
+ @property
72
+ def standards(self) -> list[VersionStandard]:
73
+ compatible = []
74
+
75
+ # SEMVER: requires minor and patch, no rev/post/dev
76
+ if (
77
+ self.minor is not None
78
+ and self.patch is not None
79
+ and self.rev is None
80
+ and self.post is None
81
+ and self.dev is None
82
+ ):
83
+ compatible.append(VersionStandard.SEMVER)
84
+
85
+ # NUGET: requires minor and patch, no metadata/post/dev
86
+ if (
87
+ self.minor is not None
88
+ and self.patch is not None
89
+ and self.metadata is None
90
+ and self.post is None
91
+ and self.dev is None
92
+ ):
93
+ compatible.append(VersionStandard.NUGET)
94
+
95
+ # CALVER: requires minor, major >= 20, no pre/post/dev
96
+ if (
97
+ self.minor is not None
98
+ and self.major >= 20
99
+ and self.pre is None
100
+ and self.post is None
101
+ and self.dev is None
102
+ ):
103
+ compatible.append(VersionStandard.CALVER)
104
+
105
+ # LOOSE: no pre/post/dev
106
+ if self.pre is None and self.post is None and self.dev is None:
107
+ compatible.append(VersionStandard.LOOSE)
108
+
109
+ # PEP440 is always compatible
110
+ compatible.append(VersionStandard.PEP440)
111
+
112
+ return compatible
113
+
114
+ @property
115
+ def format(self) -> "VersionFormatter":
116
+ return VersionFormatter(self)
117
+
118
+ def _compare_core(self, other: "Version") -> int:
119
+ self_parts = (self.major, self.minor or 0, self.patch or 0, self.rev or 0)
120
+ other_parts = (other.major, other.minor or 0, other.patch or 0, other.rev or 0)
121
+ if self_parts < other_parts:
122
+ return -1
123
+ if self_parts > other_parts:
124
+ return 1
125
+ return 0
126
+
127
+ def _compare_pre(self, other: "Version") -> int:
128
+ if self.pre is None and other.pre is None:
129
+ return 0
130
+ if self.pre is None:
131
+ return 1
132
+ if other.pre is None:
133
+ return -1
134
+
135
+ self_parts = (self.pre.name.casefold(), self.pre.number or 0)
136
+ other_parts = (other.pre.name.casefold(), other.pre.number or 0)
137
+ if self_parts < other_parts:
138
+ return -1
139
+ if self_parts > other_parts:
140
+ return 1
141
+ return 0
142
+
143
+ def __eq__(self, other: object) -> bool:
144
+ if not isinstance(other, Version):
145
+ return NotImplemented
146
+ return (
147
+ self._compare_core(other) == 0
148
+ and self._compare_pre(other) == 0
149
+ and (self.post or 0) == (other.post or 0)
150
+ and (self.dev or 0) == (other.dev or 0)
151
+ )
152
+
153
+ def __hash__(self) -> int:
154
+ return hash((
155
+ self.major,
156
+ self.minor,
157
+ self.patch,
158
+ self.rev,
159
+ self.pre.name.casefold() if self.pre else None,
160
+ self.pre.number if self.pre else None,
161
+ self.post,
162
+ self.dev,
163
+ ))
164
+
165
+ def __lt__(self, other: object) -> bool:
166
+ if not isinstance(other, Version):
167
+ return NotImplemented
168
+
169
+ core_cmp = self._compare_core(other)
170
+ if core_cmp != 0:
171
+ return core_cmp < 0
172
+
173
+ dev_cmp = (self.dev or 0, other.dev or 0)
174
+ if dev_cmp[0] != dev_cmp[1]:
175
+ if self.dev is None:
176
+ return False
177
+ if other.dev is None:
178
+ return True
179
+ return self.dev < other.dev
180
+
181
+ pre_cmp = self._compare_pre(other)
182
+ if pre_cmp != 0:
183
+ return pre_cmp < 0
184
+
185
+ return (self.post or 0) < (other.post or 0)
186
+
187
+ def __str__(self) -> str:
188
+ standards = self.standards
189
+ return self.format(standards[0] if standards else VersionStandard.PEP440)
190
+
191
+ def get_constraint(self, constraint: VersionConstraint) -> str:
192
+ major = self.major
193
+ minor = self.minor or 0
194
+ patch = self.patch or 0
195
+ full_version = str(self)
196
+
197
+ if constraint == VersionConstraint.EXACT:
198
+ return f"=={full_version}"
199
+ if constraint == VersionConstraint.MINIMUM_ONLY:
200
+ return f">={full_version}"
201
+ if constraint == VersionConstraint.RANGE_NEXT_MAJOR:
202
+ return f">={full_version},<{major + 1}.0.0"
203
+ if constraint == VersionConstraint.RANGE_NEXT_MINOR:
204
+ return f">={full_version},<{major}.{minor + 1}.0"
205
+ if constraint == VersionConstraint.RANGE_NEXT_PATCH:
206
+ return f">={full_version},<{major}.{minor}.{patch + 1}"
207
+ if major > 0:
208
+ upper = f"{major + 1}.0.0"
209
+ elif minor > 0:
210
+ upper = f"0.{minor + 1}.0"
211
+ else:
212
+ upper = f"0.0.{patch + 1}"
213
+ return f">={full_version},<{upper}"
214
+
215
+ @staticmethod
216
+ def parse(version_string: Optional[str]) -> Optional["Version"]:
217
+ if version_string:
218
+ for parser in _get_parsers():
219
+ result = parser.parse(version_string.strip())
220
+ if result:
221
+ return result
222
+ return None
223
+
224
+
225
+ @dataclass(slots=True)
226
+ class VersionFormatter:
227
+ version: Version
228
+
229
+ def __call__(self, standard: VersionStandard) -> str:
230
+ if standard == VersionStandard.PEP440:
231
+ parts = [self.version.core]
232
+ if self.version.pre:
233
+ canonical = _PEP440_CANONICAL_LABELS.get(self.version.pre.name.casefold(), self.version.pre.name)
234
+ num = str(self.version.pre.number) if self.version.pre.number is not None else ""
235
+ parts.append(f"{canonical}{num}")
236
+ if self.version.post is not None:
237
+ parts.append(f".post{self.version.post}")
238
+ if self.version.dev is not None:
239
+ parts.append(f".dev{self.version.dev}")
240
+ if self.version.metadata:
241
+ parts.append(f"+{self.version.metadata}")
242
+ return "".join(parts)
243
+
244
+ if standard == VersionStandard.SEMVER:
245
+ parts = [self.version.core]
246
+ if self.version.pre:
247
+ parts.append(f"-{self.version.pre.name}")
248
+ if self.version.pre.number is not None:
249
+ parts.append(f".{self.version.pre.number}")
250
+ if self.version.metadata:
251
+ parts.append(f"+{self.version.metadata}")
252
+ return "".join(parts)
253
+
254
+ if standard == VersionStandard.NUGET:
255
+ parts = [self.version.core]
256
+ if self.version.pre:
257
+ parts.append(f"-{self.version.pre.name}")
258
+ if self.version.pre.number is not None:
259
+ parts.append(f".{self.version.pre.number}")
260
+ return "".join(parts)
261
+
262
+ if standard in (VersionStandard.CALVER, VersionStandard.LOOSE):
263
+ if self.version.metadata:
264
+ return f"{self.version.core}-{self.version.metadata}"
265
+ return self.version.core
266
+
267
+ return self(VersionStandard.PEP440)
268
+
269
+ @property
270
+ def pep440(self) -> str:
271
+ return self(VersionStandard.PEP440)
272
+
273
+ @property
274
+ def semver(self) -> str:
275
+ return self(VersionStandard.SEMVER)
276
+
277
+ @property
278
+ def nuget(self) -> str:
279
+ return self(VersionStandard.NUGET)
280
+
281
+ @property
282
+ def calver(self) -> str:
283
+ return self(VersionStandard.CALVER)
284
+
285
+ @property
286
+ def loose(self) -> str:
287
+ return self(VersionStandard.LOOSE)
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class VersionConstraint(str, Enum):
5
+ EXACT = "exact"
6
+ MINIMUM_ONLY = "minimum-only"
7
+ RANGE_NEXT_MAJOR = "range-next-major"
8
+ RANGE_NEXT_MINOR = "range-next-minor"
9
+ RANGE_NEXT_PATCH = "range-next-patch"
10
+ COMPATIBLE = "compatible"
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+ from functools import total_ordering
3
+
4
+
5
+ @total_ordering
6
+ @dataclass(slots=True)
7
+ class VersionMetadata:
8
+ value: str
9
+
10
+ def __str__(self) -> str:
11
+ return self.value
12
+
13
+ @property
14
+ def parts(self) -> list[str]:
15
+ return self.value.split(".")
16
+
17
+ def __eq__(self, other: object) -> bool:
18
+ if not isinstance(other, VersionMetadata):
19
+ return NotImplemented
20
+ return self.value == other.value
21
+
22
+ def __lt__(self, other: object) -> bool:
23
+ if not isinstance(other, VersionMetadata):
24
+ return NotImplemented
25
+ return self.value < other.value
26
+
27
+ def __hash__(self) -> int:
28
+ return hash(self.value)
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass
2
+ from functools import total_ordering
3
+ from typing import Optional
4
+
5
+
6
+ @total_ordering
7
+ @dataclass(slots=True)
8
+ class VersionPreRelease:
9
+ name: str
10
+ number: Optional[int] = None
11
+
12
+ def __post_init__(self) -> None:
13
+ if self.number is not None and self.number < 0:
14
+ raise ValueError(f"Pre-release number must be non-negative, got {self.number}")
15
+
16
+ def __str__(self) -> str:
17
+ result = self.name
18
+ if self.number is not None:
19
+ result += str(self.number)
20
+ return result
21
+
22
+ def __eq__(self, other: object) -> bool:
23
+ if not isinstance(other, VersionPreRelease):
24
+ return NotImplemented
25
+ return self.name.casefold() == other.name.casefold() and (self.number or 0) == (other.number or 0)
26
+
27
+ def __lt__(self, other: object) -> bool:
28
+ if not isinstance(other, VersionPreRelease):
29
+ return NotImplemented
30
+ self_name = self.name.casefold()
31
+ other_name = other.name.casefold()
32
+ if self_name != other_name:
33
+ return self_name < other_name
34
+ return (self.number or 0) < (other.number or 0)
35
+
36
+ def __hash__(self) -> int:
37
+ return hash((self.name.casefold(), self.number))
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class VersionStandard(Enum):
5
+ PEP440 = "pep440"
6
+ SEMVER = "semver"
7
+ NUGET = "nuget"
8
+ CALVER = "calver"
9
+ LOOSE = "loose"
@@ -0,0 +1,13 @@
1
+ from ._calver_parser import CalVerParser
2
+ from ._loose_parser import LooseParser
3
+ from ._nuget_parser import NuGetParser
4
+ from ._pep440_parser import PEP440Parser
5
+ from ._semver_parser import SemVerParser
6
+
7
+ __all__ = [
8
+ "CalVerParser",
9
+ "LooseParser",
10
+ "NuGetParser",
11
+ "PEP440Parser",
12
+ "SemVerParser",
13
+ ]
@@ -0,0 +1,10 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ from .. import Version
5
+
6
+
7
+ class BaseParser(ABC):
8
+ @abstractmethod
9
+ def parse(self, version_string: str) -> Optional[Version]:
10
+ pass
@@ -0,0 +1,38 @@
1
+ import re
2
+ from typing import Optional, cast
3
+
4
+ from .. import Version, VersionMetadata
5
+ from ._base_parser import BaseParser
6
+
7
+
8
+ class CalVerParser(BaseParser):
9
+ PATTERN = re.compile(
10
+ r"^(?P<major>20\d{2}|\d{2})"
11
+ r"\.(?P<minor>\d+)"
12
+ r"(?:\.(?P<patch>\d+))?"
13
+ r"(?:\.(?P<rev>\d+))?"
14
+ r"(?:[.\-+](?P<suffix>.+))?$"
15
+ )
16
+
17
+ def parse(self, version_string: str) -> Optional[Version]:
18
+ match = self.PATTERN.match(version_string)
19
+ if not match:
20
+ return None
21
+
22
+ groups = match.groupdict()
23
+ major = int(groups["major"])
24
+
25
+ if major < 20 or (major > 99 and major < 2020):
26
+ return None
27
+
28
+ patch = groups["patch"]
29
+ rev = groups["rev"]
30
+ suffix = groups["suffix"]
31
+
32
+ return Version(
33
+ major=major,
34
+ minor=int(groups["minor"]),
35
+ patch=int(patch) if patch else None,
36
+ rev=int(rev) if rev else None,
37
+ metadata=VersionMetadata(cast(str, suffix)) if suffix else None,
38
+ )
@@ -0,0 +1,34 @@
1
+ import re
2
+ from typing import Optional, cast
3
+
4
+ from .. import Version, VersionMetadata
5
+ from ._base_parser import BaseParser
6
+
7
+
8
+ class LooseParser(BaseParser):
9
+ PATTERN = re.compile(
10
+ r"^(?P<major>\d+)"
11
+ r"(?:\.(?P<minor>\d+))?"
12
+ r"(?:\.(?P<patch>\d+))?"
13
+ r"(?:\.(?P<rev>\d+))?"
14
+ r"(?:[.\-+](?P<suffix>.+))?$"
15
+ )
16
+
17
+ def parse(self, version_string: str) -> Optional[Version]:
18
+ match = self.PATTERN.match(version_string)
19
+ if not match:
20
+ return None
21
+
22
+ groups = match.groupdict()
23
+ minor = groups["minor"]
24
+ patch = groups["patch"]
25
+ rev = groups["rev"]
26
+ suffix = groups["suffix"]
27
+
28
+ return Version(
29
+ major=int(groups["major"]),
30
+ minor=int(minor) if minor else None,
31
+ patch=int(patch) if patch else None,
32
+ rev=int(rev) if rev else None,
33
+ metadata=VersionMetadata(cast(str, suffix)) if suffix else None,
34
+ )
@@ -0,0 +1,42 @@
1
+ import re
2
+ from typing import Optional, cast
3
+
4
+ from .. import Version, VersionPreRelease
5
+ from ._base_parser import BaseParser
6
+
7
+
8
+ class NuGetParser(BaseParser):
9
+ PATTERN = re.compile(
10
+ r"^(?P<major>\d+)"
11
+ r"\.(?P<minor>\d+)"
12
+ r"\.(?P<patch>\d+)"
13
+ r"(?:\.(?P<rev>\d+))?"
14
+ r"(?:-(?P<pre>[0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?$"
15
+ )
16
+
17
+ def parse(self, version_string: str) -> Optional[Version]:
18
+ match = self.PATTERN.match(version_string)
19
+ if not match:
20
+ return None
21
+
22
+ groups = match.groupdict()
23
+ pre_str = cast(Optional[str], groups["pre"])
24
+ pre_release: Optional[VersionPreRelease] = None
25
+
26
+ if pre_str:
27
+ pre_parts = re.match(r"^([A-Za-z]+)\.?(\d+)?", pre_str)
28
+ if pre_parts:
29
+ pre_num = pre_parts.group(2)
30
+ pre_release = VersionPreRelease(
31
+ cast(str, pre_parts.group(1)),
32
+ int(pre_num) if pre_num else None,
33
+ )
34
+
35
+ rev = groups["rev"]
36
+ return Version(
37
+ major=int(groups["major"]),
38
+ minor=int(groups["minor"]),
39
+ patch=int(groups["patch"]),
40
+ rev=int(rev) if rev else None,
41
+ pre=pre_release,
42
+ )
@@ -0,0 +1,45 @@
1
+ import re
2
+ from typing import Optional, cast
3
+
4
+ from .. import Version, VersionMetadata, VersionPreRelease
5
+ from ._base_parser import BaseParser
6
+
7
+
8
+ class PEP440Parser(BaseParser):
9
+ PATTERN = re.compile(
10
+ r"^(?P<major>\d+)"
11
+ r"(?:\.(?P<minor>\d+))?"
12
+ r"(?:\.(?P<patch>\d+))?"
13
+ r"(?:\.(?P<rev>\d+))?"
14
+ r"(?:(?P<pre_label>a|alpha|b|beta|rc|c|pre|preview)(?P<pre_num>\d+))?"
15
+ r"(?:\.post(?P<post>\d+))?"
16
+ r"(?:\.dev(?P<dev>\d+))?"
17
+ r"(?:\+(?P<meta>[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*))?$",
18
+ re.IGNORECASE,
19
+ )
20
+
21
+ def parse(self, version_string: str) -> Optional[Version]:
22
+ match = self.PATTERN.match(version_string)
23
+ if not match:
24
+ return None
25
+
26
+ groups = match.groupdict()
27
+ minor = groups["minor"]
28
+ patch = groups["patch"]
29
+ rev = groups["rev"]
30
+ pre_label = groups["pre_label"]
31
+ pre_num = groups["pre_num"]
32
+ post = groups["post"]
33
+ dev = groups["dev"]
34
+ meta = groups["meta"]
35
+
36
+ return Version(
37
+ major=int(groups["major"]),
38
+ minor=int(minor) if minor else None,
39
+ patch=int(patch) if patch else None,
40
+ rev=int(rev) if rev else None,
41
+ pre=VersionPreRelease(pre_label, int(pre_num)) if pre_label and pre_num else None,
42
+ post=int(post) if post else None,
43
+ dev=int(dev) if dev else None,
44
+ metadata=VersionMetadata(cast(str, meta)) if meta else None,
45
+ )
@@ -0,0 +1,42 @@
1
+ import re
2
+ from typing import Optional, cast
3
+
4
+ from .. import Version, VersionMetadata, VersionPreRelease
5
+ from ._base_parser import BaseParser
6
+
7
+
8
+ class SemVerParser(BaseParser):
9
+ PATTERN = re.compile(
10
+ r"^(?P<major>\d+)"
11
+ r"\.(?P<minor>\d+)"
12
+ r"\.(?P<patch>\d+)"
13
+ r"(?:-(?P<pre>[0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?"
14
+ r"(?:\+(?P<meta>[0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?$"
15
+ )
16
+
17
+ def parse(self, version_string: str) -> Optional[Version]:
18
+ match = self.PATTERN.match(version_string)
19
+ if not match:
20
+ return None
21
+
22
+ groups = match.groupdict()
23
+ pre_str = cast(Optional[str], groups["pre"])
24
+ pre_release: Optional[VersionPreRelease] = None
25
+
26
+ if pre_str:
27
+ pre_parts = re.match(r"^([A-Za-z]+)\.?(\d+)?", pre_str)
28
+ if pre_parts:
29
+ pre_num = pre_parts.group(2)
30
+ pre_release = VersionPreRelease(
31
+ cast(str, pre_parts.group(1)),
32
+ int(pre_num) if pre_num else None,
33
+ )
34
+
35
+ meta = groups["meta"]
36
+ return Version(
37
+ major=int(groups["major"]),
38
+ minor=int(groups["minor"]),
39
+ patch=int(groups["patch"]),
40
+ pre=pre_release,
41
+ metadata=VersionMetadata(cast(str, meta)) if meta else None,
42
+ )