Repository: cogent3/cogent3
Pull request #1463 (2023-06-02 06:53:41)

MAINT: changes addressing speed of operations


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 63702f1d7..e378cbd40 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -575,14 +575,9 @@ class _SequenceCollectionBase:
 
     @annotation_db.setter
     def annotation_db(self, value):
-        from cogent3.core.annotation_db import SupportsFeatures
-
         if value == self._annotation_db:
             return
 
-        if value and not isinstance(value, SupportsFeatures):
-            raise TypeError(f"{type(value)} does not satisfy SupportsFeatures")
-
         self._annotation_db = value
 
         for seq in self.seqs:
@@ -1428,7 +1423,7 @@ class _SequenceCollectionBase:
         result = self.__class__(moltype=self.moltype, data=seqs, info=self.info)
 
         if self.annotation_db:
-            result.annotation_db = deepcopy(self.annotation_db)
+            result.annotation_db = self.annotation_db
         return result
 
     def has_terminal_stops(self, gc=None, allow_partial=False):
@@ -1477,7 +1472,7 @@ class _SequenceCollectionBase:
             moltype=self.moltype, data=new_seqs, info=self.info, **kwargs
         )
         if self.annotation_db:
-            result.annotation_db = deepcopy(self.annotation_db)
+            result.annotation_db = self.annotation_db
         return result
 
     def get_lengths(self, include_ambiguity=False, allow_gap=False):
@@ -1684,7 +1679,7 @@ class _SequenceCollectionBase:
             data=data, moltype=moltype, name=self.name, info=self.info
         )
         if self.annotation_db:
-            result.annotation_db = deepcopy(self.annotation_db)
+            result.annotation_db = self.annotation_db
         return result
 
     def to_dna(self):
@@ -1705,7 +1700,7 @@ class _SequenceCollectionBase:
         rc = self.__class__(
             data=seqs, names=self.names[:], name=self.name, info=self.info
         )
-        rc.annotation_db = deepcopy(self.annotation_db)
+        rc.annotation_db = self.annotation_db
         return rc
 
     def reverse_complement(self):
@@ -1824,9 +1819,11 @@ class _SequenceCollectionBase:
 
         # Deep copying Aligned instance to ensure only region specified by Aligned.map is displayed.
         if isinstance(seq1, Aligned):
-            seq1 = seq1.deepcopy(sliced=True)
+            seq1 = seq1.deepcopy(sliced=True, exclude_annotations=True)
+            seq1.data.annotation_db = self.annotation_db
         if isinstance(seq2, Aligned):
-            seq2 = seq2.deepcopy(sliced=True)
+            seq2 = seq2.deepcopy(sliced=True, exclude_annotations=True)
+            seq2.data.annotation_db = self.annotation_db
 
         if seq1.is_annotated() or seq2.is_annotated():
             annotated = True
@@ -4765,9 +4762,6 @@ class Alignment(AlignmentI, SequenceCollection):
 
     @annotation_db.setter
     def annotation_db(self, value):
-        if value and not isinstance(value, SupportsFeatures):
-            raise TypeError
-
         # Without knowing the contents of the db we cannot
         # establish whether self.moltype is compatible, so
         # we rely on the user to get that correct
@@ -4779,7 +4773,7 @@ class Alignment(AlignmentI, SequenceCollection):
 
         self._annotation_db = value
         for seq in self.seqs:
-            seq.data.annotation_db = value
+            seq.data.replace_annotation_db(value, check=False)
 
     def _mapped(self, slicemap):
         align = []
@@ -4933,7 +4927,7 @@ class Alignment(AlignmentI, SequenceCollection):
             # on Aligned
             masked_seqs += [seq._masked_annotations(biotypes, mask_char, shadow)]
         new = self.__class__(data=masked_seqs, info=self.info, name=self.name)
-        new.annotation_db = deepcopy(self.annotation_db)
+        new.annotation_db = self.annotation_db
         return new
 
     @extend_docstring_from(ArrayAlignment.filtered)
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index 5645048bd..c8af9fa4d 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -252,7 +252,12 @@ def _matching_conditions(
         for col, val in conditions.items():
             # conditions are filtered for None before here, so we should add
             # an else where the op is assigned !=
-            if val is not None:
+            if isinstance(val, (tuple, set, list)):
+                placeholders = ",".join(["?" for _ in val])
+                op = "IN"
+                conds.append(f"{col} {op} ({placeholders})")
+                vals.extend(val)
+            elif val is not None:
                 op = "LIKE" if isinstance(val, str) and "%" in val else "="
                 conds.append(f"{col} {op} ?")
                 vals.append(val)
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index d57115b90..992d9d379 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -648,7 +648,7 @@ class SequenceI(object):
             result = self.__class__([missing for _ in len(self)], info=self.info)
         else:
             prefix = missing * first_nongap
-            mid = str(self[first_nongap : last_nongap + 1])
+            mid = str(self)[first_nongap : last_nongap + 1]
             suffix = missing * (len(self) - last_nongap - 1)
             result = self.__class__(prefix + mid + suffix, info=self.info)
         return result
@@ -875,7 +875,7 @@ class Sequence(SequenceI):
         # for both DNA and RNA. But if a user trys get_slice()
         # on a '-' strand feature, they will get a TypError.
         # I think that's enough.
-        self.replace_annotation_db(value, check=True)
+        self.replace_annotation_db(value, check=False)
 
     def replace_annotation_db(
         self, value: SupportsFeatures, check: bool = True
@@ -1284,7 +1284,7 @@ class Sequence(SequenceI):
         new = self.__class__(
             "".join(segments), name=self.name, check=False, info=self.info
         )
-        new.annotation_db = copy.deepcopy(self.annotation_db)
+        new.annotation_db = self.annotation_db
         return new
 
     def gapped_by_map_segment_iter(self, map, allow_gaps=True, recode_gaps=False):
@@ -1324,7 +1324,7 @@ class Sequence(SequenceI):
         myclass = f"{self.__class__.__name__}"
         myclass = myclass.split(".")[-1]
         if len(self) > 10:
-            seq = f"{str(self[:7])}... {len(self)}"
+            seq = f"{str(self)[:7]}... {len(self)}"
         else:
             seq = str(self)
         return f"{myclass}({seq})"
@@ -1346,7 +1346,7 @@ class Sequence(SequenceI):
             preserve_offset = stride > 0
 
         if self.annotation_db is not None and preserve_offset:
-            new.annotation_db = self.annotation_db
+            new.replace_annotation_db(self.annotation_db, check=False)
             new.annotation_offset = self.annotation_offset
 
         if hasattr(self, "_repr_policy"):
@@ -1407,8 +1407,9 @@ class Sequence(SequenceI):
             raise ValueError(f"k must be an int, not {k}")
 
         canonical = set(self.moltype)
-        for i in range(len(self) - k + 1):
-            kmer = self[i : i + k]
+        seq = str(self)
+        for i in range(len(seq) - k + 1):
+            kmer = seq[i : i + k]
             if not strict:
                 yield kmer
             else:
@@ -1483,14 +1484,14 @@ class Sequence(SequenceI):
         seq = self.__class__(
             "".join(gapless), name=self.get_name(), info=self.info, preserve_case=True
         )
-        seq.annotation_db = copy.deepcopy(self.annotation_db)
+        seq.annotation_db = self.annotation_db
         return map, seq
 
     def replace(self, oldchar, newchar):
         """return new instance with oldchar replaced by newchar"""
         new = self._seq.replace(oldchar, newchar)
         result = self.__class__(new, name=self.name, info=self.info)
-        result.annotation_db = copy.deepcopy(self.annotation_db)
+        result.annotation_db = self.annotation_db
         return result
 
     def is_annotated(self):
@@ -1619,7 +1620,7 @@ class NucleicAcidSequence(Sequence):
         rc = self.__class__(
             self._seq[::-1], name=self.name, check=False, info=self.info
         )
-        rc.annotation_db = copy.deepcopy(self.annotation_db)
+        rc.annotation_db = self.annotation_db
         return rc
 
     def has_terminal_stop(self, gc=None, allow_partial=False):
@@ -1669,7 +1670,7 @@ class NucleicAcidSequence(Sequence):
             codons = codons[:-3]
 
         result = self.__class__(codons, name=self.name, info=self.info)
-        result.annotation_db = copy.deepcopy(self.annotation_db)
+        result.annotation_db = self.annotation_db
         return result
 
     def get_translation(self, gc=None, incomplete_ok=False, include_stop=False):
@@ -1700,8 +1701,9 @@ class NucleicAcidSequence(Sequence):
         codon_alphabet = gc.get_alphabet(include_stop=include_stop).with_gap_motif()
         # translate the codons
         translation = []
-        for posn in range(0, len(self) - 2, 3):
-            orig_codon = str(self[posn : posn + 3])
+        seq = str(self)
+        for posn in range(0, len(seq) - 2, 3):
+            orig_codon = str(seq[posn : posn + 3])
             try:
                 resolved = codon_alphabet.resolve_ambiguity(orig_codon)
             except AlphabetError:


Test patch:
diff --git a/tests/test_core/test_annotation_db.py b/tests/test_core/test_annotation_db.py
index fa2e4d39e..9d3f5cb6e 100644
--- a/tests/test_core/test_annotation_db.py
+++ b/tests/test_core/test_annotation_db.py
@@ -66,11 +66,6 @@ def test_assign_valid_db(seq, anno_db):
     assert seq.annotation_db is anno_db
 
 
-def test_assign_invalid_db(seq):
-    with pytest.raises(TypeError):
-        seq.annotation_db = 2
-
-
 def test_replace_annotation_db_check_invalid(seq):
     with pytest.raises(TypeError):
         seq.replace_annotation_db(2, check=True)
@@ -462,6 +457,68 @@ def test_matching_conditions():
     assert got == expect
 
 
+def test_matching_conditions_IN():
+    got, cond = _matching_conditions({"biotype": ("CDS", "mRNA", "exon")}, allow_partial=True)
+    expect = "biotype IN (?,?,?)"
+    assert got == expect
+    assert cond == ("CDS", "mRNA", "exon")
+
+
+@pytest.mark.parametrize(
+    "biotype_value_1", ["CDS", "mRNA", "exon", "three_prime_UTR", "intron"]
+)
+@pytest.mark.parametrize(
+    "biotype_value_2", ["CDS", "mRNA", "exon", "five_prime_UTR", "intron"]
+)
+def test_get_features_matching_multiple_biotype_tuple(
+    seq_db, biotype_value_1, biotype_value_2
+):
+    """querying for features with multiple values should return the
+    same result as the sum of querying for each value seperately"""
+    where_1 = list(seq_db.get_features(biotype=biotype_value_1))
+    where_2 = list(seq_db.get_features(biotype=biotype_value_2))
+    in_both = list(seq_db.get_features(biotype=(biotype_value_1, biotype_value_2)))
+
+    if biotype_value_1 == biotype_value_2:
+        assert len(where_1) == len(in_both) and len(where_2) == len(in_both)
+    else:
+        assert len(where_1) + len(where_2) == len(in_both)
+
+
+@pytest.mark.parametrize("biotype_value_1", ["CDS", "mRNA", "exon", "intron"])
+@pytest.mark.parametrize("biotype_value_2", ["CDS", "mRNA", "exon", "intron"])
+def test_get_features_matching_multiple_biotype_list(
+    seq_db, biotype_value_1, biotype_value_2
+):
+    """querying for features with multiple values should return the
+    same result as the sum of querying for each value seperately"""
+    where_1 = list(seq_db.get_features(biotype=biotype_value_1))
+    where_2 = list(seq_db.get_features(biotype=biotype_value_2))
+    in_both = list(seq_db.get_features(biotype=[biotype_value_1, biotype_value_2]))
+
+    if biotype_value_1 == biotype_value_2:
+        assert len(where_1) == len(in_both) and len(where_2) == len(in_both)
+    else:
+        assert len(where_1) + len(where_2) == len(in_both)
+
+
+@pytest.mark.parametrize("biotype_value_1", ["CDS", "mRNA", "exon", "intron"])
+@pytest.mark.parametrize("biotype_value_2", ["CDS", "mRNA", "exon", "intron"])
+def test_get_features_matching_multiple_biotype_set(
+    seq_db, biotype_value_1, biotype_value_2
+):
+    """querying for features with multiple values should return the
+    same result as the sum of querying for each value seperately"""
+    where_1 = list(seq_db.get_features(biotype=biotype_value_1))
+    where_2 = list(seq_db.get_features(biotype=biotype_value_2))
+    in_both = list(seq_db.get_features(biotype={biotype_value_1, biotype_value_2}))
+
+    if biotype_value_1 == biotype_value_2:
+        assert len(where_1) == len(in_both) and len(where_2) == len(in_both)
+    else:
+        assert len(where_1) + len(where_2) == len(in_both)
+
+
 def test_get_features_matching_start_stop_seqview(seq):
     """testing that get_features_matching adjusts"""
     seq.annotate_from_gff(DATA_DIR / "simple.gff")