Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/pr359.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added optional parameter optional_minor_and_patch to allow optional
Comment thread
OidaTiftla marked this conversation as resolved.
Outdated
minor and patch parts.
7 changes: 7 additions & 0 deletions docs/usage/parse-version-string.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ Use the function :func:`Version.parse <semver.version.Version.parse>`::

>>> Version.parse("3.4.5-pre.2+build.4")
Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4')

You can set the parameter ``optional_minor_and_patch=True`` to allow optional
minor and patch parts. Optional parts are set to zero. But keep in mind, that this
deviates from the semver specification.::
Comment thread
OidaTiftla marked this conversation as resolved.
Outdated

>>> Version.parse("1.2", optional_minor_and_patch=True)
Version(major=1, minor=2, patch=0, prerelease=None, build=None)
48 changes: 39 additions & 9 deletions src/semver/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,19 @@ class Version:
__slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build")
#: Regex for number in a prerelease
_LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+")
#: Regex for a semver version
_REGEX = re.compile(
#: Regex template for a semver version
_REGEX_TEMPLATE = \
r"""
^
(?P<major>0|[1-9]\d*)
\.
(?P<minor>0|[1-9]\d*)
\.
(?P<patch>0|[1-9]\d*)
(?:
\.
(?P<minor>0|[1-9]\d*)
(?:
\.
(?P<patch>0|[1-9]\d*)
){opt_patch}
){opt_minor}
(?:-(?P<prerelease>
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
Expand All @@ -86,7 +90,15 @@ class Version:
(?:\.[0-9a-zA-Z-]+)*
))?
$
""",
"""
#: Regex for a semver version
_REGEX = re.compile(
_REGEX_TEMPLATE.format(opt_patch='', opt_minor=''),
re.VERBOSE,
)
#: Regex for a semver version that might be shorter
_REGEX_OPTIONAL_MINOR_AND_PATCH = re.compile(
_REGEX_TEMPLATE.format(opt_patch='?', opt_minor='?'),
re.VERBOSE,
)

Expand Down Expand Up @@ -553,15 +565,26 @@ def match(self, match_expr: str) -> bool:
return cmp_res in possibilities

@classmethod
def parse(cls, version: String) -> "Version":
def parse(
cls,
version: String,
optional_minor_and_patch: bool = False
) -> "Version":
"""
Parse version string to a Version instance.

.. versionchanged:: 2.11.0
Changed method from static to classmethod to
allow subclasses.
.. versionchanged:: 3.0.0
Added optional parameter optional_minor_and_patch to allow optional
minor and patch parts.

:param version: version string
:param optional_minor_and_patch: if set to true, the version string to parse \
can contain optional minor and patch parts. Optional parts are set to zero.
By default (False), the version string to parse has to follow the semver
specification.
:return: a new :class:`Version` instance
:raises ValueError: if version is invalid
:raises TypeError: if version contains the wrong type
Expand All @@ -575,11 +598,18 @@ def parse(cls, version: String) -> "Version":
elif not isinstance(version, String.__args__): # type: ignore
raise TypeError("not expecting type '%s'" % type(version))

match = cls._REGEX.match(version)
if optional_minor_and_patch:
match = cls._REGEX_OPTIONAL_MINOR_AND_PATCH.match(version)
else:
match = cls._REGEX.match(version)
if match is None:
raise ValueError(f"{version} is not valid SemVer string")

matched_version_parts: Dict[str, Any] = match.groupdict()
if not matched_version_parts['minor']:
matched_version_parts['minor'] = 0
if not matched_version_parts['patch']:
matched_version_parts['patch'] = 0

return cls(**matched_version_parts)

Expand Down
50 changes: 50 additions & 0 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,56 @@ def test_should_parse_version(version, expected):
assert result == expected


@pytest.mark.parametrize(
"version,expected",
[
# no. 1
(
"1.2-alpha.1.2+build.11.e0f985a",
{
"major": 1,
"minor": 2,
"patch": 0,
"prerelease": "alpha.1.2",
"build": "build.11.e0f985a",
},
),
# no. 2
(
"1-alpha-1+build.11.e0f985a",
{
"major": 1,
"minor": 0,
"patch": 0,
"prerelease": "alpha-1",
"build": "build.11.e0f985a",
},
),
(
"0.1-0f",
{"major": 0, "minor": 1, "patch": 0, "prerelease": "0f", "build": None},
),
(
"0-0foo.1",
{"major": 0, "minor": 0, "patch": 0, "prerelease": "0foo.1", "build": None},
),
(
"0-0foo.1+build.1",
{
"major": 0,
"minor": 0,
"patch": 0,
"prerelease": "0foo.1",
"build": "build.1",
},
),
],
)
def test_should_parse_version_with_optional_minor_and_patch(version, expected):
result = Version.parse(version, optional_minor_and_patch=True)
assert result == expected


def test_parse_version_info_str_hash():
s_version = "1.2.3-alpha.1.2+build.11.e0f985a"
v = parse_version_info(s_version)
Expand Down