Repository: python-semver/python-semver
Pull request #243 (2020-04-26 09:43:19)

Implement __getitem__ for #138
This PR is a fresh implementation of #138 and contains:

* Add `__getitem__` to `VersionInfo` class
* Add test cases
* Add user documentation
* Extend CHANGELOG

----

@tlaferriere, @bittner: As pr #138 was a bit older and a lot of things have changed now, I've created a fresh new branch so we won't get any conflicts. On top of that, I've added the implementation. If you would like to have a look, that would be great. :+1: 


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ddefadc..b731db8 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 91e4d06..63b18f6 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 <semver.VersionInfo>` object allows to access its data through
+the magic method :func:`__getitem__ <semver.VersionInfo.__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 a5739b1..b396108 100644
--- a/semver.py
+++ b/semver.py
@@ -446,6 +446,42 @@ build='build.10')
     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)


Test patch:
diff --git a/test_semver.py b/test_semver.py
index 5daf3f1..6c7eea4 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",
     [