From ec6588e0819f3b829653636f275a14d6fa1d2d15 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 26 Apr 2020 11:36:18 +0200 Subject: [PATCH] Implement __getitem__ for #138 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add __getitem__ to VersionInfo class * Add test cases * Add user documentation * Extend CHANGELOG Co-authored-by: Thomas Laferriere Co-authored-by: Peter Bittner Co-authored-by: Karol Werner Co-authored-by: Sébastien Celles --- CHANGELOG.rst | 2 ++ docs/usage.rst | 55 ++++++++++++++++++++++++++++++++++++-- semver.py | 36 +++++++++++++++++++++++++ test_semver.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ddefadcb..b731db8e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,8 @@ Version 2.10.0 (WIP) Features -------- +* :pr:`138`: Added ``__getitem__`` magic method to ``semver.VersionInfo`` class. + Allows to access a version like ``version[1]``. * :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising the old and deprecated module-level functions. diff --git a/docs/usage.rst b/docs/usage.rst index 91e4d069..63b18f6a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -141,8 +141,10 @@ classmethod :func:`semver.VersionInfo.isvalid`: False -Accessing Parts of a Version ----------------------------- +.. _sec.properties.parts: + +Accessing Parts of a Version Through Names +------------------------------------------ The :class:`semver.VersionInfo` contains attributes to access the different parts of a version: @@ -184,6 +186,55 @@ In case you need the different parts of a version stepwise, iterate over the :cl [3, 4, 5, 'pre.2', 'build.4'] +.. _sec.getitem.parts: + +Accessing Parts Through Index Numbers +------------------------------------- + +.. versionadded:: 2.10.0 + +Another way to access parts of a version is to use an index notation. The underlying +:class:`VersionInfo ` object allows to access its data through +the magic method :func:`__getitem__ `. + +For example, the ``major`` part can be accessed by index number 0 (zero). +Likewise the other parts: + +.. code-block:: python + + >>> ver = semver.VersionInfo.parse("10.3.2-pre.5+build.10") + >>> ver[0], ver[1], ver[2], ver[3], ver[4] + (10, 3, 2, 'pre.5', 'build.10') + +If you need more than one part at the same time, use the slice notation: + +.. code-block:: python + + >>> ver[0:3] + (10, 3, 2) + +Or, as an alternative, you can pass a :func:`slice` object: + +.. code-block:: python + + >>> sl = slice(0,3) + >>> ver[sl] + (10, 3, 2) + +Negative numbers or undefined parts raise an :class:`IndexError` exception: + +.. code-block:: python + + >>> ver = semver.VersionInfo.parse("10.3.2") + >>> ver[3] + Traceback (most recent call last): + ... + IndexError: Version part undefined + >>> ver[-2] + Traceback (most recent call last): + ... + IndexError: Version index cannot be negative + .. _sec.replace.parts: Replacing Parts of a Version diff --git a/semver.py b/semver.py index a5739b16..b3961083 100644 --- a/semver.py +++ b/semver.py @@ -446,6 +446,42 @@ def __gt__(self, other): def __ge__(self, other): return self.compare(other) >= 0 + def __getitem__(self, index): + """ + self.__getitem__(index) <==> 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 :func:`slice` object + :raises: IndexError, if index is beyond the range or a part is None + :return: the requested part of the version at position index + + >>> ver = semver.VersionInfo.parse("3.4.5") + >>> ver[0], ver[1], ver[2] + (3, 4, 5) + """ + if isinstance(index, int): + index = slice(index, index + 1) + + if ( + isinstance(index, slice) + and (index.start is None or index.start < 0) + and (index.stop is None or index.stop < 0) + ): + raise IndexError("Version index cannot be negative") + + # Could raise IndexError: + part = tuple(filter(None, self.to_tuple()[index])) + + if len(part) == 1: + part = part[0] + if not part: + raise IndexError("Version part undefined") + return part + def __repr__(self): s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) return "%s(%s)" % (type(self).__name__, s) diff --git a/test_semver.py b/test_semver.py index 5daf3f1a..6c7eea49 100644 --- a/test_semver.py +++ b/test_semver.py @@ -695,6 +695,77 @@ def test_should_be_able_to_use_integers_as_prerelease_build(): assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, "4", "5") +@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), + ("1.2.3", 10), + ("1.2.3", slice(-3)), + ], +) +def test_version_info_should_throw_index_error(version, index): + version_info = VersionInfo.parse(version) + with pytest.raises(IndexError): + version_info[index] + + @pytest.mark.parametrize( "cli,expected", [