Repository: cogent3/cogent3
Pull request #1895 (2024-06-10 01:23:14)

DEV: Porting sequence tests (trim_stop_codon) and new module methods
Porting `sequence` tests, mainly related to trim_stop_codons, and changes to `new_moltype` and `new_genetic_code` to get these working (+ tests). Adding review comments for things that need extra attention.


Patch:
diff --git a/src/cogent3/core/genetic_code.py b/src/cogent3/core/genetic_code.py
index 4d37a3c8a..82f03847f 100644
--- a/src/cogent3/core/genetic_code.py
+++ b/src/cogent3/core/genetic_code.py
@@ -289,7 +289,7 @@ class GeneticCode:
         fixed_codon = str(codon).upper().replace("U", "T")
         return fixed_codon in self.start_codons
 
-    def is_stop(self, codon):
+    def is_stop(self, codon):  # ported
         """Returns True if codon is a stop codon, False otherwise."""
         return self[codon] == "*"
 
diff --git a/src/cogent3/core/moltype.py b/src/cogent3/core/moltype.py
index e1018b0e9..4306f82d2 100644
--- a/src/cogent3/core/moltype.py
+++ b/src/cogent3/core/moltype.py
@@ -609,7 +609,7 @@ class MolType:
         self._serialisable = {k: v for k, v in locals().items() if k != "self"}
         self.gap = gap
         self.missing = missing
-        self.gaps = frozenset([gap, missing])
+        self.gaps = frozenset([gap, missing])  # ported
         if gaps:
             self.gaps = self.gaps.union(frozenset(gaps))
         self.label = label
diff --git a/src/cogent3/core/new_genetic_code.py b/src/cogent3/core/new_genetic_code.py
index d3f0f7454..ede9558e7 100644
--- a/src/cogent3/core/new_genetic_code.py
+++ b/src/cogent3/core/new_genetic_code.py
@@ -128,7 +128,8 @@ class GeneticCode:
         return hash(self.name)
 
     def __eq__(self, other):
-        return self.name == other.name
+        """Allows two GeneticCode objects to be compared to each other."""
+        return self.name == getattr(other, "name", None)
 
     @property
     def stop_codons(self) -> SetStr:
@@ -162,12 +163,6 @@ class GeneticCode:
         display.set_repr_policy(show_shape=False)
         return display._repr_html_()
 
-    def __eq__(self, other):
-        """Allows two GeneticCode objects to be compared to each other.
-        Two GeneticCode objects are equal if they have equal CodeSequences.
-        """
-        return str(self) == str(other)
-
     def __getitem__(self, item):
         """Returns amino acid corresponding to codon, or codons for an aa.
 
@@ -232,6 +227,10 @@ class GeneticCode:
         for strand, start in itertools.product(("+", "-"), range(3)):
             yield strand, start, self.translate(seq, start, rc=strand == "-")
 
+    def is_stop(self, codon):
+        """Returns True if codon is a stop codon, False otherwise."""
+        return self[codon] == "*"
+
 
 _mapping_cols = "ncbi_code_sequence", "ID", "name", "ncbi_start_codon_map"
 # code mappings are based on the product of bases in order TCAG
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index f05ed9358..1f67367b0 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -375,6 +375,7 @@ class MolType:
             if ambigs and gap
             else None
         )
+
         if complements:
             # assume we have a nucleic acid moltype
             dest = "".join(complements[c] for c in self.degen_gapped_alphabet)
@@ -424,6 +425,11 @@ class MolType:
         """monomers + gap + ambiguous characters"""
         return self._degen_gapped
 
+    @property
+    def gaps(self) -> frozenset:
+        gaps = [char for char in (self.gap, self.missing) if char is not None]
+        return frozenset(gaps)
+
     def is_valid(self, seq: StrORArray) -> bool:
         """checks against most degenerate alphabet"""
         alpha = next(
@@ -535,7 +541,6 @@ class MolType:
 
     @get_degenerate_positions.register
     def _(self, seq: numpy.ndarray, include_gap=True) -> numpy.ndarray:
-
         for index, val in enumerate(self.degen_gapped_alphabet):
             if include_gap and val in self.gap or val in self.ambiguities:
                 break
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index 6407fcaf2..e5e1e9631 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -1662,12 +1662,14 @@ class NucleicAcidSequenceMixin:
         Parameters
         ----------
         gc
-            valid input to cogent3.get_code(), a genetic code object, number
+            valid input to new_genetic_code.get_code(), a genetic code object, number
             or name
         strict
             If True, raises an exception if length not divisible by 3
         """
-        gc = get_code(gc)
+        from cogent3.core import new_genetic_code
+
+        gc = new_genetic_code.get_code(gc)
         _, s = self.parse_out_gaps()
 
         divisible_by_3 = len(s) % 3 == 0
@@ -1690,7 +1692,7 @@ class NucleicAcidSequenceMixin:
         Parameters
         ----------
         gc
-            valid input to cogent3.get_code(), a genetic code object, number
+            valid input to new_genetic_code.get_code(), a genetic code object, number
             or name
         strict
             If True, raises an exception if length not divisible by 3
@@ -1700,10 +1702,12 @@ class NucleicAcidSequenceMixin:
         If sequence contains gap characters, the result preserves the sequence
         length by adding gap characters at the end.
         """
+        from cogent3.core import new_genetic_code
+
         if not self.has_terminal_stop(gc=gc, strict=strict):
             return self
 
-        gc = get_code(gc)
+        gc = new_genetic_code.get_code(gc)
         m, s = self.parse_out_gaps()
 
         divisible_by_3 = len(s) % 3 == 0


Test patch:
diff --git a/tests/test_core/test_new_genetic_code.py b/tests/test_core/test_new_genetic_code.py
index 4bfd0a245..ed337e46a 100644
--- a/tests/test_core/test_new_genetic_code.py
+++ b/tests/test_core/test_new_genetic_code.py
@@ -20,6 +20,12 @@ def test_stop_codons():
     assert code.stop_codons == {"TAA", "TAG", "TGA"}
 
 
+@pytest.mark.parametrize("codons", ("TAA", "TAG", "TGA"))
+def test_is_stop_true(codons):
+    code = new_genetic_code.DEFAULT
+    assert code.is_stop(codons)
+
+
 def test_start_codons():
     code = new_genetic_code.DEFAULT
     dna = code.moltype.alphabet
@@ -75,3 +81,17 @@ def test_sizeframes():
     }
     got = set(code.sixframes(seq))
     assert got == expect
+
+
+@pytest.mark.parametrize("code_id", range(1, 5))
+def test_eq_true(code_id):
+    gc0 = new_genetic_code.get_code(code_id)
+    gc1 = new_genetic_code.get_code(code_id)
+    assert gc0 == gc1
+
+
+@pytest.mark.parametrize("code_id", range(1, 5))
+def test_eq_false(code_id):
+    gc0 = new_genetic_code.get_code(code_id)
+    gc1 = new_genetic_code.get_code(code_id + 1)
+    assert gc0 != gc1
diff --git a/tests/test_core/test_new_moltype.py b/tests/test_core/test_new_moltype.py
index bbe7d6f64..25f668202 100644
--- a/tests/test_core/test_new_moltype.py
+++ b/tests/test_core/test_new_moltype.py
@@ -1,7 +1,7 @@
 import numpy
 import pytest
 
-from cogent3.core import moltype, new_moltype
+from cogent3.core import moltype, new_moltype, new_sequence
 
 
 def test_make_pairs():
@@ -192,3 +192,43 @@ def test_get_degenerate_positions(data_type, moltype):
     got = moltype.get_degenerate_positions(seq)
     expect = numpy.array([], dtype=bool)
     assert numpy.array_equal(got, expect)
+
+
+@pytest.mark.parametrize(
+    "moltype",
+    (
+        new_moltype.ASCII,
+        new_moltype.DNA,
+        new_moltype.RNA,
+        new_moltype.PROTEIN,
+        new_moltype.PROTEIN_WITH_STOP,
+    ),
+)
+def test_gaps(moltype):
+    # TODO: fred, add new_moltype.BYTES
+    got = moltype.gaps
+    expect = frozenset({"-", "?"})
+    assert got == expect
+
+
+def test_gaps_none():
+    mt = new_moltype.MolType(
+        "no_gap",
+        monomers="".join(new_moltype.IUPAC_DNA_chars),
+        make_seq=new_sequence.DnaSequence,
+        gap=None,
+    )
+
+    got = mt.gaps
+    expect = frozenset({"?"})
+    assert got == expect
+
+    mt = new_moltype.MolType(
+        "no_missing",
+        monomers="".join(new_moltype.IUPAC_DNA_chars),
+        make_seq=new_sequence.DnaSequence,
+        missing=None,
+    )
+    got = mt.gaps
+    expect = frozenset({"-"})
+    assert got == expect
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index 0f9de00bb..917819479 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -1,6 +1,14 @@
+import re
+
+import numpy
 import pytest
 
-from cogent3.core import new_moltype, new_sequence
+from cogent3.core import (
+    new_alphabet,
+    new_genetic_code,
+    new_moltype,
+    new_sequence,
+)
 
 
 @pytest.mark.parametrize("name", ("dna", "rna", "protein", "protein_with_stop", "text"))
@@ -12,9 +20,6 @@ def test_moltype_make_seq(name):
     assert str(seq) == raw
 
 
-@pytest.mark.xfail(
-    reason="bytes CharAlphabet is not consistent in handling of string types"
-)
 def test_moltype_make_bytes_seq():
     raw = "ACGGA"
     name = "bytes"
@@ -24,7 +29,158 @@ def test_moltype_make_bytes_seq():
     assert str(seq) == raw
 
 
-# From test_sequence.py
+# Tests from test_sequence.py
+@pytest.mark.parametrize("gc,seq", ((1, "TCCTGA"), (1, "ACGTAA---"), (2, "TCCAGG")))
+def test_has_terminal_stop_true(gc, seq):
+    gc = new_genetic_code.get_code(gc)
+    seq = new_moltype.DNA.make_seq(seq=seq)
+    assert seq.has_terminal_stop(gc=gc)
+
+
+@pytest.mark.parametrize(
+    "gc,seq", ((1, "TCCAGG"), (2, "TCCAAA"), (1, "CCTGA"), (2, "CCAGG"))
+)
+def test_has_terminal_stop_false(gc, seq):
+    gc = new_genetic_code.get_code(gc)
+    seq = new_moltype.DNA.make_seq(seq=seq)
+    assert not seq.has_terminal_stop(gc=gc)
+
+
+@pytest.mark.xfail(
+    reason="cogent3.core.alphabet.AlphabetError: None length not divisible by 3"
+)
+def test_has_terminal_stop_strict():
+    gc = new_genetic_code.get_code(1)
+    seq = new_moltype.DNA.make_seq(seq="TCCAG")
+    with pytest.raises(new_alphabet.AlphabetError):
+        seq.has_terminal_stop(gc=gc, strict=True)
+
+
+@pytest.mark.parametrize(
+    "gc,seq",
+    (
+        (2, "TCCAGG"),
+        (1, "TAATGA"),
+        (1, "ACGTGA---"),
+        (1, "--AT-CTGA"),
+    ),
+)
+def test_trim_terminal_stop_true(gc, seq):
+    gc = new_genetic_code.get_code(gc)
+    expect = re.sub("(TGA|AGG)(?=[-]*$)", "---" if "-" in seq else "", seq)
+
+    seq = new_moltype.DNA.make_seq(seq=seq)
+    got = str(seq.trim_stop_codon(gc=gc))
+    assert got == expect
+
+
+@pytest.mark.parametrize("gc,seq", ((1, "T?CTGC"), (2, "TCCAAG")))
+def test_trim_terminal_stop_nostop(gc, seq):
+    gc = new_genetic_code.get_code(gc)
+    seq = new_moltype.DNA.make_seq(seq=seq)
+    got = seq.trim_stop_codon(gc=gc)
+    assert str(got) == str(seq)
+    # since there's no stop, we just return the same object
+    assert got is seq
+
+
+@pytest.mark.parametrize(
+    "gc,seq", ((1, "TCCAGG"), (2, "TCCAAA"), (1, "CCTGA"), (2, "CCAGG"))
+)
+def test_trim_terminal_stop_false(gc, seq):
+    gc = new_genetic_code.get_code(gc)
+    seq = new_moltype.DNA.make_seq(seq=seq)
+    assert str(seq.trim_stop_codon(gc=gc)) == str(seq)
+
+
+@pytest.mark.xfail(
+    reason="cogent3.core.alphabet.AlphabetError: None length not divisible by 3"
+)
+def test_trim_terminal_stop_strict():
+    gc = new_genetic_code.get_code(1)
+    seq = new_moltype.DNA.make_seq(seq="TCCAG")
+    with pytest.raises(new_alphabet.AlphabetError):
+        seq.trim_stop_codon(gc=gc, strict=True)
+
+
+@pytest.mark.parametrize("cast", (int, numpy.int32, numpy.int64, numpy.uint8))
+def test_index_a_seq(cast):
+    seq = new_moltype.DNA.make_seq(seq="TCCAG")
+    got = seq[cast(1)]
+    assert isinstance(got, new_sequence.Sequence)
+
+
+@pytest.mark.parametrize("cast", (float, numpy.float32))
+def test_index_a_seq_float_fail(cast):
+    seq = new_moltype.DNA.make_seq(seq="TCCAG")
+    index = cast(1)
+    with pytest.raises(TypeError):
+        seq[index]  # pylint: disable=W0104
+
+
+@pytest.mark.parametrize("moltype", ("dna", "protein"))
+def test_same_moltype(moltype):
+    moltype = new_moltype.get_moltype(moltype)
+    seq = moltype.make_seq(seq="TCCAG")
+    got = seq.to_moltype(moltype)
+    assert got is seq
+
+
+def test_gapped_by_map_segment_iter():
+    moltype = new_moltype.DNA
+    m, seq = moltype.make_seq(seq="-TCC--AG").parse_out_gaps()
+    g = list(seq.gapped_by_map_segment_iter(m, allow_gaps=True, recode_gaps=False))
+    assert g == ["-", "TCC", "--", "AG"]
+
+
+@pytest.mark.parametrize("rev", (False, True))
+@pytest.mark.parametrize("sliced", (False, True))
+@pytest.mark.parametrize("start_stop", ((None, None), (3, 7)))
+def test_copied_parent_coordinates(sliced, rev, start_stop):
+    orig_name = "orig"
+    seq = new_moltype.DNA.make_seq(seq="ACGGTGGGAC", name=orig_name)
+    start, stop = start_stop
+    start = start or 0
+    stop = stop or len(seq)
+    sl = slice(start, stop)
+    seq = seq[sl]
+    sliced_name = "sliced"
+    seq.name = sliced_name
+    assert seq.name == sliced_name
+    seq = seq.rc() if rev else seq
+    copied = seq.copy(sliced=sliced)
+    assert copied.name == sliced_name
+    # matches original
+    assert copied.parent_coordinates() == seq.parent_coordinates()
+    # and expected -- the coordinate name always reflects the underlying sequence
+    assert copied.parent_coordinates() == (orig_name, start, stop, -1 if rev else 1)
+
+
+@pytest.mark.parametrize("rev", (False, True))
+def test_parent_coordinates(rev):
+    seq = new_moltype.DNA.make_seq(seq="ACGGTGGGAC")
+    seq = seq[1:1]
+    seq = seq.rc() if rev else seq
+    seq.name = "sliced"  # this assignment does not affect the
+    # note that when a sequence has zero length, the parent seqid is None
+    assert seq.parent_coordinates() == (None, 0, 0, 1)
+
+
+@pytest.mark.parametrize(
+    "cls", (new_moltype.DNA.make_seq, new_sequence.SeqView, str, bytes)
+)
+def test_coerce_to_seqview(cls):
+    seq = "AC--GGTGGGAC"
+    seqid = "seq1"
+    if cls in (str, bytes):
+        s = bytes(seq, "utf8") if cls == bytes else seq
+        got = new_sequence._coerce_to_seqview(s, seqid)
+    else:
+        got = new_sequence._coerce_to_seqview(cls(seq=seq), seqid)
+    assert got.value == seq
+    assert isinstance(got, new_sequence.SeqView)
+
+
 def test_seqview_seqid():
     sv = new_sequence.SeqView(seq="ACGGTGGGAC")
     assert sv.seqid is None
diff --git a/tests/test_core/test_sequence.py b/tests/test_core/test_sequence.py
index 7eb81a778..11b86381d 100644
--- a/tests/test_core/test_sequence.py
+++ b/tests/test_core/test_sequence.py
@@ -2502,139 +2502,9 @@ def test_get_drawable(DATA_DIR):
         assert "(incomplete)" in trace.text
 
 
-@pytest.mark.parametrize("gc,seq", ((1, "TCCTGA"), (1, "ACGTAA---"), (2, "TCCAGG")))
-def test_has_terminal_stop_true(gc, seq):
-    gc = cogent3.get_code(gc)
-    seq = cogent3.make_seq(seq, moltype="dna")
-    assert seq.has_terminal_stop(gc=gc)
-
-
 @pytest.mark.parametrize(
-    "gc,seq", ((1, "TCCAGG"), (2, "TCCAAA"), (1, "CCTGA"), (2, "CCAGG"))
-)
-def test_has_terminal_stop_false(gc, seq):
-    gc = cogent3.get_code(gc)
-    seq = cogent3.make_seq(seq, moltype="dna")
-    assert not seq.has_terminal_stop(gc=gc)
-
-
-def test_has_terminal_stop_strict():
-    gc = cogent3.get_code(1)
-    seq = cogent3.make_seq("TCCAG", moltype="dna")
-    with pytest.raises(AlphabetError):
-        seq.has_terminal_stop(gc=gc, strict=True)
-
-
-@pytest.mark.parametrize(
-    "gc,seq",
-    (
-        (2, "TCCAGG"),
-        (1, "TAATGA"),
-        (1, "ACGTGA---"),
-        (1, "--AT-CTGA"),
-    ),
-)
-def test_trim_terminal_stop_true(gc, seq):
-    gc = cogent3.get_code(gc)
-    expect = re.sub("(TGA|AGG)(?=[-]*$)", "---" if "-" in seq else "", seq)
-
-    seq = cogent3.make_seq(seq, moltype="dna")
-    got = str(seq.trim_stop_codon(gc=gc))
-    assert got == expect
-
-
-@pytest.mark.parametrize("gc,seq", ((1, "T?CTGC"), (2, "TCCAAG")))
-def test_trim_terminal_stop_nostop(gc, seq):
-    gc = cogent3.get_code(gc)
-    seq = cogent3.make_seq(seq, moltype="dna")
-    got = seq.trim_stop_codon(gc=gc)
-    assert str(got) == str(seq)
-    # since there's no stop, we just return the same object
-    assert got is seq
-
-
-@pytest.mark.parametrize(
-    "gc,seq", ((1, "TCCAGG"), (2, "TCCAAA"), (1, "CCTGA"), (2, "CCAGG"))
-)
-def test_trim_terminal_stop_false(gc, seq):
-    gc = cogent3.get_code(gc)
-    seq = cogent3.make_seq(seq, moltype="dna")
-    assert str(seq.trim_stop_codon(gc=gc)) == str(seq)
-
-
-def test_trim_terminal_stop_strict():
-    gc = cogent3.get_code(1)
-    seq = cogent3.make_seq("TCCAG", moltype="dna")
-    with pytest.raises(AlphabetError):
-        seq.trim_stop_codon(gc=gc, strict=True)
-
-
-@pytest.mark.parametrize("cast", (int, numpy.int32, numpy.int64, numpy.uint8))
-def test_index_a_seq(cast):
-    seq = cogent3.make_seq("TCCAG", moltype="dna")
-    got = seq[cast(1)]
-    assert isinstance(got, Sequence)
-
-
-@pytest.mark.parametrize("cast", (float, numpy.float32))
-def test_index_a_seq_float_fail(cast):
-    seq = cogent3.make_seq("TCCAG", moltype="dna")
-    index = cast(1)
-    with pytest.raises(TypeError):
-        seq[index]
-
-
-@pytest.mark.parametrize("moltype", ("dna", "protein"))
-def test_same_moltype(moltype):
-    moltype = get_moltype(moltype)
-    seq = moltype.make_seq("TCCAG")
-    got = seq.to_moltype(moltype)
-    assert got is seq
-
-
-def test_gapped_by_map_segment_iter():
-    moltype = get_moltype("dna")
-    m, seq = moltype.make_seq("-TCC--AG").parse_out_gaps()
-    g = list(seq.gapped_by_map_segment_iter(m, allow_gaps=True, recode_gaps=False))
-    assert g == ["-", "TCC", "--", "AG"]
-
-
-@pytest.mark.parametrize("rev", (False, True))
-@pytest.mark.parametrize("sliced", (False, True))
-@pytest.mark.parametrize("start_stop", ((None, None), (3, 7)))
-def test_copied_parent_coordinates(sliced, rev, start_stop):
-    orig_name = "orig"
-    seq = DNA.make_seq("ACGGTGGGAC", name=orig_name)
-    start, stop = start_stop
-    start = start or 0
-    stop = stop or len(seq)
-    sl = slice(start, stop)
-    seq = seq[sl]
-    sliced_name = "sliced"
-    seq.name = sliced_name
-    assert seq.name == sliced_name
-    if rev:
-        seq = seq.rc()
-    copied = seq.copy(sliced=sliced)
-    assert copied.name == sliced_name
-    # matches original
-    assert copied.parent_coordinates() == seq.parent_coordinates()
-    # and expected -- the coordinate name always reflects the underlying sequence
-    assert copied.parent_coordinates() == (orig_name, start, stop, -1 if rev else 1)
-
-
-@pytest.mark.parametrize("rev", (False, True))
-def test_parent_coordinates(rev):
-    seq = DNA.make_seq("ACGGTGGGAC")
-    seq = seq[1:1]
-    if rev:
-        seq = seq.rc()
-    seq.name = "sliced"  # this assignment does not affect the
-    # note that when a sequence has zero length, the parent seqid is None
-    assert seq.parent_coordinates() == (None, 0, 0, 1)
-
-
-@pytest.mark.parametrize("cls", (Aligned, Sequence, SeqView, ArraySequence, str, bytes))
+    "cls", (Aligned, ArraySequence)
+)  # ported for Sequence, SeqView, str and bytes
 def test_coerce_to_seqview(cls):
     seq = "AC--GGTGGGAC"
     seqid = "seq1"