Repository: cogent3/cogent3
Pull request #1942 (2024-07-05 01:21:07)

TST: Port Sequence unittest classes
- 3 new xfails
- 3 xpasses removed
- @KatherineCaley could you check if any old ArraySeq-related tests in the following classes should be ported? If not, I think it's the last of the Sequence tests!
    - DnaSequenceTests
    - CodonSequenceTests
    - DnaSequenceGapTests
    - SequenceIntegrationTests
    - ModelSequenceSequenceTests




Patch:
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index af1a7b219..a7c2c0201 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -802,6 +802,11 @@ class Sequence:
                 )
         other_seq = str(other)
 
+        if not self.moltype.most_degen_alphabet().is_valid(str(other)):
+            raise new_alphabet.AlphabetError(
+                f"Invalid sequence characters in other for moltype={self.moltype.label}"
+            )
+
         # If two sequences with the same name are being added together the name should not be None
         if type(other) == type(self):
             name = self.name if self.name == other.name else None


Test patch:
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index 4a60dc4c6..6d740a78b 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -1,5 +1,6 @@
 import json
 import re
+import typing
 
 from pickle import dumps
 
@@ -174,6 +175,50 @@ def test_sequence_strip_degenerate(seq, expect):
     assert got == expect
 
 
+def test_add():
+    mt = new_moltype.DNA
+    seq1 = mt.make_seq(seq="AAA")
+    seq2 = mt.make_seq(seq="CCC")
+    got = seq1 + seq2
+    assert got == "AAACCC"
+    assert got.moltype == mt
+
+
+def test_add_bad():
+    mt = new_moltype.DNA
+    seq1 = mt.make_seq(seq="AAA")
+    with pytest.raises(new_alphabet.AlphabetError):
+        _ = seq1 + "s8d3j%31 s-']"
+
+
+@pytest.mark.parametrize(
+    "moltype,label",
+    [
+        (new_moltype.ASCII, "text"),
+        (new_moltype.BYTES, "bytes"),
+        (new_moltype.DNA, "dna"),
+    ],
+)
+def test_get_type(moltype, label):
+    seq = moltype.make_seq(seq="ARCGT")
+    got = seq.get_type()
+    assert got == label
+
+
+@pytest.mark.xfail(
+    reason="cannot iterate over SeqView and KeyError with unambiguous character"
+)
+@pytest.mark.parametrize(
+    "s, expect",
+    [("ARC", [("A",), ("A", "G"), ("C",)]), ("AGC", [("A",), ("G",), ("C",)])],
+)
+def test_resolved_ambiguities(s, expect):
+    mt = new_moltype.DNA
+    seq = mt.make_seq(seq=s)
+    got = seq.resolved_ambiguities()
+    assert got == expect
+
+
 @pytest.mark.parametrize(
     "seq, expect",
     (
@@ -264,9 +309,6 @@ def test_sequence_is_gapped():
     assert new_moltype.RNA.make_seq(seq="CAGU-").is_gapped()
 
 
-@pytest.mark.xfail(
-    reason="AttributeError: 'MolType' object has no attribute 'count_gaps'"
-)
 def test_sequence_is_gap():
     """Sequence is_gap should return True if char is a valid gap char"""
     r = new_moltype.RNA.make_seq(seq="ACGUCAGUACGUCAGNRCGAUYRNRYRN")
@@ -472,6 +514,45 @@ def test_can_mismatch():
     assert not new_moltype.RNA.make_seq(seq="U--").can_mismatch("U--")
 
 
+@pytest.mark.parametrize(
+    "size, expect",
+    [
+        (60, ">even\nTCAGAT\n"),  # default block_size
+        (2, ">even\nTC\nAG\nAT\n"),
+        (4, ">even\nTCAG\nAT\n"),
+    ],
+)
+def test_to_fasta_even(size, expect):
+    mt = new_moltype.DNA
+    seq = mt.make_seq(seq="TCAGAT", name="even")
+    got = seq.to_fasta(block_size=size)
+    assert got == expect
+
+
+@pytest.mark.parametrize(
+    "size, expect",
+    [
+        (60, ">odd\nTCAGATAAA\n"),  # default block_size
+        (2, ">odd\nTC\nAG\nAT\nAA\nA\n"),
+        (4, ">odd\nTCAG\nATAA\nA\n"),
+    ],
+)
+def test_to_fasta_odd(size, expect):
+    mt = new_moltype.DNA
+    seq = mt.make_seq(seq="TCAGATAAA", name="odd")
+    got = seq.to_fasta(block_size=size)
+    assert got == expect
+
+
+@pytest.mark.xfail(reason="no Sequence.to_phylip()")
+def test_to_phylip():
+    mt = new_moltype.DNA
+    seq = mt.make_seq(seq="TCAGAT", name="seq_name")
+    got = seq.to_phylip()
+    expect = f"seq_name{' '*27}TCAGAT"
+    assert got == expect
+
+
 @pytest.mark.parametrize("start", (None, 0, 1, 10, -1, -10))
 @pytest.mark.parametrize("stop", (None, 10, 8, 1, 0, -1, -11))
 @pytest.mark.parametrize("step", (None, 1, 2, -1, -2))
@@ -1073,6 +1154,33 @@ def test_seqview_repr():
     assert repr(view) == expected
 
 
+@pytest.mark.parametrize("k", range(1, 7))
+def test_iter_kmers_returns_generator(k):
+    orig = "TCAGGA"
+    mt = new_moltype.DNA
+    r = mt.make_seq(seq=orig)
+    got = r.iter_kmers(k=k)
+    assert isinstance(got, typing.Generator)
+
+
+def test_iter_kmers_empty():
+    orig = ""
+    mt = new_moltype.DNA
+    r = mt.make_seq(seq=orig)
+    got = r.iter_kmers(k=1)
+    assert not list(got)
+
+
+@pytest.mark.parametrize("k", (0, -1, 1.1))
+def test_iter_kmers_handles_invalid(k):
+    orig = ""
+    mt = new_moltype.DNA
+    r = mt.make_seq(seq=orig)
+    got = r.iter_kmers(k=k)
+    with pytest.raises(ValueError):
+        list(got)
+
+
 @pytest.mark.parametrize(
     "k, strict, expect",
     [
@@ -1531,25 +1639,17 @@ def test_annotate_gff_nested_features(DATA_DIR):
     assert tuple(str(ex.get_slice()) for ex in exons) == exon_seqs
 
 
-@pytest.mark.xfail(
-    reason="AttributeError: 'MolType' object has no attribute 'coerce_str'"
-)
 def test_to_moltype_dna():
     """to_moltype("dna") ensures conversion from T to U"""
     seq = new_moltype.DNA.make_seq(seq="AAAAGGGGTTT", name="seq1")
     rna = seq.to_moltype("rna")
-
     assert "T" not in rna
 
 
-@pytest.mark.xfail(
-    reason="AttributeError: 'MolType' object has no attribute 'coerce_str'"
-)
 def test_to_moltype_rna():
     """to_moltype("rna") ensures conversion from U to T"""
     seq = new_moltype.RNA.make_seq(seq="AAAAGGGGUUU", name="seq1")
     rna = seq.to_moltype("dna")
-
     assert "U" not in rna
 
 
diff --git a/tests/test_core/test_sequence.py b/tests/test_core/test_sequence.py
index 674c444ad..f464e636d 100644
--- a/tests/test_core/test_sequence.py
+++ b/tests/test_core/test_sequence.py
@@ -961,7 +961,7 @@ class SequenceTests(TestCase):
 class SequenceSubclassTests(TestCase):
     SequenceClass = Sequence
 
-    def test_DnaSequence(self):
+    def test_DnaSequence(self):  # ported
         """DnaSequence should behave as expected"""
         x = DnaSequence("tcag")
         # note: no longer preserves case
@@ -974,13 +974,13 @@ class SequenceSubclassTests(TestCase):
         self.assertRaises(AlphabetError, x.__add__, "z")
         self.assertEqual(DnaSequence("TTTAc").rc(), "GTAAA")
 
-    def test_get_type(self):
+    def test_get_type(self):  # ported
         """returns moltype label"""
         for moltype in ("text", "dna", "bytes"):
             seq = get_moltype(moltype).make_seq("ARCGT")
             self.assertEqual(seq.get_type(), moltype)
 
-    def test_resolved_ambiguities(self):
+    def test_resolved_ambiguities(self):  # ported
         seq = get_moltype("dna").make_seq("ARC")
         got = seq.resolved_ambiguities()
         self.assertEqual(got, [("A",), ("A", "G"), ("C",)])
@@ -989,7 +989,9 @@ class SequenceSubclassTests(TestCase):
         got = seq.resolved_ambiguities()
         self.assertEqual(got, [("A",), ("G",), ("C",)])
 
-    def test_iter_kmers(self):
+    def test_iter_kmers(
+        self,
+    ):  # ported tests for Generator and empty orig, dupes of test_get_kmers...()
         """correctly yield all k-mers"""
         from typing import Generator
 
@@ -1008,7 +1010,7 @@ class SequenceSubclassTests(TestCase):
         got = list(r.iter_kmers(k=1))
         self.assertEqual(got, [])
 
-    def test_iter_kmers_handles_invalid(self):
+    def test_iter_kmers_handles_invalid(self):  # ported
         """raise exceptions on invalid input to iter_kmers"""
         orig = "TCAGGA"
         r = self.SequenceClass(orig)
@@ -1016,7 +1018,7 @@ class SequenceSubclassTests(TestCase):
             with self.assertRaises(ValueError):
                 _ = list(r.iter_kmers(k))
 
-    def test_get_kmers(self):
+    def test_get_kmers(self):  # not porting, dupes of test_get_kmers..()
         """returns a list of k-mers"""
         orig = "TCAGGA"
         r = self.SequenceClass(orig)
@@ -1028,12 +1030,12 @@ class SequenceSubclassTests(TestCase):
 
 
 # TODO move methods of this class onto the single class that inherits from it!
-class ModelSequenceTests(object):
+class ModelSequenceTests(object):  # ported
     """base class for tests of specific ArraySequence objects."""
 
     SequenceClass = None  # override in derived classes
 
-    def test_to_fasta(self):
+    def test_to_fasta(self):  # ported
         """Sequence to_fasta() should return Fasta-format string"""
         even = "TCAGAT"
         odd = even + "AAA"
@@ -1046,13 +1048,15 @@ class ModelSequenceTests(object):
         # check that changing the linewrap again works
         self.assertEqual(even_dna.to_fasta(block_size=4), ">even\nTCAG\nAT\n")
 
-    def test_to_phylip(self):
+    def test_to_phylip(self):  # ported
         """Sequence to_phylip() should return one-line phylip string"""
         s = self.SequenceClass("ACG", name="xyz")
         self.assertEqual(s.to_phylip(), "xyz" + " " * 27 + "ACG")
 
 
-class DnaSequenceTests(ModelSequenceTests, TestCase):
+class DnaSequenceTests(
+    ModelSequenceTests, TestCase
+):  # not porting: not supporting ArraySeqs
     class SequenceClass(ArrayNucleicAcidSequence):
         alphabet = DNA.alphabets.base
 
@@ -1068,7 +1072,9 @@ class DnaSequenceTests(ModelSequenceTests, TestCase):
         self.assertEqual(str(r), orig)
 
 
-class CodonSequenceTests(SequenceTests, TestCase):
+class CodonSequenceTests(
+    SequenceTests, TestCase
+):  # not porting: not supporting ArraySeqs
     class SequenceClass(ArrayCodonSequence):
         alphabet = DNA.alphabets.base**3
 
@@ -1084,7 +1090,7 @@ class CodonSequenceTests(SequenceTests, TestCase):
         self.assertEqual(str(r), orig)
 
 
-class DnaSequenceGapTests(TestCase):
+class DnaSequenceGapTests(TestCase):  # not porting: not supporting ArraySeqs
     """Tests of gapped DNA sequences."""
 
     class SequenceClass(ArrayNucleicAcidSequence):
@@ -1128,7 +1134,7 @@ class DnaSequenceGapTests(TestCase):
         self.assertEqual(got.name, "blah")
 
 
-class SequenceIntegrationTests(TestCase):
+class SequenceIntegrationTests(TestCase):  # not porting: not supporting ArraySeqs
     """Should be able to convert regular to model sequences, and back"""
 
     def test_regular_to_model(self):
@@ -1180,7 +1186,7 @@ class SequenceIntegrationTests(TestCase):
         self.assertEqual(str(r.to_dna()), "TTTCGT")
 
 
-class ModelSequenceTests(SequenceTests):
+class ModelSequenceTests(SequenceTests):  # not porting: not supporting ArraySeqs
     """Tests of the ArraySequence class's inheritance of SequenceI."""
 
     SEQ = ArraySequence