diff --git a/semver.py b/semver.py index 6bc9fcab..66d5262d 100644 --- a/semver.py +++ b/semver.py @@ -174,6 +174,39 @@ def __iter__(self): for v in self._astuple(): yield v + def __getitem__(self, index): + """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 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): + 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 but leave self untouched diff --git a/test_semver.py b/test_semver.py index 6b1bf579..0edbe61c 100644 --- a/test_semver.py +++ b/test_semver.py @@ -698,3 +698,63 @@ def test_should_return_versioninfo_with_replaced_parts(version, def test_replace_raises_ValueError_for_non_numeric_values(): with pytest.raises(ValueError): VersionInfo.parse("1.2.3").replace(major="x") + + +@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_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, 10), (1, 2, 3)), + ("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+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), +]) +def test_version_info_should_throw_index_error(version, index): + version_info = VersionInfo.parse(version) + with pytest.raises(IndexError): + version_info[index]