From 80a2db4bc3fb91d16e6d983ac0c75a6410462fa1 Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Thu, 12 May 2022 15:07:34 +0200 Subject: [PATCH 1/8] Allow shorter version for parsing --- src/semver/version.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/semver/version.py b/src/semver/version.py index 9e02544f..d43119de 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -89,6 +89,31 @@ class Version: """, re.VERBOSE, ) + #: Regex for a semver version that might be shorter + _REGEX_ALLOW_SHORT = re.compile( + r""" + ^ + (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + )? + )? + (?:-(?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* + ))? + (?:\+(?P + [0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+)* + ))? + $ + """, + re.VERBOSE, + ) def __init__( self, @@ -553,7 +578,7 @@ def match(self, match_expr: str) -> bool: return cmp_res in possibilities @classmethod - def parse(cls, version: String) -> "Version": + def parse(cls, version: String, allowShorterVersion: bool = False) -> "Version": """ Parse version string to a Version instance. @@ -575,11 +600,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 allowShorterVersion: + match = cls._REGEX_ALLOW_SHORT.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) From 548301f5bdb788a6711da9e00d1c5e05d7e799cc Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Wed, 18 May 2022 17:25:48 +0200 Subject: [PATCH 2/8] Improvements from PR feedback - Fix variable naming (snake_case and clearer name) - Add missing doc string - Fix DRY principle Co-authored-by: Tom Schraitle --- src/semver/version.py | 46 ++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/src/semver/version.py b/src/semver/version.py index d43119de..a51849ca 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -68,29 +68,8 @@ 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( - r""" - ^ - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - (?:-(?P - (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) - (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* - ))? - (?:\+(?P - [0-9a-zA-Z-]+ - (?:\.[0-9a-zA-Z-]+)* - ))? - $ - """, - re.VERBOSE, - ) - #: Regex for a semver version that might be shorter - _REGEX_ALLOW_SHORT = re.compile( + #: Regex template for a semver version + _REGEX_TEMPLATE = \ r""" ^ (?P0|[1-9]\d*) @@ -100,8 +79,8 @@ class Version: (?: \. (?P0|[1-9]\d*) - )? - )? + ){} + ){} (?:-(?P (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* @@ -111,7 +90,15 @@ class Version: (?:\.[0-9a-zA-Z-]+)* ))? $ - """, + """ + #: Regex for a semver version + _REGEX = re.compile( + _REGEX_TEMPLATE.format('', ''), + re.VERBOSE, + ) + #: Regex for a semver version that might be shorter + _REGEX_OPTIONAL_MINOR_AND_PATCH = re.compile( + _REGEX_TEMPLATE.format('?', '?'), re.VERBOSE, ) @@ -578,7 +565,7 @@ def match(self, match_expr: str) -> bool: return cmp_res in possibilities @classmethod - def parse(cls, version: String, allowShorterVersion: bool = False) -> "Version": + def parse(cls, version: String, optional_minor_and_patch: bool = False) -> "Version": """ Parse version string to a Version instance. @@ -587,6 +574,7 @@ def parse(cls, version: String, allowShorterVersion: bool = False) -> "Version": allow subclasses. :param version: version string + :param optional_minor_and_patch: if set to true, the minor and patch version digits are optional, but this will deviate from the semver spec :return: a new :class:`Version` instance :raises ValueError: if version is invalid :raises TypeError: if version contains the wrong type @@ -600,8 +588,8 @@ def parse(cls, version: String, allowShorterVersion: bool = False) -> "Version": elif not isinstance(version, String.__args__): # type: ignore raise TypeError("not expecting type '%s'" % type(version)) - if allowShorterVersion: - match = cls._REGEX_ALLOW_SHORT.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: From fe1c89497fcb0445d0ce159e35f9075a6ebe6aec Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Mon, 23 May 2022 22:40:57 +0200 Subject: [PATCH 3/8] Named placeholders Co-authored-by: Tom Schraitle --- src/semver/version.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/semver/version.py b/src/semver/version.py index a51849ca..2d953e5d 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -79,8 +79,8 @@ class Version: (?: \. (?P0|[1-9]\d*) - ){} - ){} + ){opt_patch} + ){opt_minor} (?:-(?P (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* @@ -93,12 +93,12 @@ class Version: """ #: Regex for a semver version _REGEX = re.compile( - _REGEX_TEMPLATE.format('', ''), + _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('?', '?'), + _REGEX_TEMPLATE.format(opt_patch='?', opt_minor='?'), re.VERBOSE, ) From c4257642c3f9a4e07527ab221d06efe47ee04073 Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Mon, 23 May 2022 22:43:04 +0200 Subject: [PATCH 4/8] Better description of parameter optional_minor_and_patch Co-authored-by: Tom Schraitle --- src/semver/version.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/semver/version.py b/src/semver/version.py index 2d953e5d..3de85324 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -574,7 +574,9 @@ def parse(cls, version: String, optional_minor_and_patch: bool = False) -> "Vers allow subclasses. :param version: version string - :param optional_minor_and_patch: if set to true, the minor and patch version digits are optional, but this will deviate from the semver spec + :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 From 4e73806187c815d5d143ccd08bb42cafafca1012 Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Mon, 23 May 2022 23:29:59 +0200 Subject: [PATCH 5/8] Fix long lines --- src/semver/version.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/semver/version.py b/src/semver/version.py index 3de85324..08af5e00 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -565,7 +565,11 @@ def match(self, match_expr: str) -> bool: return cmp_res in possibilities @classmethod - def parse(cls, version: String, optional_minor_and_patch: bool = False) -> "Version": + def parse( + cls, + version: String, + optional_minor_and_patch: bool = False + ) -> "Version": """ Parse version string to a Version instance. @@ -574,9 +578,10 @@ def parse(cls, version: String, optional_minor_and_patch: bool = False) -> "Vers allow subclasses. :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. + :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 From 65105dc2fcd7a9d666bd648d48cbf402bee585da Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Mon, 23 May 2022 23:48:18 +0200 Subject: [PATCH 6/8] Add tests for parameter optional_minor_and_patch --- tests/test_parsing.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 25c55c74..ddf52196 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -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) From 6c51b0e39274ddb8895260b17748bbcb2c570b21 Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Tue, 24 May 2022 00:42:26 +0200 Subject: [PATCH 7/8] Add documentation and changelog entry --- changelog.d/pr359.feature.rst | 2 ++ docs/usage/parse-version-string.rst | 7 +++++++ src/semver/version.py | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 changelog.d/pr359.feature.rst diff --git a/changelog.d/pr359.feature.rst b/changelog.d/pr359.feature.rst new file mode 100644 index 00000000..51ec88cc --- /dev/null +++ b/changelog.d/pr359.feature.rst @@ -0,0 +1,2 @@ +Added optional parameter optional_minor_and_patch to allow optional +minor and patch parts. diff --git a/docs/usage/parse-version-string.rst b/docs/usage/parse-version-string.rst index ddd421e7..70a866f1 100644 --- a/docs/usage/parse-version-string.rst +++ b/docs/usage/parse-version-string.rst @@ -6,3 +6,10 @@ Use the function :func:`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.:: + + >>> Version.parse("1.2", optional_minor_and_patch=True) + Version(major=1, minor=2, patch=0, prerelease=None, build=None) diff --git a/src/semver/version.py b/src/semver/version.py index 08af5e00..096acdf2 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -576,6 +576,9 @@ def parse( .. 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 \ From 94e0fe582f2c384a8b7d278a367ba7e1f2ee94ba Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Tue, 24 May 2022 09:32:26 +0200 Subject: [PATCH 8/8] Improve documentation by suggestions from PR review Co-authored-by: Tom Schraitle --- changelog.d/pr359.feature.rst | 2 +- docs/usage/parse-version-string.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/changelog.d/pr359.feature.rst b/changelog.d/pr359.feature.rst index 51ec88cc..5c18c9d2 100644 --- a/changelog.d/pr359.feature.rst +++ b/changelog.d/pr359.feature.rst @@ -1,2 +1,2 @@ -Added optional parameter optional_minor_and_patch to allow optional +Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional minor and patch parts. diff --git a/docs/usage/parse-version-string.rst b/docs/usage/parse-version-string.rst index 70a866f1..0a39c8a3 100644 --- a/docs/usage/parse-version-string.rst +++ b/docs/usage/parse-version-string.rst @@ -7,9 +7,9 @@ Use the function :func:`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.:: +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)