Repository: fulcrumgenomics/fgpyo (language: python)

Issue:
Feature request: add __get_item__() to Cigar class
This will enable users to access the i-th element of `cigar` with `cigar[i]` rather than `cigar.elements[i]`




Discussion / hints:
Should we instead use `__getitem__` and `__setitem__` and subclass `list`.
I dont' think so. currently it is frozen and uses a tuple which means that it's immutable. changing that seems like a much bigger change in the API.
Agree that it should remain immutable ... and also like the idea of implementing `__get_item__()`.

Fix patch:
diff --git a/fgpyo/sam/__init__.py b/fgpyo/sam/__init__.py
index 9355fd6c..20a7471a 100644
--- a/fgpyo/sam/__init__.py
+++ b/fgpyo/sam/__init__.py
@@ -111,6 +111,20 @@
     >>> [e.operator.is_indel for e in cigar.elements]
     [False, True, False, True, False]
 
+Any particular tuple can be accessed directly with its index (and works with negative indexes
+and slices):
+
+    >>> cigar = Cigar.from_cigarstring("50M2D5M2I10S")
+    >>> cigar[0].length
+    50
+    >>> cigar[1].operator
+    <CigarOp.D: (2, 'D', False, True)>
+    >>> cigar[-1].operator
+    <CigarOp.S: (4, 'S', True, False)>
+    >>> tuple(x.operator.character for x in cigar[1:3])
+    ('D','M')
+    >>> tuple(x.operator.character for x in cigar[-2:])
+    ('I', 'S')
 
 Examples of parsing the SA tag and individual supplementary alignments
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -543,6 +557,24 @@ def length_on_target(self) -> int:
         """Returns the length of the alignment on the target sequence."""
         return sum([elem.length_on_target for elem in self.elements])
 
+    def __getitem__(
+        self, index: Union[int, slice]
+    ) -> Union[CigarElement, Tuple[CigarElement, ...]]:
+        """Returns the cigar element indexed by index
+
+        Arguments:
+            index: int The index of the requested cigar element(s)
+
+        Returns: CigarElement or Tuple[CigarElement,...]
+            The element(s) selected by index
+
+        Throws:
+            TypeError if index isn't an integer or a slice
+            IndexError: if there's no such element
+
+        """
+        return self.elements[index]
+
 
 @attr.s(frozen=True, auto_attribs=True)
 class SupplementaryAlignment:


Test patch:
diff --git a/fgpyo/sam/tests/test_cigar.py b/fgpyo/sam/tests/test_cigar.py
new file mode 100644
index 00000000..5c43ec0f
--- /dev/null
+++ b/fgpyo/sam/tests/test_cigar.py
@@ -0,0 +1,33 @@
+import pytest
+
+from fgpyo.sam import Cigar
+
+cigar = Cigar.from_cigarstring("1M4D45N37X23I11=")
+
+
+@pytest.mark.parametrize("index", range(-len(cigar.elements), len(cigar.elements)))
+def test_direct_access(index: int) -> None:
+    assert cigar[index] == cigar.elements[index]
+    assert cigar[index:] == cigar.elements[index:]
+    assert cigar[:index] == cigar.elements[:index]
+    assert cigar[index:-1] == cigar.elements[index:-1]
+
+
+@pytest.mark.parametrize(
+    "index",
+    [
+        -7,
+        -100,
+        6,
+        100,
+    ],
+)
+def test_bad_index_raises_index_error(index: int) -> None:
+    with pytest.raises(IndexError):
+        cigar[index]
+
+
+@pytest.mark.parametrize("index", ["a", "b", (1, 2)])
+def test_bad_index_raises_type_error(index: int) -> None:
+    with pytest.raises(TypeError):
+        cigar[index]