Repository: cogent3/cogent3
Pull request #1850 (2024-05-02 05:18:57)

Fix `__repr__` handling of limit in Alignment/SeqCol related objects
Also adds tests for each object's `__repr__`


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 26b577c5f..a7abe6c0e 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -564,6 +564,11 @@ class _SequenceCollectionBase:
         if isinstance(data, (typing.Generator, typing.Iterator)):
             data = tuple(data)
 
+        if len(data) == 0:
+            raise ValueError(
+                f"{self.__class__.__name__} must take at least one sequence."
+            )
+
         if moltype is None:
             moltype = _moltype_from_data(data)
         moltype = cogent3.get_moltype(moltype or self.moltype)
@@ -2157,9 +2162,6 @@ class SequenceCollection(_SequenceCollectionBase):
         return dist_calc_app(self)
 
     def __repr__(self):
-        if len(self.names) == 0:
-            return f"0x () {self.moltype.get_type()} seqcollection"
-
         seqs = []
         limit = 10
         delimiter = ""
@@ -2175,7 +2177,7 @@ class SequenceCollection(_SequenceCollectionBase):
         for name in repr_seq_names:
             elts = list(str(self.named_seqs[name])[: limit + 1])
             if len(elts) > limit:
-                elts.append("...")
+                elts[-1] = "..."
             seqs.append(f"{name}[{delimiter.join(elts)}]")
 
         if len(self.names) > 2:
@@ -2634,7 +2636,7 @@ class AlignmentI(object):
                 break
             elts = list(str(self.named_seqs[name])[: limit + 1])
             if len(elts) > limit:
-                elts.append("...")
+                elts[-1] = "..."
             seqs.append(f"{name}[{delimiter.join(elts)}]")
         seqs = ", ".join(seqs)
 
@@ -4307,6 +4309,9 @@ class ArrayAlignment(AlignmentI, _SequenceCollectionBase):
             names = [self.names[i] for i in seqs]
         else:
             names = self.names
+        if not len(names):
+            return None
+
         return self.__class__(
             data.T,
             list(map(str, names)),
diff --git a/src/cogent3/evolve/coevolution.py b/src/cogent3/evolve/coevolution.py
index 79f874453..877a2bee1 100755
--- a/src/cogent3/evolve/coevolution.py
+++ b/src/cogent3/evolve/coevolution.py
@@ -622,7 +622,9 @@ def get_subalignments(aln, position, selections):
     result = []
     for s in aln.alphabet.to_indices(selections):
         seqs_to_keep = nonzero(aln.array_seqs[:, position] == s)[0]
-        result.append(aln.get_sub_alignment(seqs=seqs_to_keep))
+        sub_align = aln.get_sub_alignment(seqs=seqs_to_keep)
+        if sub_align is not None:
+            result.append(sub_align)
     return result
 
 


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index 5000b7c56..264cc79ea 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -3705,23 +3705,23 @@ def test_slice_aligned_featuremap_multi_spans():
 
 def test_sequence_collection_repr():
     data = {
-        "ENSMUSG00000056468": "GCCAGGGGGGAAAGGGAGAA",
+        "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
         "ENSMUSG00000039616": "GCCCTTCAAATTT",
     }
     seqs = SequenceCollection(data=data, moltype=DNA)
     assert (
         repr(seqs)
-        == "2x (ENSMUSG00000039616[GCCCTTCAAAT...], ENSMUSG00000056468[GCCAGGGGGGA...]) dna seqcollection"
+        == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
     )
 
     data = {
         "ENSMUSG00000039616": "GCCCTTCAAATTT",
-        "ENSMUSG00000056468": "GCCAGGGGGGAAAGGGAGAA",
+        "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
     }
     seqs = SequenceCollection(data=data, moltype=DNA)
     assert (
         repr(seqs)
-        == "2x (ENSMUSG00000039616[GCCCTTCAAAT...], ENSMUSG00000056468[GCCAGGGGGGA...]) dna seqcollection"
+        == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
     )
 
     data = {
@@ -3730,6 +3730,12 @@ def test_sequence_collection_repr():
     seqs = SequenceCollection(data=data, moltype=DNA)
     assert repr(seqs) == "1x (a[TCGAT]) dna seqcollection"
 
+    data = {
+        "a": "TCGAT" * 2,
+    }
+    seqs = SequenceCollection(data=data, moltype=DNA)
+    assert repr(seqs) == "1x (a[TCGATTCGAT]) dna seqcollection"
+
     data = {
         "a": "A" * 11,
         "b": "B" * 3,
@@ -3738,8 +3744,59 @@ def test_sequence_collection_repr():
         "e": "E" * 8,
     }
     seqs = SequenceCollection(data=data, moltype=ASCII)
-    assert repr(seqs) == "5x (b[BBB], ..., d[DDDDDDDDDDD...]) text seqcollection"
+    assert repr(seqs) == "5x (b[BBB], ..., d[DDDDDDDDDD...]) text seqcollection"
+
+
+@pytest.mark.parametrize("cls", (ArrayAlignment, Alignment))
+def test_alignment_repr(cls):
+    data = {
+        "ENSMUSG00000056468": "GCCAGGGGGAAAA",
+        "ENSMUSG00000039616": "GCCCTTCAAATTT",
+    }
+    seqs = cls(data=data, moltype=DNA)
+    assert (
+        repr(seqs)
+        == "2 x 13 dna alignment: ENSMUSG00000056468[GCCAGGGGGA...], ENSMUSG00000039616[GCCCTTCAAA...]"
+    )
+
+    data = {
+        "ENSMUSG00000039616": "GCCCTTCAAATTT",
+        "ENSMUSG00000056468": "GCCAGGGGGAAAA",
+    }
+    seqs = cls(data=data, moltype=DNA)
+    assert (
+        repr(seqs)
+        == "2 x 13 dna alignment: ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]"
+    )
+
+    data = {
+        "a": "TCGAT",
+    }
+    seqs = cls(data=data, moltype=DNA)
+    assert repr(seqs) == "1 x 5 dna alignment: a[TCGAT]"
+
+    data = {
+        "a": "TCGAT" * 2,
+    }
+    seqs = cls(data=data, moltype=DNA)
+    assert repr(seqs) == "1 x 10 dna alignment: a[TCGATTCGAT]"
+
+    data = {
+        "a": "A" * 11,
+        "b": "B" * 11,
+        "c": "C" * 11,
+        "d": "D" * 11,
+        "e": "E" * 11,
+    }
+    seqs = cls(data=data, moltype=ASCII)
+    assert (
+        repr(seqs)
+        == "5 x 11 text alignment: a[AAAAAAAAAA...], b[BBBBBBBBBB...], c[CCCCCCCCCC...], ..."
+    )
+
 
-    data = {}
-    seqs = SequenceCollection(data=data, moltype=BYTES)
-    assert repr(seqs) == "0x () bytes seqcollection"
+@pytest.mark.parametrize("cls", (ArrayAlignment, Alignment, SequenceCollection))
+def test_empty_data(cls):
+    with pytest.raises(ValueError) as e:
+        _ = cls(())
+    assert str(e.value) == f"{cls.__name__} must take at least one sequence."
diff --git a/tests/test_evolve/test_coevolution.py b/tests/test_evolve/test_coevolution.py
index eafcf7ccc..54c6e1e67 100755
--- a/tests/test_evolve/test_coevolution.py
+++ b/tests/test_evolve/test_coevolution.py
@@ -1833,7 +1833,6 @@ class CoevolutionTests(TestCase):
             data={1: "AAAA", 2: "AAAC", 3: "AACG", 4: "ACCT", 5: "ACG-"},
             moltype=PROTEIN,
         )
-        sub_aln_0C = {}
         sub_aln_1A = ArrayAlignment(
             data={1: "AAAA", 2: "AAAC", 3: "AACG"}, moltype=PROTEIN
         )
@@ -1841,7 +1840,7 @@ class CoevolutionTests(TestCase):
         sub_aln_2G = ArrayAlignment(data={5: "ACG-"}, moltype=PROTEIN)
 
         self.assertEqual(get_subalignments(aln, 0, ["A"]), [sub_aln_0A])
-        self.assertEqual(get_subalignments(aln, 0, ["C"]), [sub_aln_0C])
+        self.assertEqual(get_subalignments(aln, 0, ["C"]), [])
         self.assertEqual(get_subalignments(aln, 1, ["A"]), [sub_aln_1A])
         self.assertEqual(get_subalignments(aln, 1, ["C"]), [sub_aln_1C])
         self.assertEqual(