Skip to content

Commit 94232b4

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. * Update CHANGELOG.rst
1 parent f2c23f6 commit 94232b4

File tree

4 files changed

+89
-0
lines changed

4 files changed

+89
-0
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Features
2222
* :gh:`201` (:pr:`202`): Reformatted source code with black
2323
* :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid`
2424
and extend :command:`pysemver` with :command:`check` subcommand
25+
* :gh:`210` (:pr:`211`): Added classmethod :func:`semver.VersionInfo.coerce`
2526

2627

2728
Bug Fixes

docs/usage.rst

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

215215

216+
Parsing Incomplete Versions
217+
---------------------------
218+
219+
If a version is incomplete (some parts are missing) this would lead to an
220+
exception during the parsing process. As such, you cannot get a valid
221+
:class:`semver.VersionInfo` instance.
222+
223+
To avoid such situations, such incomplete version strings can still be parsed
224+
with the :func:`semver.VersionInfo.coerce` function. This function applies
225+
the following rules:
226+
227+
* Tries to detect a "basic" version string (``major.minor.patch``).
228+
* If not enough components can be found, missing components are
229+
set to zero to obtain a valid semver version.
230+
231+
It expects a version string and returns a tuple. The first item of the tuple
232+
is either a :class:`VersionInfo` instance or ``None`` if the function cannot
233+
extract a version. The second item is the rest of the string:
234+
235+
.. code-block:: python
236+
237+
>>> semver.VersionInfo.coerce("v1.2")
238+
(VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '')
239+
>>> semver.VersionInfo.coerce("v1.2-abc")
240+
(VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '-abc')
241+
>>> semver.VersionInfo.coerce("abc")
242+
(None, 'abc')
243+
244+
216245
Increasing Parts of a Version
217246
-----------------------------
218247

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
^
@@ -336,6 +349,34 @@ def isvalid(cls, version):
336349
except ValueError:
337350
return False
338351

352+
@classmethod
353+
def coerce(cls, version):
354+
"""
355+
Convert an incomplete version string into a semver-compatible VersionInfo
356+
object
357+
358+
* Tries to detect a "basic" version string (``major.minor.patch``).
359+
* If not enough components can be found, missing components are
360+
set to zero to obtain a valid semver version.
361+
362+
:param str version: the version string to convert
363+
:return: a tuple with a :class:`VersionInfo` instance (or ``None``
364+
if it's not a version) and the rest of the string which doesn't
365+
belong to a basic version.
366+
:rtype: tuple(:class:`VersionInfo` | None, str)
367+
"""
368+
match = _BASEVERSION.search(version)
369+
if not match:
370+
return (None, version)
371+
372+
ver = {
373+
key: 0 if value is None else value
374+
for key, value in match.groupdict().items()
375+
}
376+
ver = cls(**ver)
377+
rest = match.string[match.end() :] # noqa: E203
378+
return ver, rest
379+
339380

340381
def _to_dict(obj):
341382
if isinstance(obj, VersionInfo):

test_semver.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,3 +841,21 @@ def test_replace_raises_ValueError_for_non_numeric_values():
841841
def test_should_versioninfo_isvalid():
842842
assert VersionInfo.isvalid("1.0.0") is True
843843
assert VersionInfo.isvalid("foo") is False
844+
845+
846+
@pytest.mark.parametrize(
847+
"version,expected",
848+
[
849+
("v1", ("1.0.0", "")),
850+
("v1.1", ("1.1.0", "")),
851+
("v1.1.1", ("1.1.1", "")),
852+
("v1.1.1-abc", ("1.1.1", "-abc")),
853+
("not-semver", (None, "not-semver")),
854+
("1-2-3", ("1.0.0", "-2-3")),
855+
],
856+
)
857+
def test_should_versioninfo_coerce(version, expected):
858+
ver = list(VersionInfo.coerce(version))
859+
if ver[0] is not None:
860+
ver[0] = str(ver[0])
861+
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