Repository: cogent3/cogent3
Pull request #1419 (2023-05-08 00:10:36)

MAINT: simplify new Feature class


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 94b4348d9..3a498b115 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -18,6 +18,8 @@
     passed in a stream of two-item label, sequence pairs. However, this can
     cause confusion when testing.
 """
+from __future__ import annotations
+
 import functools
 import json
 import os
@@ -65,7 +67,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
diff --git a/src/cogent3/core/annotation.py b/src/cogent3/core/annotation.py
index add61c40f..652615c9e 100644
--- a/src/cogent3/core/annotation.py
+++ b/src/cogent3/core/annotation.py
@@ -402,7 +402,7 @@ class _Serialisable:
 
 
 # todo gah implement __repr__ and __str__ methods
-class Feature(_AnnotationCore, _Serialisable):
+class Feature:
     """new style annotation, created on demand"""
 
     __slots__ = (
@@ -412,12 +412,11 @@ class Feature(_AnnotationCore, _Serialisable):
         "biotype",
         "name",
         "_serialisable",
-        "base",  # todo gah do we need this?
-        "base_map",  # todo gah do we need this?
     )
 
     # todo gah implement a __new__ to trap args for serialisation purposes?
     def __init__(self, *, parent, seqid: str, map: Map, biotype: str, name: str):
+        # _serialisable is used for creating derivative instances
         d = locals()
         exclude = ("self", "__class__", "kw")
         self._serialisable = {k: v for k, v in d.items() if k not in exclude}
@@ -425,28 +424,9 @@ class Feature(_AnnotationCore, _Serialisable):
         self.name = name
         self.parent = parent
         self.seqid = seqid
-        if isinstance(map, Map):
-            assert map.parent_length == len(parent), (map, len(parent))
-        else:
-            map = Map(locations=map, parent_length=len(parent))
-
+        assert map.parent_length == len(parent), (map, len(parent))
         self.map = map
 
-    def get_drawable(self):
-        """returns plotly trace"""
-        from cogent3.draw.drawable import make_shape
-
-        return make_shape(type_=self)
-
-    def _mapped(self, slicemap):
-        # the self._serialisable dict is used for serialisation, so we need to
-        # use a copy to create the new instance
-        kwargs = {
-            **self._serialisable,
-            **{"map": slicemap, "name": f"{repr(slicemap)} of {self.name}"},
-        }
-        return self.__class__(**kwargs)
-
     def get_slice(self, complete: bool = False, allow_gaps: bool = False):
         """
         The corresponding sequence fragment.
@@ -481,46 +461,34 @@ class Feature(_AnnotationCore, _Serialisable):
         if self.map.complete:
             return self
         keep = self.map.nongap()
-        return self.__class__(
-            parent=self.parent,
-            map=self.map[keep],
-            biotype=self.biotype,
-            name=self.name,
-            seqid=self.seqid,
-        )
+        kwargs = {**self._serialisable, **dict(map=self.map[keep])}
+        return self.__class__(**kwargs)
 
     def as_one_span(self):
         """returns a feature that preserves any gaps"""
-        new_map = self.map.get_covering_span()
-        return self.__class__(
-            parent=self.parent,
-            map=new_map,
-            biotype=self.biotype,
-            name=f"one-span {self.name}",
-            seqid=self.seqid,
-        )
+        kwargs = {
+            **self._serialisable,
+            **dict(map=self.map.get_covering_span(), name=f"one-span {self.name}"),
+        }
+        return self.__class__(**kwargs)
 
     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,
-        )
+        kwargs = {
+            **self._serialisable,
+            **{
+                "map": self.map.shadow(),
+                "biotype": f"not {self.biotype}",
+                "name": f"not {self.name}",
+            },
+        }
+        return self.__class__(**kwargs)
 
     @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}",
-            seqid=self.seqid,
-        )
+    def get_shadow(self):  # pragma: no cover
+        return self.shadow()
 
     def __len__(self):
         return len(self.map)
@@ -553,7 +521,6 @@ class Feature(_AnnotationCore, _Serialisable):
         make_feature = self.parent.make_feature
         db = self.parent.annotation_db
         for child in db.get_feature_parent(biotype=biotype, name=self.name):
-
             yield make_feature(feature=child)
 
     def union(self, features):
@@ -588,9 +555,12 @@ class Feature(_AnnotationCore, _Serialisable):
             map = map.reversed()
         seqid = ", ".join(seqids) if seqids else None
         biotype = ", ".join(biotypes)
-        return self.__class__(
-            parent=self.parent, seqid=seqid, map=map, biotype=biotype, name=name
-        )
+        kwargs = {
+            **self._serialisable,
+            **{"map": map, "seqid": seqid, "biotype": biotype, "name": name},
+        }
+
+        return self.__class__(**kwargs)
 
     def get_drawable(self):
         """returns plotly trace"""
@@ -599,9 +569,6 @@ class Feature(_AnnotationCore, _Serialisable):
         return make_shape(type_=self)
 
 
-# https://pythonspeed.com/products/filmemoryprofiler/
-
-
 class _Feature(_Annotatable, _Serialisable):  # pragma: no cover
     qualifier_names = ["type", "name"]
     __slots__ = ["parent", "map", "original", "_serialisable", "base", "base_map"]
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index 23ea5ac3d..5fd08e6e6 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import collections
 import inspect
 import json
@@ -5,6 +7,7 @@ import os
 import pathlib
 import re
 import sqlite3
+import sys
 import typing
 
 import numpy
@@ -30,6 +33,8 @@ OptionalStrList = typing.Optional[typing.Union[str, typing.List[str]]]
 OptionalBool = typing.Optional[bool]
 ReturnType = typing.Tuple[str, tuple]  # the sql statement and corresponding values
 
+# used for presence of sqlite feature
+_is_ge_3_11 = (sys.version_info.major, sys.version_info.minor) >= (3, 11)
 
 # Define custom types for storage in sqlite
 # https://stackoverflow.com/questions/18621513/python-insert-numpy-array-into-sqlite3-database
@@ -422,17 +427,31 @@ class SqliteAnnotationDbMixin:
 
     def __deepcopy__(self, memodict=None):
         memodict = memodict or {}
-        new = self.__class__(source=self.source)
-        new._db.deserialize(self._db.serialize())
-        return new
+        if _is_ge_3_11:
+            new = self.__class__(source=self.source)
+            new._db.deserialize(self._db.serialize())
+            return new
+
+        # use rich dict
+        rd = self.to_rich_dict()
+        return type(self).from_dict(rd)
 
     def __getstate__(self):
-        return {"data": self._db.serialize(), "source": self.source}
+        if _is_ge_3_11:
+            return {"data": self._db.serialize(), "source": self.source}
+
+        return self.to_rich_dict()
 
     def __setstate__(self, state):
-        new = self.__class__(source=state.pop("source", None))
-        new._db.deserialize(state["data"])
-        self.__dict__.update(new.__dict__)
+        if _is_ge_3_11:
+            new = self.__class__(source=state.pop("source", None))
+            new._db.deserialize(state["data"])
+            self.__dict__.update(new.__dict__)
+            return self
+
+        # from the rich dict method
+        data = type(self).from_dict(state)
+        self.__dict__.update(data.__dict__)
         return self
 
     @property
@@ -929,7 +948,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
 
             record_keys = col_key_map.keys() & record.keys()
             attrs_keys = record.keys() - col_key_map.keys() - exclude
-            store |= {col_key_map[k]: record[k] for k in record_keys}
+            store.update({col_key_map[k]: record[k] for k in record_keys})
             store["attributes"] = {k: record[k] for k in attrs_keys}
             if "name" not in store:
                 store["name"] = [f"fakeid-{self._num_fakeids}"]


Test patch:
diff --git a/tests/test_core/test_features.py b/tests/test_core/test_features.py
index 658b5776b..e0f8baea2 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, get_moltype, make_aligned_seqs
 from cogent3.core.annotation_db import GffAnnotationDb
-
 # Complete version of manipulating sequence annotations
 from cogent3.util.deserialise import deserialise_object