Repository: cogent3/cogent3
Pull request #1826 (2024-04-25 05:24:10)

MAINT: improves testing of edge cases


Patch:
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index a19bf5170..7acbfeeb8 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -522,6 +522,9 @@ class TerminalPadding(_LostSpan):
         return f"?{self.length}?"
 
 
+SpanTypes = Union[Span, _LostSpan]
+
+
 class Map:  # pragma: no cover
     """A map holds a list of spans."""
 
@@ -1201,7 +1204,7 @@ O = tuple[numpy.ndarray, Sequence]
 
 
 def spans_to_gap_coords(
-    indel_spans: list[Union[Span, LostSpan]], dtype: Union[type, str] = numpy.int32
+    indel_spans: list[SpanTypes], dtype: Union[type, str] = numpy.int32
 ) -> tuple[numpy.ndarray, numpy.ndarray]:
     """returns coordinates of sequence gaps
 
@@ -1299,8 +1302,10 @@ class IndelMap(MapABC):
         self.cum_gap_lengths.flags.writeable = False
         self._serialisable.pop("gap_lengths", None)
 
+    T = Union[list[SpanTypes], tuple[SpanTypes]]
+
     @classmethod
-    def from_spans(cls, spans, parent_length, termini_unknown=False):
+    def from_spans(cls, spans: T, parent_length: int, termini_unknown: bool = False):
         gap_pos, cum_lengths = spans_to_gap_coords(spans)
         return cls(
             gap_pos=gap_pos,
@@ -1604,7 +1609,7 @@ class IndelMap(MapABC):
         starts, ends = _gap_spans(self.gap_pos, self.cum_gap_lengths)
         return numpy.array([starts, ends]).T.flatten()[:-1].tolist()
 
-    def nongap(self) -> Iterator[Union[Span, LostSpan]]:
+    def nongap(self) -> Iterator[SpanTypes]:
         """ungappeed segments in this map in aligned coordinates"""
         # we want to know the coordinates of the ungapped segments on
         # the aligned sequence. The gap_pos attribute is in sequence
@@ -1627,7 +1632,7 @@ class IndelMap(MapABC):
             yield Span(prev_pos, len(self))
 
     @property
-    def spans(self) -> Iterator[Union[Span, LostSpan]]:
+    def spans(self) -> Iterator[SpanTypes]:
         """generator of spans"""
         if not self.num_gaps:
             yield Span(0, self.parent_length)
@@ -1698,9 +1703,18 @@ class IndelMap(MapABC):
         return numpy.array([self.gap_pos, cum_lengths]).T.tolist()
 
     def get_gap_align_coordinates(self) -> NDArray[int]:
-        """returns [(gap start, gap end), ...] in alignment indices"""
+        """returns [(gap start, gap end), ...] in alignment indices
+
+        Returns
+        -------
+        A 2D numpy array of integers. If the result is empty, it still
+        has shape (0, 2).
+        """
         starts, ends = _gap_spans(self.gap_pos, self.cum_gap_lengths)
-        return numpy.array([starts, ends]).T
+        result = numpy.array([starts, ends]).T
+        if not len(result):
+            result = result.reshape((0, 2))
+        return result
 
     def merge_maps(self, other, parent_length: Optional[int] = None):
         """merge gaps of other with self
@@ -1900,8 +1914,10 @@ class FeatureMap(MapABC):
         self._spans = tuple(spans)
         self.length = posn
 
+    T = Union[list[SpanTypes], tuple[SpanTypes]]
+
     @classmethod
-    def from_spans(cls, spans, parent_length):
+    def from_spans(cls, spans: T, parent_length: int, **kwargs):
         return cls(spans=spans, parent_length=parent_length)
 
     def __len__(self):
diff --git a/src/cogent3/draw/dotplot.py b/src/cogent3/draw/dotplot.py
index 6c0ecf73f..5bd0b2f1b 100644
--- a/src/cogent3/draw/dotplot.py
+++ b/src/cogent3/draw/dotplot.py
@@ -70,7 +70,8 @@ def get_align_coords(map1, map2, aligned=False) -> MatchedSeqPaths:
     """sequence coordinates of aligned segments"""
     from cogent3.align.pycompare import segment
 
-    assert len(map1) == len(map2), "aligned sequences not equal length"
+    if aligned:
+        assert len(map1) == len(map2), "aligned sequences not equal length"
 
     # we get the gap coordinates in alignment indices for both sequences
     # sorting this allows us to trivially identify the alignment indices of


Test patch:
diff --git a/tests/test_core/test_location.py b/tests/test_core/test_location.py
index ece8c9a84..45fdc2915 100644
--- a/tests/test_core/test_location.py
+++ b/tests/test_core/test_location.py
@@ -1037,3 +1037,41 @@ def test_gap_coords_to_map():
     # and gaps outside sequence
     with pytest.raises(ValueError):
         gap_coords_to_map({20: 1}, len(seq))
+
+
+@pytest.mark.parametrize(
+    "seq, expected",
+    [
+        ("ACGTACGT", []),
+        ("----ACGTACGT----", [(0, 4), (12, 16)]),
+        ("ACGT----ACGT", [4, 8]),
+        ("----", [[0, 4]]),
+        ("----ACGT", [[0, 4]]),
+        ("ACGTACGT----", [(8, 12)]),
+    ],
+)
+def test_get_gap_align_coordinates1(seq, expected):
+    expected = numpy.array(expected, dtype=int)
+    if not len(expected):
+        expected = expected.reshape((0, 2))
+    im, _ = make_seq(seq).parse_out_gaps()
+    result = im.get_gap_align_coordinates()
+    assert (result == expected).all(), f"{expected=}, {result=}"
+
+
+@pytest.mark.parametrize(
+    "seq, expected",
+    [
+        ("", []),
+        ("A", []),
+        ("-", [[0, 1]]),
+    ],
+)
+def test_get_gap_align_coordinates_edge_cases(seq, expected):
+    expected = numpy.array(expected, dtype=int)
+    if not len(expected):
+        expected = expected.reshape((0, 2))
+
+    im, _ = make_seq(seq).parse_out_gaps()
+    result = im.get_gap_align_coordinates()
+    assert (result == expected).all(), f"{expected=}, {result=}"
diff --git a/tests/test_draw/test_dotplot.py b/tests/test_draw/test_dotplot.py
index 334961bf9..a113c039b 100644
--- a/tests/test_draw/test_dotplot.py
+++ b/tests/test_draw/test_dotplot.py
@@ -166,3 +166,12 @@ def test_aligned_path():
         [0, 3, None, 4, 4, None, 5, 5, None, 6, 7, None, 8, 9],
         [0, 3, None, 10, 10, None, 15, 15, None, 18, 19, None, 22, 23],
     )
+
+
+def test_dotplot_unaligned():
+    seqs = make_unaligned_seqs(dict(a="ACGGT", b="CGTT"), moltype="dna")
+    dp = seqs.dotplot(window=3, k=2)
+    assert dp
+    # trigger building traces
+    _ = dp.figure
+    assert len(dp.traces[0].x)