From f1d7cdee8c1deccfba9b0ee39f5f19a0a1cdfc42 Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Fri, 16 Aug 2019 16:39:38 -0400 Subject: [PATCH 01/10] Add __getitem__ method to allow easy access to version parts --- semver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/semver.py b/semver.py index d234f26a..8f1b46e2 100644 --- a/semver.py +++ b/semver.py @@ -164,6 +164,9 @@ def __iter__(self): for v in self._astuple(): yield v + def __getitem__(self, index): + return self._astuple()[index] + @comparator def __eq__(self, other): return _compare_by_keys(self._asdict(), _to_dict(other)) == 0 From ba5899e0bb95bb6c34ab86a4f928064c4ae06d6b Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Mon, 19 Aug 2019 09:56:55 -0400 Subject: [PATCH 02/10] Add unit tests for index access to version parts --- test_semver.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test_semver.py b/test_semver.py index 96b78967..d857f729 100644 --- a/test_semver.py +++ b/test_semver.py @@ -497,3 +497,15 @@ def test_immutable_unknown_attribute(version): def test_version_info_should_be_iterable(version): assert tuple(version) == (version.major, version.minor, version.patch, version.prerelease, version.build) + + +@pytest.mark.parametrize("version, index, expected", [ + ("1.2.3-rc.0+build.0", 0, 1), + ("1.2.3-rc.0+build.0", 1, 2), + ("1.2.3-rc.0+build.0", 2, 3), + ("1.2.3-rc.0+build.0", 3, "rc.0"), + ("1.2.3-rc.0+build.0", 4, "build.0"), +]) +def test_version_info_should_be_accessed_with_index(version, index, expected): + version_info = VersionInfo.parse(version) + assert version_info[index] == expected From e88e0ba8a49655cafbafe70f4674440dffb9a2dd Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Mon, 19 Aug 2019 09:59:41 -0400 Subject: [PATCH 03/10] Remove whitespace on blank line --- test_semver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_semver.py b/test_semver.py index d857f729..a22ed67c 100644 --- a/test_semver.py +++ b/test_semver.py @@ -498,7 +498,7 @@ def test_version_info_should_be_iterable(version): assert tuple(version) == (version.major, version.minor, version.patch, version.prerelease, version.build) - + @pytest.mark.parametrize("version, index, expected", [ ("1.2.3-rc.0+build.0", 0, 1), ("1.2.3-rc.0+build.0", 1, 2), From 80da20c78117c59a9d37567aeb8591f55cef9a93 Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Tue, 8 Oct 2019 11:45:41 -0400 Subject: [PATCH 04/10] Fix stash conflicts and create more test vectors. Also add private utility function for check the validity of the index. --- semver.py | 27 ++++++++++++++++++- test_semver.py | 71 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/semver.py b/semver.py index f9950efc..39ac420d 100644 --- a/semver.py +++ b/semver.py @@ -168,8 +168,33 @@ def __iter__(self): for v in self._astuple(): yield v + @staticmethod + def _validate_index(index): + if isinstance(index, slice): + checks = [index.stop >= 0] + if index.start is not None: + checks.append(index.start >= 0) + if index.step is not None: + if index.step > 0: + checks.append(index.start < index.stop) + else: + checks.append(index.start > index.stop) + elif index.step is not None: + checks.append(index.step > 0) + return all(checks) + else: + return index >= 0 + def __getitem__(self, index): - return self._astuple()[index] + if VersionInfo._validate_index(index): + sub_version = self._astuple()[index] + if sub_version is not None: + return sub_version + else: + raise IndexError("Version part at index %d is not defined" + % index) + else: + raise IndexError("VersionInfo does not support negative index") def bump_major(self): """Raise the major part of the version, return a new object diff --git a/test_semver.py b/test_semver.py index f3d86604..fba6c053 100644 --- a/test_semver.py +++ b/test_semver.py @@ -548,18 +548,6 @@ def test_version_info_should_be_iterable(version): version.prerelease, version.build) -<<<<<<< HEAD -@pytest.mark.parametrize("version, index, expected", [ - ("1.2.3-rc.0+build.0", 0, 1), - ("1.2.3-rc.0+build.0", 1, 2), - ("1.2.3-rc.0+build.0", 2, 3), - ("1.2.3-rc.0+build.0", 3, "rc.0"), - ("1.2.3-rc.0+build.0", 4, "build.0"), -]) -def test_version_info_should_be_accessed_with_index(version, index, expected): - version_info = VersionInfo.parse(version) - assert version_info[index] == expected -======= def test_should_compare_prerelease_and_build_with_numbers(): assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < \ VersionInfo(major=1, minor=9, patch=1, prerelease=2, build=1) @@ -592,4 +580,61 @@ def test_should_be_able_to_use_integers_as_prerelease_build(): assert isinstance(v.prerelease, str) assert isinstance(v.build, str) assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, '4', '5') ->>>>>>> c585f5cb8b9a0d5859a885e94a7e84597a554d67 + + +@pytest.mark.parametrize("version, index, expected", [ + # Simple positive indices + ("1.2.3-rc.0+build.0", 0, 1), + ("1.2.3-rc.0+build.0", 1, 2), + ("1.2.3-rc.0+build.0", 2, 3), + ("1.2.3-rc.0+build.0", 3, "rc.0"), + ("1.2.3-rc.0+build.0", 4, "build.0"), + ("1.2.3-rc.0", 0, 1), + ("1.2.3-rc.0", 1, 2), + ("1.2.3-rc.0", 2, 3), + ("1.2.3-rc.0", 3, "rc.0"), + ("1.2.3", 0, 1), + ("1.2.3", 1, 2), + ("1.2.3", 2, 3), +]) +def test_version_info_should_be_accessed_with_positive_index(version, + index, expected): + version_info = VersionInfo.parse(version) + assert version_info[index] == expected + + +@pytest.mark.parametrize("version, slice_object, expected", [ + # Slice indices + ("1.2.3-rc.0+build.0", slice(0, 5), (1, 2, 3, "rc.0", "build.0")), + ("1.2.3-rc.0+build.0", slice(0, 4), (1, 2, 3, "rc.0")), + ("1.2.3-rc.0+build.0", slice(0, 3), (1, 2, 3)), + ("1.2.3-rc.0+build.0", slice(0, 2), (1, 2)), + ("1.2.3-rc.0+build.0", slice(3, 5), ("rc.0", "build.0")), + ("1.2.3-rc.0", slice(0, 4), (1, 2, 3, "rc.0")), + ("1.2.3-rc.0", slice(0, 3), (1, 2, 3)), + ("1.2.3-rc.0", slice(0, 2), (1, 2)), + ("1.2.3", slice(0, 3), (1, 2, 3)), + ("1.2.3", slice(0, 2), (1, 2)), + # Special cases + ("1.2.3-rc.0+build.0", slice(3), (1, 2, 3)), + ("1.2.3-rc.0+build.0", slice(0, 5, 2), (1, 3, "build.0")), + ("1.2.3-rc.0+build.0", slice(None, 5, 2), (1, 3, "build.0")), + ("1.2.3-rc.0+build.0", slice(5, 0, -2), ("build.0", 3)), +]) +def test_version_info_should_be_accessed_with_slice_object(version, + slice_object, + expected): + version_info = VersionInfo.parse(version) + assert version_info[slice_object] == expected + + +@pytest.mark.parametrize("version, index", [ + ("1.2.3-rc.0", 4), + ("1.2.3-rc.0+build.0", -1), + ("1.2.3", 3), + ("1.2.3", 4), +]) +def test_version_info_should_throw_index_error(version, index): + version_info = VersionInfo.parse(version) + with pytest.raises(IndexError): + version_info[index] From a610c2256dfc2e9de4f254dc268700512b38a7dc Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Tue, 8 Oct 2019 13:49:05 -0400 Subject: [PATCH 05/10] Use simpler implementation that allows negative indices and remove now useless _validate_index. --- semver.py | 30 ++++-------------------------- test_semver.py | 8 +++++--- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/semver.py b/semver.py index 39ac420d..07bdb03d 100644 --- a/semver.py +++ b/semver.py @@ -168,33 +168,11 @@ def __iter__(self): for v in self._astuple(): yield v - @staticmethod - def _validate_index(index): - if isinstance(index, slice): - checks = [index.stop >= 0] - if index.start is not None: - checks.append(index.start >= 0) - if index.step is not None: - if index.step > 0: - checks.append(index.start < index.stop) - else: - checks.append(index.start > index.stop) - elif index.step is not None: - checks.append(index.step > 0) - return all(checks) - else: - return index >= 0 - def __getitem__(self, index): - if VersionInfo._validate_index(index): - sub_version = self._astuple()[index] - if sub_version is not None: - return sub_version - else: - raise IndexError("Version part at index %d is not defined" - % index) - else: - raise IndexError("VersionInfo does not support negative index") + """Implement getitem. This automatically strips empty parts of the + version from the iterable from which the index is taken.""" + return tuple(part for part in self._astuple() + if part is not None)[index] def bump_major(self): """Raise the major part of the version, return a new object diff --git a/test_semver.py b/test_semver.py index fba6c053..a0f75f81 100644 --- a/test_semver.py +++ b/test_semver.py @@ -589,16 +589,18 @@ def test_should_be_able_to_use_integers_as_prerelease_build(): ("1.2.3-rc.0+build.0", 2, 3), ("1.2.3-rc.0+build.0", 3, "rc.0"), ("1.2.3-rc.0+build.0", 4, "build.0"), + ("1.2.3-rc.0+build.0", -1, "build.0"), ("1.2.3-rc.0", 0, 1), ("1.2.3-rc.0", 1, 2), ("1.2.3-rc.0", 2, 3), ("1.2.3-rc.0", 3, "rc.0"), + ("1.2.3-rc.0", -1, "rc.0"), ("1.2.3", 0, 1), ("1.2.3", 1, 2), ("1.2.3", 2, 3), + ("1.2.3", -1, 3), ]) -def test_version_info_should_be_accessed_with_positive_index(version, - index, expected): +def test_version_info_should_be_accessed_with_index(version, index, expected): version_info = VersionInfo.parse(version) assert version_info[index] == expected @@ -613,6 +615,7 @@ def test_version_info_should_be_accessed_with_positive_index(version, ("1.2.3-rc.0", slice(0, 4), (1, 2, 3, "rc.0")), ("1.2.3-rc.0", slice(0, 3), (1, 2, 3)), ("1.2.3-rc.0", slice(0, 2), (1, 2)), + ("1.2.3", slice(0, 10), (1, 2, 3)), ("1.2.3", slice(0, 3), (1, 2, 3)), ("1.2.3", slice(0, 2), (1, 2)), # Special cases @@ -630,7 +633,6 @@ def test_version_info_should_be_accessed_with_slice_object(version, @pytest.mark.parametrize("version, index", [ ("1.2.3-rc.0", 4), - ("1.2.3-rc.0+build.0", -1), ("1.2.3", 3), ("1.2.3", 4), ]) From 95e79d8d98c349f4aed1b23265851a36877e6835 Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Thu, 10 Oct 2019 11:08:05 -0400 Subject: [PATCH 06/10] Add param and raises to __getitem__ docstring Co-Authored-By: Tom Schraitle --- semver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/semver.py b/semver.py index 07bdb03d..a15e2793 100644 --- a/semver.py +++ b/semver.py @@ -170,7 +170,11 @@ def __iter__(self): def __getitem__(self, index): """Implement getitem. This automatically strips empty parts of the - version from the iterable from which the index is taken.""" + version from the iterable from which the index is taken. + + :param int index: a positive or negative integer indicating the offset + :raises: IndexError, if index is beyond the range + """ return tuple(part for part in self._astuple() if part is not None)[index] From 27632b726ca63a2fdbfef2a787e8977fc218252c Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Tue, 19 Nov 2019 20:49:59 -0500 Subject: [PATCH 07/10] Reiterate the logic for __getitem__ and update tests to follow the actual features. --- semver.py | 33 +++++++++++++++++++++++++++------ test_semver.py | 6 +++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/semver.py b/semver.py index e6d0f88b..0c8b6fdd 100644 --- a/semver.py +++ b/semver.py @@ -175,14 +175,35 @@ def __iter__(self): yield v def __getitem__(self, index): - """Implement getitem. This automatically strips empty parts of the - version from the iterable from which the index is taken. + """Implement getitem. If the part requested is undefined, or a part of the + range requested is undefined, it will throw an index error. + Negative indices are not supported - :param int index: a positive or negative integer indicating the offset - :raises: IndexError, if index is beyond the range + :param int index: a positive integer indicating the offset or a slice + :raises: IndexError, if index is beyond the range or the part is None """ - return tuple(part for part in self._astuple() - if part is not None)[index] + undefined_error = IndexError("Version part undefined") + negative_error = IndexError("Version index cannot be negative") + if isinstance(index, slice): + if (False if index.start is None else index.start < 0) or (False if index.stop is None else index.stop < 0): + raise negative_error + else: + slice_is_full = True + part = self._astuple()[index] + for i in part: + if i is None: + slice_is_full = False + elif not slice_is_full: + raise undefined_error + part = tuple(filter(None, part)) + else: + if index < 0: + raise negative_error + else: + part = self._astuple()[index] + if part is None: + raise undefined_error + return part def bump_major(self): """Raise the major part of the version, return a new object diff --git a/test_semver.py b/test_semver.py index f09b5170..0edbe61c 100644 --- a/test_semver.py +++ b/test_semver.py @@ -707,16 +707,13 @@ def test_replace_raises_ValueError_for_non_numeric_values(): ("1.2.3-rc.0+build.0", 2, 3), ("1.2.3-rc.0+build.0", 3, "rc.0"), ("1.2.3-rc.0+build.0", 4, "build.0"), - ("1.2.3-rc.0+build.0", -1, "build.0"), ("1.2.3-rc.0", 0, 1), ("1.2.3-rc.0", 1, 2), ("1.2.3-rc.0", 2, 3), ("1.2.3-rc.0", 3, "rc.0"), - ("1.2.3-rc.0", -1, "rc.0"), ("1.2.3", 0, 1), ("1.2.3", 1, 2), ("1.2.3", 2, 3), - ("1.2.3", -1, 3), ]) def test_version_info_should_be_accessed_with_index(version, index, expected): version_info = VersionInfo.parse(version) @@ -750,7 +747,10 @@ def test_version_info_should_be_accessed_with_slice_object(version, @pytest.mark.parametrize("version, index", [ + ("1.2.3-rc.0+build.0", -1), + ("1.2.3-rc.0", -1), ("1.2.3-rc.0", 4), + ("1.2.3", -1), ("1.2.3", 3), ("1.2.3", 4), ]) From 976a4ee2da6ad276e26ff9839611dacd2e27d9e9 Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Tue, 19 Nov 2019 20:54:12 -0500 Subject: [PATCH 08/10] Update __getitem__ docstring --- semver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/semver.py b/semver.py index 0c8b6fdd..f30af70e 100644 --- a/semver.py +++ b/semver.py @@ -179,8 +179,8 @@ def __getitem__(self, index): range requested is undefined, it will throw an index error. Negative indices are not supported - :param int index: a positive integer indicating the offset or a slice - :raises: IndexError, if index is beyond the range or the part is None + :param Union[int, slice] index: a positive integer indicating the offset or a slice + :raises: IndexError, if index is beyond the range or a part is None """ undefined_error = IndexError("Version part undefined") negative_error = IndexError("Version index cannot be negative") From cf65b3d38c77bc6286baa6ccf983f104dd404fd2 Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Tue, 19 Nov 2019 21:03:38 -0500 Subject: [PATCH 09/10] Fix lint issues --- semver.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/semver.py b/semver.py index f30af70e..6b0a806f 100644 --- a/semver.py +++ b/semver.py @@ -179,13 +179,15 @@ def __getitem__(self, index): range requested is undefined, it will throw an index error. Negative indices are not supported - :param Union[int, slice] index: a positive integer indicating the offset or a slice + :param Union[int, slice] index: a positive integer indicating the offset + or a slice :raises: IndexError, if index is beyond the range or a part is None """ undefined_error = IndexError("Version part undefined") negative_error = IndexError("Version index cannot be negative") if isinstance(index, slice): - if (False if index.start is None else index.start < 0) or (False if index.stop is None else index.stop < 0): + if (False if index.start is None else index.start < 0) or \ + (False if index.stop is None else index.stop < 0): raise negative_error else: slice_is_full = True From dabebd96bbb93e1082e9f8f983900690ac02b09c Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Tue, 19 Nov 2019 23:03:58 -0500 Subject: [PATCH 10/10] Fix lint issues --- semver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/semver.py b/semver.py index 6b0a806f..66d5262d 100644 --- a/semver.py +++ b/semver.py @@ -179,8 +179,8 @@ def __getitem__(self, index): range requested is undefined, it will throw an index error. Negative indices are not supported - :param Union[int, slice] index: a positive integer indicating the offset - or a slice + :param Union[int, slice] index: a positive integer indicating the + offset or a slice :raises: IndexError, if index is beyond the range or a part is None """ undefined_error = IndexError("Version part undefined")