Repository: cogent3/cogent3
Pull request #2130 (2024-11-22 08:56:17)

DEV: views and seqs_data do not support serialisation
## Summary by Sourcery

Remove support for SeqView serialization and simplify the serialization process by directly using sequence strings. Update tests to reflect the removal of SeqView serialization.

Enhancements:
- Simplify the serialization process by removing the use of SeqView in the to_rich_dict method.

Tests:
- Remove tests related to SeqView serialization, as the feature is no longer supported.


Patch:
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index a0f5a3e1a..cc825aa1d 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -305,9 +305,6 @@ class SeqsDataABC(ABC):
     @abstractmethod
     def add_seqs(self, seqs, **kwargs) -> SeqsDataABC: ...
 
-    @abstractmethod
-    def to_rich_dict(self) -> dict: ...
-
     @abstractmethod
     def __len__(self) -> int: ...
 
@@ -544,33 +541,6 @@ class SeqsData(SeqsDataABC):
     def _(self, index: int) -> new_sequence.SeqViewABC:
         return self[self.names[index]]
 
-    def to_rich_dict(self) -> dict[str, str | dict[str, str]]:
-        """returns a json serialisable dict"""
-        return {
-            "init_args": {
-                "data": {name: self.get_seq_str(seqid=name) for name in self.names},
-                "alphabet": self.alphabet.to_rich_dict(),
-                "offset": self._offset,
-            },
-            "type": get_object_provenance(self),
-            "version": __version__,
-        }
-
-    @classmethod
-    def from_rich_dict(cls, data: dict[str, str | dict[str, str]]) -> SeqsData:
-        """returns a new instance from a rich dict"""
-        alphabet = deserialise_object(data["init_args"]["alphabet"])
-        return cls(
-            data=data["init_args"]["data"],
-            alphabet=alphabet,
-            offset=data["init_args"]["offset"],
-        )
-
-
-@register_deserialiser(get_object_provenance(SeqsData))
-def deserialise_seqs_data(data: dict[str, str | dict[str, str]]) -> SeqsData:
-    return SeqsData.from_rich_dict(data)
-
 
 class SequenceCollection:
     """A container of unaligned sequences"""
@@ -640,11 +610,12 @@ class SequenceCollection:
         # both SequenceCollection and Alignment implement _get_init_kwargs,
         # ensuring methods in SequenceCollection that are inherited by Alignment
         # capture initialisation arguments unique to the subclass.
+        # mutable arguments are copied
         return {
             "seqs_data": self._seqs_data,
             "moltype": self.moltype,
-            "name_map": self._name_map,
-            "info": self.info,
+            "name_map": self._name_map.copy(),
+            "info": self.info.copy(),
             "annotation_db": self.annotation_db,
             "is_reversed": self._is_reversed,
         }
@@ -886,8 +857,6 @@ class SequenceCollection:
         """
         kwargs = self._get_init_kwargs()
         kwargs.pop("is_reversed", None)  # reversal is realised
-        kwargs["name_map"] = self._name_map.copy()  # name_map is mutable
-        kwargs["info"] = self.info.copy()  # info is mutable
         kwargs["moltype"] = self.moltype.label
         kwargs.pop("annotation_db", None)
         kwargs.pop(
@@ -3591,10 +3560,6 @@ class AlignedSeqsData(AlignedSeqsDataABC):
 
         return self._gapped[indices, start:stop:step]
 
-    def to_rich_dict(self):
-        # todo: kath
-        ...
-
 
 class AlignedDataViewABC(new_sequence.SeqViewABC):
     __slots__ = ()
@@ -3788,10 +3753,6 @@ class AlignedDataView(new_sequence.SeqViewABC):
             "slice_record": self.slice_record,
         }
 
-    def to_rich_dict(self) -> dict:
-        ...
-        # todo: kath...
-
     def get_seq_view(self) -> new_sequence.SeqViewABC:
         # we want the parent coordinates in sequence coordinates
         # parent_seq_coords does not account for the stride
@@ -3927,8 +3888,8 @@ class Alignment(SequenceCollection):
         return {
             "seqs_data": self._seqs_data,
             "moltype": self.moltype,
-            "name_map": self._name_map,
-            "info": self.info,
+            "name_map": self._name_map.copy(),
+            "info": self.info.copy(),
             "annotation_db": self.annotation_db,
             "slice_record": self._slice_record,
         }
@@ -6128,8 +6089,6 @@ class Alignment(SequenceCollection):
     def copy(self):
         """creates new instance, only mutable attributes are copied"""
         kwargs = self._get_init_kwargs()
-        kwargs["name_map"] = self._name_map.copy()
-        kwargs["info"] = self.info.copy()
         return self.__class__(**kwargs)
 
     def deepcopy(self, **kwargs):
@@ -6144,8 +6103,6 @@ class Alignment(SequenceCollection):
 
         kwargs = self._get_init_kwargs()
         kwargs.pop("seqs_data")
-        kwargs["name_map"] = self._name_map.copy()
-        kwargs["info"] = self.info.copy()
         kwargs["annotation_db"] = (
             None
             if len(self) != self._seqs_data.align_len
@@ -6211,8 +6168,6 @@ class Alignment(SequenceCollection):
         """returns a json serialisable dict"""
         kwargs = self._get_init_kwargs()
         kwargs.pop("slice_record")  # slice is realised
-        kwargs["name_map"] = self._name_map.copy()  # name_map is mutable
-        kwargs["info"] = self.info.copy()  # info is mutable
         kwargs["moltype"] = self.moltype.label
         kwargs.pop("annotation_db", None)  # we dont serialise the annotation db
         kwargs.pop(
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index 09832ad8a..85bcc20be 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -84,20 +84,12 @@ def _moltype_seq_from_rich_dict(data):
     data.pop("type")
     data.pop("version")
     data.pop("annotation_db", None)
-    offset = data.pop("annotation_offset", 0)
+    data.pop("annotation_offset", 0)
 
     moltype = data.pop("moltype")
     moltype = new_moltype.get_moltype(moltype)
 
-    seqview_data = data.pop("seq")
-    seq = _coerce_to_seqview(
-        seqview_data["init_args"]["parent"],
-        data["name"],
-        moltype.most_degen_alphabet(),
-        offset,
-    )
-    seq = seq[:: seqview_data["init_args"]["slice_record"]["init_args"]["step"]]
-
+    seq = data.pop("seq")
     return moltype, seq
 
 
@@ -276,10 +268,9 @@ class Sequence:
             info.pop("Refs")
 
         info = info or None
-        seq = self._seq.to_rich_dict() if hasattr(self, "_seq") else str(self)
         data = dict(
             name=self.name,
-            seq=seq,
+            seq=str(self),
             moltype=self.moltype.label,
             info=info,
             type=get_object_provenance(self),
@@ -2612,9 +2603,6 @@ class SeqViewABC(ABC):
     @abstractmethod
     def copy(self, sliced: bool = False): ...
 
-    @abstractmethod
-    def to_rich_dict(self) -> dict: ...
-
     @abstractmethod
     def __str__(self) -> str: ...
 
@@ -2775,38 +2763,6 @@ class SeqView(SeqViewABC):
             f"slice_record={self.slice_record.__repr__()})"
         )
 
-    def to_rich_dict(self) -> dict[str, str | dict[str, str]]:
-        """returns a json serialisable dict
-
-        Notes
-        -----
-        This method will slice the underlying sequence to the start and stop values
-
-        Warning
-        -------
-        This method is not intended to provide serialisation of this object,
-        instead, it is intended for usage by an enclosing class.
-        """
-        # get the current state
-        data = {"type": get_object_provenance(self), "version": __version__}
-        data["init_args"] = self._get_init_kwargs()
-
-        if self.slice_record.is_reversed:
-            adj = self.parent_len + 1
-            start, stop = self.slice_record.stop + adj, self.slice_record.start + adj
-
-        else:
-            start, stop = self.slice_record.start, self.slice_record.stop
-
-        data["init_args"]["parent"] = self.parent[start:stop]
-        new_sr = SliceRecord(
-            parent_len=(stop - start),
-            step=self.slice_record.step,
-        )
-        data["init_args"]["slice_record"] = new_sr.to_rich_dict()
-        data["init_args"]["alphabet"] = self.alphabet.to_rich_dict()
-        return data
-
     def copy(self, sliced: bool = False):
         """returns copy
 
@@ -2824,13 +2780,16 @@ class SeqView(SeqViewABC):
                 slice_record=self.slice_record.copy(),
                 parent_len=self.parent_len,
             )
-        data = self.to_rich_dict()
+        # the plus start and stop are applied, but the step is not applied
+        sr = SliceRecord(parent_len=len(self), step=self.slice_record.step)
         return self.__class__(
-            parent=data["init_args"]["parent"],
-            parent_len=data["init_args"]["parent_len"],
+            parent=self.parent[
+                self.slice_record.plus_start : self.slice_record.plus_stop
+            ],
+            parent_len=len(self),
             seqid=self.seqid,
             alphabet=self.alphabet,
-            slice_record=SliceRecord(**data["init_args"]["slice_record"]["init_args"]),
+            slice_record=sr,
         )
 
 


Test patch:
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 2c9c80c7d..77da9ef34 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -335,18 +335,6 @@ def test_seqs_data_to_alphabet_invalid():
         _ = seqs.to_alphabet(DNA)
 
 
-def test_seqs_data_round_trip(dna_alphabet):
-    seqs_data = new_alignment.SeqsData(
-        data={"seq1": "ACGG", "seq2": "CGCA", "seq3": "CCG-"}, alphabet=dna_alphabet
-    )
-    rd = seqs_data.to_rich_dict()
-    got = deserialise_object(rd)
-    assert isinstance(got, new_alignment.SeqsData)
-    assert got.to_rich_dict() == seqs_data.to_rich_dict()
-    expect = "ACGG"
-    assert str(got.get_view("seq1")) == expect
-
-
 @pytest.mark.parametrize(
     "index",
     [
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index e583c46b4..7a6883a35 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -2103,7 +2103,6 @@ def test_seqview_copy(sliced, rev, integer_seq):
     copied = sv.copy(sliced=sliced)
 
     assert copied.str_value == raw_data[slice_start:slice_end]
-    assert copied.is_reversed == integer_seq.is_reversed
     assert (
         sliced and copied.parent is not sv.parent or copied.parent is integer_seq.parent
     )
@@ -2216,18 +2215,14 @@ def test_to_moltype_rna():
 
 
 def test_to_rich_dict():
-    """Sequence to_dict works"""
+    """Sequence.to_rich_dict works"""
     dna = new_moltype.DNA
     s = "AAGGCC"
     r = dna.make_seq(seq=s, name="seq1")
     got = r.to_rich_dict()
-    seq = new_sequence.SeqView(
-        parent=s, parent_len=len(s), seqid="seq1", alphabet=dna.most_degen_alphabet()
-    ).to_rich_dict()
-
     expect = {
         "name": "seq1",
-        "seq": seq,
+        "seq": s,
         "moltype": r.moltype.label,
         "info": None,
         "type": get_object_provenance(r),
@@ -2244,16 +2239,9 @@ def test_sequence_to_json():
     r = dna.make_seq(seq="AAGGCC", name="seq1")
     got = json.loads(r.to_json())
     data = "AAGGCC"
-    seq = new_sequence.SeqView(
-        parent=data,
-        parent_len=len(data),
-        seqid="seq1",
-        alphabet=dna.most_degen_alphabet(),
-    ).to_rich_dict()
-
     expect = {
         "name": "seq1",
-        "seq": seq,
+        "seq": data,
         "moltype": dna.label,
         "info": None,
         "type": get_object_provenance(r),
@@ -2264,37 +2252,6 @@ def test_sequence_to_json():
     assert got == expect
 
 
-@pytest.mark.parametrize("coord", ("start", "stop"))
-def test_seqview_to_rich_dict(coord, dna_alphabet):
-    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
-    sv = new_sequence.SeqView(
-        parent=parent, parent_len=len(parent), alphabet=dna_alphabet
-    )
-    plus = sv.to_rich_dict()
-    minus = sv[::-1].to_rich_dict()
-    plus = plus.pop("init_args")
-    minus = minus.pop("init_args")
-    assert plus.pop("parent") == minus.pop("parent")
-    assert (
-        plus["slice_record"]["init_args"]["step"]
-        == -minus["slice_record"]["init_args"]["step"]
-    )
-    assert coord not in plus
-    assert coord not in minus
-
-
-@pytest.mark.parametrize("reverse", (False, True))
-def test_sliced_seqview_rich_dict(reverse, dna_alphabet):
-    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
-    sl = slice(2, 13)
-    sv = new_sequence.SeqView(
-        parent=parent, parent_len=len(parent), alphabet=dna_alphabet
-    )[sl]
-    sv = sv[::-1] if reverse else sv
-    rd = sv.to_rich_dict()
-    assert rd["init_args"]["parent"] == parent[sl]
-
-
 @pytest.mark.parametrize(
     "sl",
     (
@@ -2571,19 +2528,6 @@ def test_seqview_seqid(dna_alphabet):
     assert sv.seqid == "seq1"
 
 
-def test_seqview_to_rich_dict_seqid(dna_alphabet):
-    seq = "ACGGTGGGAC"
-    sv = new_sequence.SeqView(
-        parent=seq, parent_len=len(seq), seqid="seq1", alphabet=dna_alphabet
-    )
-    rd = sv.to_rich_dict()
-    assert rd["init_args"]["seqid"] == "seq1"
-
-    sv = new_sequence.SeqView(parent=seq, parent_len=len(seq), alphabet=dna_alphabet)
-    rd = sv.to_rich_dict()
-    assert rd["init_args"]["seqid"] is None
-
-
 def test_seqview_slice_propagates_seqid(dna_alphabet):
     parent = "ACGGTGGGAC"
     sv = new_sequence.SeqView(