Skip to content

Commit bdf2c12

Browse files
committed
Fix #210: Add classmethod VersionInfo.coerce
Signature: VersionInfo.coerce(cls, version:str) -> tuple Convert an incomplete version string into a semver-compatible VersionInfo object - Tries to detect a "basic" version string (``major.minor.patch``). - If not enough components can be found, missing components are set to zero to obtain a valid semver version.
1 parent 445f47a commit bdf2c12

File tree

3 files changed

+88
-0
lines changed

3 files changed

+88
-0
lines changed

docs/usage.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,35 @@ Depending which function you call, you get different types
192192
{'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None}
193193

194194

195+
Parsing Incomplete Versions
196+
---------------------------
197+
198+
If a version is incomplete (some parts are missing) this would lead to an
199+
exception during the parsing process. As such, you cannot get a valid
200+
:class:`semver.VersionInfo` instance.
201+
202+
To avoid such situations, such incomplete version strings can still be parsed
203+
with the :func:`semver.VersionInfo.coerce` function. This function applies
204+
the following rules:
205+
206+
* Tries to detect a "basic" version string (``major.minor.patch``).
207+
* If not enough components can be found, missing components are
208+
set to zero to obtain a valid semver version.
209+
210+
It expects a version string and returns a tuple. The first item of the tuple
211+
is either a :class:`VersionInfo` instance or ``None`` if the function cannot
212+
extract a version. The second item is the rest of the string:
213+
214+
.. code-block:: python
215+
216+
>>> semver.VersionInfo.coerce("v1.2")
217+
(VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '')
218+
>>> semver.VersionInfo.coerce("v1.2-abc")
219+
(VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '-abc')
220+
>>> semver.VersionInfo.coerce("abc")
221+
(None, 'abc')
222+
223+
195224
Increasing Parts of a Version
196225
-----------------------------
197226

semver.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@
1616
__maintainer__ = "Sebastien Celles"
1717
__maintainer_email__ = "s.celles@gmail.com"
1818

19+
_BASEVERSION = re.compile(
20+
r"""[vV]?
21+
(?P<major>0|[1-9]\d*)
22+
(\.
23+
(?P<minor>0|[1-9]\d*)
24+
(\.
25+
(?P<patch>0|[1-9]\d*)
26+
)?
27+
)?
28+
""",
29+
re.VERBOSE,
30+
)
31+
1932
_REGEX = re.compile(
2033
r"""
2134
^
@@ -321,6 +334,34 @@ def replace(self, **parts):
321334
)
322335
raise TypeError(error)
323336

337+
@classmethod
338+
def coerce(cls, version):
339+
"""
340+
Convert an incomplete version string into a semver-compatible VersionInfo
341+
object
342+
343+
* Tries to detect a "basic" version string (``major.minor.patch``).
344+
* If not enough components can be found, missing components are
345+
set to zero to obtain a valid semver version.
346+
347+
:param str version: the version string to convert
348+
:return: a tuple with a :class:`VersionInfo` instance (or ``None``
349+
if it's not a version) and the rest of the string which doesn't
350+
belong to a basic version.
351+
:rtype: tuple(:class:`VersionInfo` | None, str)
352+
"""
353+
match = _BASEVERSION.search(version)
354+
if not match:
355+
return (None, version)
356+
357+
ver = {
358+
key: 0 if value is None else value
359+
for key, value in match.groupdict().items()
360+
}
361+
ver = cls(**ver)
362+
rest = match.string[match.end() :]
363+
return ver, rest
364+
324365

325366
def _to_dict(obj):
326367
if isinstance(obj, VersionInfo):

test_semver.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,3 +803,21 @@ def test_should_return_versioninfo_with_replaced_parts(version, parts, expected)
803803
def test_replace_raises_ValueError_for_non_numeric_values():
804804
with pytest.raises(ValueError):
805805
VersionInfo.parse("1.2.3").replace(major="x")
806+
807+
808+
@pytest.mark.parametrize(
809+
"version,expected",
810+
[
811+
("v1", ("1.0.0", "")),
812+
("v1.1", ("1.1.0", "")),
813+
("v1.1.1", ("1.1.1", "")),
814+
("v1.1.1-abc", ("1.1.1", "-abc")),
815+
("not-semver", (None, "not-semver")),
816+
("1-2-3", ("1.0.0", "-2-3")),
817+
],
818+
)
819+
def test_should_versioninfo_coerce(version, expected):
820+
ver = list(VersionInfo.coerce(version))
821+
if ver[0] is not None:
822+
ver[0] = str(ver[0])
823+
assert tuple(ver) == expected

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy