Repository: cogent3/cogent3
Pull request #1399 (2023-04-28 02:18:53)

Annotation refactor


Patch:
diff --git a/src/cogent3/__init__.py b/src/cogent3/__init__.py
index 3de4e33b5..56e2acd9c 100644
--- a/src/cogent3/__init__.py
+++ b/src/cogent3/__init__.py
@@ -19,7 +19,6 @@ from cogent3.core.alignment import (
 )
 from cogent3.core.annotation_db import load_annotations
 from cogent3.core.genetic_code import available_codes, get_code
-
 # note that moltype has to be imported last, because it sets the moltype in
 # the objects created by the other modules.
 from cogent3.core.moltype import (
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 7c91ce866..187845b4f 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
@@ -1712,6 +1711,7 @@ class _SequenceCollectionBase:
         )
         if isinstance(self, _Annotatable) and self.annotations:
             self._annotations_nucleic_reversed_on(rc)
+        rc.annotation_db = self.annotation_db
         return rc
 
     def reverse_complement(self):
diff --git a/src/cogent3/core/annotation.py b/src/cogent3/core/annotation.py
index 4079cd50b..45f56785f 100644
--- a/src/cogent3/core/annotation.py
+++ b/src/cogent3/core/annotation.py
@@ -424,6 +424,7 @@ class Annotation(_AnnotationCore, _Serialisable):
         self.biotype = biotype
         self.name = name
         self.parent = parent
+        self.seqid = seqid
         if isinstance(map, Map):
             assert map.parent_length == len(parent), (map, len(parent))
         else:
@@ -461,18 +462,43 @@ class Annotation(_AnnotationCore, _Serialisable):
             return self
         keep = self.map.nongap()
         return self.__class__(
-            self.parent, self.map[keep], biotype=self.biotype, name=self.name
+            self.parent,
+            self.map[keep],
+            biotype=self.biotype,
+            name=self.name,
+            seqid=self.seqid,
         )
 
     def as_one_span(self):
         new_map = self.map.get_covering_span()
         return self.__class__(
-            self.parent, new_map, biotype=self.biotype, name=f"one-span {self.name}"
+            self.parent,
+            new_map,
+            biotype=self.biotype,
+            name=f"one-span {self.name}",
+            seqid=self.seqid,
         )
 
+    def shadow(self):
+        """returns new instance corresponding to disjoint of self coordinates"""
+        return self.__class__(
+            parent=self.parent,
+            map=self.map.shadow(),
+            biotype=f"not {self.biotype}",
+            name=f"not {self.name}",
+            seqid=self.seqid,
+        )
+
+    @c3warn.deprecated_callable(
+        "2023.7", reason="new method", new="<instance>.shadow()"
+    )
     def get_shadow(self):
         return self.__class__(
-            self.parent, self.map.shadow(), type="region", name=f"not {self.name}"
+            self.parent,
+            self.map.shadow(),
+            type="region",
+            name=f"not {self.name}",
+            seqid=self.seqid,
         )
 
     def __len__(self):
@@ -480,7 +506,7 @@ class Annotation(_AnnotationCore, _Serialisable):
 
     def __repr__(self):
         name = f' "{self.name}"'
-        return f"{self.biotype}{name} at {self.map}"
+        return f"{self.seqid!r} {self.biotype}{name} at {self.map}"
 
     def _projected_to_base(self, base):
         if self.parent == base:
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index 9ba70b2cb..d0f334d2a 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -690,7 +690,7 @@ class Map(object):
             last_x = x
             last_y = y
         assert y == 0
-        return Map(result, parent_length=self.parent_length)
+        return Map(locations=result, parent_length=self.parent_length)
 
     def reversed(self):
         """Reversed location on same parent"""
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index 66de6cc34..b992f9496 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -1432,7 +1432,7 @@ class NucleicAcidSequence(Sequence):
         )
         if self.annotation_db is not None:
             rc.annotation_db = self.annotation_db
-        rc._seq = self._seq[::-1]
+
         return rc
 
     def has_terminal_stop(self, gc=None, allow_partial=False):


Test patch:
diff --git a/tests/test_core/test_features.py b/tests/test_core/test_features.py
index 6b9abf2d4..7b7ef4afb 100755
--- a/tests/test_core/test_features.py
+++ b/tests/test_core/test_features.py
@@ -4,7 +4,6 @@ import pytest
 
 from cogent3 import ASCII, DNA, make_aligned_seqs
 from cogent3.core.annotation import Feature, Variable
-
 # Complete version of manipulating sequence annotations
 from cogent3.util.deserialise import deserialise_object
 
@@ -100,68 +99,21 @@ class FeaturesTest(TestCase):
         combined = exon1.union(exons)
         self.assertEqual(str(combined.get_slice()), "CCCCCTTTTTAAAAA")
 
-        # eg: all the exon sequence:
-
-        self.assertEqual(
-            str(self.s.get_region_covering_all(exons).get_slice()), "CCCCCTTTTTAAAAA"
-        )
-
-        # or with slice notation:
-
-        self.assertEqual(str(self.s[self.exon1, self.exon2]), "CCCCCTTTTTAAAAA")
-
-    @pytest.mark.xfail(reason="todo gah update test to use latest API")
-    def test_slice_errors_from_merged(self):
-        """no overlap in merged features"""
+    def test_shadow(self):
+        """combines multiple features into shadow"""
 
-        # Though .get_region_covering_all also guarantees
-        # no overlaps within the result, slicing does not:
+        # To construct a pseudo-feature covering (or excluding)
+        # multiple features, use get_region_covering_all:
 
-        exons = self.s.get_features(biotype="exon")
-        self.assertEqual(
-            str(self.s.get_region_covering_all(exons + exons)),
-            'region "exon" at [10:15, 30:40]/48',
+        exons = list(self.s.get_features(biotype="exon"))
+        expect = str(
+            self.s[: exons[0].map.start]
+            + self.s[exons[0].map.end : exons[1].map.start]
+            + self.s[exons[1].map.end :]
         )
-        with self.assertRaises(ValueError):
-            self.s[self.exon1, self.exon1, self.exon1, self.exon1, self.exon1]
-
-        # You can use features, maps, slices or integers,
-        # but non-monotonic slices are not allowed:
-
-        with self.assertRaises(ValueError):
-            self.s[15:20, 5:16]
-
-    @pytest.mark.xfail(reason="todo gah delete test, not supporting this")
-    def test_feature_slicing(self):
-        """features can be sliced"""
-
-        # Features are themselves sliceable:
-
-        self.assertEqual(str(self.exon1[0:3].get_slice()), "CCC")
-
-        # When sequences are concatenated they keep their (non-overlapping) annotations:
-
-        c = self.s[self.exon1[4:]] + self.s
-        self.assertEqual(len(c), 49)
-
-        answers = [
-            'exon "fred" at [-4-, 0:1]/49',
-            'exon "fred" at [11:16]/49',
-            'exon "trev" at [31:41]/49',
-        ]
-
-        array = []
-
-        for feat in c.annotations:
-            array.append(str(feat))
-
-        self.assertEqual(answers, array)
-
-        # Since features know their parents you can't
-        # use a feature from one sequence to slice another:
-
-        with self.assertRaises(ValueError):
-            c[self.exon1]
+        exon1 = exons.pop(0)
+        shadow = exon1.union(exons).shadow()
+        assert str(shadow.get_slice()) == expect
 
     @pytest.mark.xfail(reason="todo gah delete test? or update to new API")
     def test_feature_attach_detach(self):