Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Allow shorter version for parsing #359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all 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 2 changelog.d/pr359.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional
minor and patch parts.
7 changes: 7 additions & 0 deletions 7 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')

Set the parameter ``optional_minor_and_patch=True`` to allow 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::

>>> 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 48 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 50 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
Morty Proxy This is a proxified and sanitized view of the page, visit original site.