Repository: cogent3/cogent3
Pull request #1390 (2023-04-25 23:44:00)

Annotation refactor


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 49d92b4a2..a8da06ccd 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -65,7 +65,6 @@ from cogent3.core.genetic_code import get_code
 from cogent3.core.info import Info as InfoClass
 from cogent3.core.profile import PSSM, MotifCountsArray
 from cogent3.core.sequence import ArraySequence, Sequence, frac_same
-
 # which is a circular import otherwise.
 from cogent3.format.alignment import save_to_filename
 from cogent3.format.fasta import alignment_to_fasta
@@ -2285,7 +2284,7 @@ class Aligned:
         if sliced:
             span = self.map.get_covering_span()
             new_seq = type(new_seq)(
-                str(new_seq[span.start : span.end]), info=new_seq.info
+                str(new_seq[span.start : span.end]), info=new_seq.info, name=self.name
             )
             new_seq.annotation_db = None
             new_map = self.map.zeroed()
@@ -5093,4 +5092,4 @@ class Alignment(_Annotatable, AlignmentI, SequenceCollection):
 
         feature = {k: v for k, v in locals().items() if k != "self"}
 
-        return self.annotation_db.add_feature(**feature)
+        self.annotation_db.add_feature(**feature)
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index 0dfcd8028..9e5675123 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -913,6 +913,8 @@ class Sequence(_Annotatable, SequenceI):
         of strand of the current instance.
         """
 
+        # todo gah force start / stop to be positive
+
         if self._annotation_db is None:
             return None
 


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index 832be2c65..207f3bb4f 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -245,16 +245,6 @@ class SequenceCollectionBaseTests(object):
         self.b = Alignment(["AAA", "AAA"])
         self.c = SequenceCollection(["AAA", "AAA"])
 
-    def test_deepcopy(self):
-        """correctly deep copy aligned objects in an alignment"""
-        data = {"seq1": "ACGACGACG", "seq2": "ACGACGACG"}
-        seqs = self.Class(data)
-        copied = seqs.deepcopy(sliced=True)
-        assert_equal(seqs.to_rich_dict(), copied.to_rich_dict())
-        self.assertNotEqual(id(copied), id(seqs))
-        for name in seqs.names:
-            self.assertNotEqual(id(copied.named_seqs[name]), copied.named_seqs[name])
-
     def test_guess_input_type(self):
         """SequenceCollection  _guess_input_type should figure out data type correctly"""
         git = self.a._guess_input_type
@@ -3417,7 +3407,8 @@ def test_copy_annotations_incompat_type_fails(seqcoll_db, gb_db):
         seqcoll_db.copy_annotations({"a": "ACGGT"})
 
 
-def test_deepcopy():
+@pytest.mark.xfail(reason="todo gah to be implemented")
+def test_deepcopy_with_features():
     """correctly deepcopy Aligned objects in an alignment"""
     path = DATA_DIR / "brca1_5.paml"
     # generates an annotatable Alignment object
@@ -3469,3 +3460,17 @@ def test_deepcopy():
     assert len(new_seq.data) == len(aln.named_seqs["Human"].data)
     assert new_seq.data.is_annotated()
     assert len(new_seq.data.annotations) == 2
+
+
+@pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
+def test_deepcopy_aligned(cls):
+    """correctly deep copy aligned objects in an alignment"""
+    data = {"seq1": "ACGACGACG", "seq2": "ACGACGACG"}
+    seqs = cls(data)
+    copied = seqs.deepcopy(sliced=True)
+    orig_rd = seqs.to_rich_dict()
+    cpy_rd = copied.to_rich_dict()
+    assert orig_rd == cpy_rd
+    assert id(copied) != id(seqs)
+    for name in seqs.names:
+        assert id(copied.named_seqs[name]) != copied.named_seqs[name]
diff --git a/tests/test_core/test_annotation.py b/tests/test_core/test_annotation.py
index 7f590374f..f3e9c9355 100644
--- a/tests/test_core/test_annotation.py
+++ b/tests/test_core/test_annotation.py
@@ -47,35 +47,6 @@ class TestAnnotations(unittest.TestCase):
         self.seq = makeSampleSequence()
         self.aln = makeSampleAlignment()
 
-    def test_inherit_feature(self):
-        """should be able to subclass and extend _Feature"""
-
-        class NewFeat(_Feature):
-            def __init__(self, *args, **kwargs):
-                super(NewFeat, self).__init__(*args, **kwargs)
-
-            def newMethod(self):
-                if len(self.map.spans) > 1:
-                    as_one = self.as_one_span()  # should create new instance of NewFeat
-                    return as_one.newMethod()
-                return True
-
-        seq = DNA.make_seq("ACGTACGTACGT")
-        f = seq.add_annotation(
-            NewFeat, as_map([(1, 3), (5, 7)], len(seq)), type="gene", name="abcd"
-        )
-        self.assertEqual(type(f.as_one_span()), NewFeat)
-        self.assertEqual(type(f.get_shadow()), NewFeat)
-        f2 = seq.add_annotation(
-            NewFeat, as_map([(3, 5)], len(seq)), type="gene", name="def"
-        )
-
-        self.assertEqual(
-            type(seq.get_region_covering_all([f, f2], feature_class=NewFeat)), NewFeat
-        )
-        # now use the new method
-        f.newMethod()
-
     def test_slice_seq_with_annotations(self):
         newseq = self.seq[:5] + self.seq[10:]
         for annot_type in ["CDS", "5'UTR"]:
diff --git a/tests/test_core/test_maps.py b/tests/test_core/test_maps.py
index af7c1c44c..444565f99 100644
--- a/tests/test_core/test_maps.py
+++ b/tests/test_core/test_maps.py
@@ -2,8 +2,9 @@
 
 import unittest
 
+import pytest
+
 from cogent3 import DNA, make_aligned_seqs
-from cogent3.core.annotation import Feature, _Feature
 from cogent3.core.location import Map, Span
 
 
@@ -17,25 +18,6 @@ __email__ = "gavin.huttley@anu.edu.au"
 __status__ = "Production"
 
 
-def SimpleAnnotation(parent, locations, name):
-    return Feature(parent, "", name, locations)
-
-
-def annotate(parent, start, end, name):
-    annot = parent.add_annotation(SimpleAnnotation, locations=[(start, end)], name=name)
-    return annot
-
-
-def structure(a, depth=0):
-    annots = [structure(annot, depth + 1) for annot in a.annotations]
-    if not isinstance(a, _Feature):
-        return ("seq", len(a), annots)
-    elif annots:
-        return (a.name, repr(a.map), annots)
-    else:
-        return (a.name, repr(a.map))
-
-
 class MapTest(unittest.TestCase):
     """Testing annotation of and by maps"""
 
@@ -54,94 +36,49 @@ class MapTest(unittest.TestCase):
             ((10, 0), "[10:5, 5:0]"),
         ]:
             r = repr(Span(start, end, reverse=start > end).remap_with(map))
-            # print (start, end), r,
             if r != expected:
                 self.fail(repr((r, expected)))
 
-    def test_maps_on_maps(self):
-        seq = DNA.make_seq("ATCGATCGAT" * 5, name="base")
-        feat1 = annotate(seq, 10, 20, "fake")
-        annotate(feat1, 3, 5, "fake2")
-        annotate(seq, 1, 3, "left")
-
-        seq2 = seq[5:]
-        self.assertEqual(
-            structure(seq),
-            (
-                "seq",
-                50,
-                [("fake", "[10:20]/50", [("fake2", "[3:5]/10")]), ("left", "[1:3]/50")],
-            ),
-        )
-        self.assertEqual(
-            structure(seq2),
-            ("seq", 45, [("fake", "[5:15]/45", [("fake2", "[3:5]/10")])]),
-        )
-
     def test_get_by_annotation(self):
         seq = DNA.make_seq("ATCGATCGAT" * 5, name="base")
-        seq.add_annotation(Feature, "test_type", "test_label", [(5, 10)])
-        seq.add_annotation(Feature, "test_type", "test_label2", [(15, 18)])
+        seq.add_feature(biotype="test_type", name="test_label", spans=[(5, 10)])
+        seq.add_feature(biotype="test_type", name="test_label2", spans=[(15, 18)])
 
-        answer = list(seq.get_by_annotation("test_type"))
+        answer = list(seq.get_features(biotype="test_type"))
         self.assertEqual(len(answer), 2)
-        self.assertEqual(str(answer[0]), "TCGAT")
-        self.assertEqual(str(answer[1]), "TCG")
+        self.assertEqual(str(seq[answer[0]]), "TCGAT")
+        self.assertEqual(str(seq[answer[1]]), "TCG")
 
-        answer = list(seq.get_by_annotation("test_type", "test_label"))
+        answer = list(seq.get_features(biotype="test_type", name="test_label"))
         self.assertEqual(len(answer), 1)
-        self.assertEqual(str(answer[0]), "TCGAT")
+        self.assertEqual(str(seq[answer[0]]), "TCGAT")
 
         # test ignoring of a partial annotation
         sliced_seq = seq[:17]
-        answer = list(sliced_seq.get_by_annotation("test_type", ignore_partial=True))
+        answer = list(sliced_seq.get_features(biotype="test_type", allow_partial=False))
         self.assertEqual(len(answer), 1)
-        self.assertEqual(str(answer[0]), "TCGAT")
+        self.assertEqual(str(seq[answer[0]]), "TCGAT")
 
+    @pytest.mark.xfail(reason="todo: see comments in test")
     def test_get_by_seq_annotation(self):
         aln = make_aligned_seqs(
             data={"a": "ATCGAAATCGAT", "b": "ATCGA--TCGAT"}, array_align=False
         )
+        # original version was putting annotation directly on seq
         b = aln.get_seq("b")
-        b.add_annotation(Feature, "test_type", "test_label", [(4, 6)])
-
-        answer = aln.get_by_seq_annotation("b", "test_type")[0].to_dict()
+        f = aln.add_feature(
+            seqid="b", biotype="test_type", name="test_label", spans=[(4, 6)]
+        )
+        print(f)  # f should be a feature, but is currently None
+
+        # in the original get_by_seq_annotations(), inside the method it
+        # used self[feature.map.start : feature.map.end],
+        # so what was returned was an alignment slice compared to below,
+        # which is only non-gap positions of "b"
+        # todo gah add argument get_slice(strict=True), which means if parent is
+        # an alignment, only the columns from non-gap positions are returned.
+        # Argument has non effect on Sequence, OR add a new get_alignment_slice()
+        # method?
+        answer = list(aln.get_features(seqid="b", biotype="test_type"))
+        answer = answer[0].get_slice().to_dict()
         self.assertEqual(answer, {"b": "A--T", "a": "AAAT"})
-
-
-if 0:  # old, needs fixes
-    # Maps
-    a = Map([(10, 20)], parent_length=100)
-
-    for (desc, map, expected) in [
-        ("a ", a, "Map([10:20] on base)"),
-        ("i ", a.inverse(), "Map([-10-, 0:10, -80-] on Map([10:20] on base))"),
-        ("1 ", a[5:], "Map([5:10] on Map([10:20] on base))"),
-        ("1r", a[5:].relative_to(b), "Map([15:20] on base)"),
-        ("2 ", a[:5], "Map([0:5] on Map([10:20] on base))"),
-        ("2r", a[:5].relative_to(b), "Map([10:15] on base)"),
-        (
-            "r ",
-            a.relative_to(a[5:]),
-            "Map([-5-, 0:5] on Map([5:10] on Map([10:20] on base)))",
-        ),
-        (
-            "r ",
-            a[2:4].relative_to(a[2:6]),
-            "Map([0:2] on Map([2:6] on Map([10:20] on base)))",
-        ),
-        (
-            "r ",
-            a[2:4].relative_to(a[2:6][0:3]),
-            "Map([0:2] on Map([0:3] on Map([2:6] on Map([10:20] on base))))",
-        ),
-    ]:
-        print(desc, repr(map), end=" ")
-        if repr(map) == expected:
-            print()
-        else:
-            print(" <--- ", expected)
-            bad = True
-
-if __name__ == "__main__":
-    unittest.main()