Repository: cogent3/cogent3
Pull request #1710 (2024-02-08 04:12:29)

DEV: Alignment construction embeds seqid in underlying SeqView 


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index b70277dc8..4cabd43a8 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -3922,9 +3922,12 @@ class ArrayAlignment(AlignmentI, _SequenceCollectionBase):
     @property
     def named_seqs(self):
         if self._named_seqs is None:
-            seqs = list(map(self.alphabet.to_string, self.array_seqs))
+            seqs = [self.alphabet.to_string(seq) for seq in self.array_seqs]
             if self.moltype:
-                seqs = [self.moltype.make_seq(s, preserve_case=True) for s in seqs]
+                seqs = [
+                    self.moltype.make_seq(seq, name, preserve_case=True)
+                    for seq, name in zip(seqs, self.names)
+                ]
             self._named_seqs = _make_named_seqs(self.names, seqs)
         return self._named_seqs
 
@@ -5394,8 +5397,7 @@ def _coerce_to_unaligned_seqs(data, names, label_to_name=str, moltype=None) -> O
     names = names or list(data.keys())
     seqs = []
     for name in names:
-        seq = _construct_unaligned_seq(data[name], moltype=moltype)
-        seq.name = name
+        seq = _construct_unaligned_seq(data[name], name=name, moltype=moltype)
         seqs.append(seq)
     return seqs, names
 
@@ -5575,8 +5577,7 @@ def _coerce_to_aligned_seqs(data, names, label_to_name=str, moltype=None) -> O:
     names = names or list(data.keys())
     seqs = []
     for name in names:
-        seq = _construct_aligned_seq(data[name], moltype=moltype)
-        seq.name = seq.data.name = name
+        seq = _construct_aligned_seq(data[name], name=name, moltype=moltype)
         seqs.append(seq)
 
     _one_length(seqs)
@@ -5638,58 +5639,63 @@ T = typing.Sequence[typing.Union[Sequence, ndarray]]
 
 
 @functools.singledispatch
-def _construct_unaligned_seq(data, moltype) -> Sequence:
+def _construct_unaligned_seq(data, name, moltype) -> Sequence:
     try:
         result = data.to_moltype(moltype) if moltype else data
     except AttributeError:
-        result = moltype.make_seq(data, preserve_case=False)
+        result = moltype.make_seq(data, name=name, preserve_case=False)
+    result.name = name
     return result
 
 
 @_construct_unaligned_seq.register
-def _(data: str, moltype) -> Sequence:
-    return moltype.make_seq(data, preserve_case=False)
+def _(data: str, name, moltype) -> Sequence:
+    return moltype.make_seq(data, name=name, preserve_case=False)
 
 
 @_construct_unaligned_seq.register
-def _(data: bytes, moltype) -> Sequence:
-    return _construct_unaligned_seq(data.decode("utf8"), moltype)
+def _(data: bytes, name, moltype) -> Sequence:
+    return _construct_unaligned_seq(data.decode("utf8"), name=name, moltype=moltype)
 
 
 @_construct_unaligned_seq.register
-def _(data: Aligned, moltype) -> Sequence:
+def _(data: Aligned, name, moltype) -> Sequence:
+    data.name = name
     return data.get_gapped_seq().to_moltype(moltype)
 
 
 @_construct_unaligned_seq.register
-def _(data: ArraySequence, moltype) -> Sequence:
-    return moltype.make_seq(str(data), name=data.name, info=data.info)
+def _(data: ArraySequence, name, moltype) -> Sequence:
+    assert name == data.name
+    return moltype.make_seq(str(data), name=name, info=data.info)
 
 
 @_construct_unaligned_seq.register
-def _(data: ndarray, moltype) -> Sequence:
-    return moltype.make_array_seq(data)
+def _(data: ndarray, name, moltype) -> Sequence:
+    return moltype.make_array_seq(data, name=name)
 
 
 @functools.singledispatch
-def _construct_aligned_seq(data, moltype) -> Aligned:
-    seq = _construct_unaligned_seq(data, moltype)
+def _construct_aligned_seq(data: str, name, moltype) -> Aligned:
+    seq = _construct_unaligned_seq(data, name, moltype)
+    seq.name = name
     return Aligned(*seq.parse_out_gaps())
 
 
 @_construct_aligned_seq.register
-def _(data: Aligned, moltype) -> Aligned:
+def _(data: Aligned, name, moltype) -> Aligned:
+    data.name = name
     return data
 
 
 @_construct_aligned_seq.register
-def _(data: list, moltype) -> Aligned:
-    return _construct_aligned_seq("".join(data), moltype)
+def _(data: list, name, moltype) -> Aligned:
+    return _construct_aligned_seq("".join(data), name, moltype)
 
 
 @_construct_aligned_seq.register
-def _(data: tuple, moltype) -> Aligned:
-    return _construct_aligned_seq("".join(data), moltype)
+def _(data: tuple, name, moltype) -> Aligned:
+    return _construct_aligned_seq("".join(data), name, moltype)
 
 
 # convert seq data into a numpy array for an array alignment
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index 8b79814e5..4e0f7d1f7 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -822,7 +822,7 @@ class Sequence(SequenceI):
             else (lambda x: x)
         )
 
-        self._seq = _coerce_seq(seq, preserve_case, checker)
+        self._seq = _coerce_to_seqview(seq, self.name, preserve_case, checker)
 
         if not isinstance(info, InfoClass):
             try:
@@ -2946,51 +2946,51 @@ class ArrayProteinWithStopSequence(ArraySequence):
 
 
 @singledispatch
-def _coerce_seq(data, preserve_case, checker):
+def _coerce_to_seqview(data, seqid, preserve_case, checker):
     from cogent3.core.alignment import Aligned
 
     if isinstance(data, Aligned):
-        return _coerce_seq(str(data), preserve_case, checker)
+        return _coerce_to_seqview(str(data), seqid, preserve_case, checker)
     raise NotImplementedError(f"{type(data)}")
 
 
-@_coerce_seq.register
-def _(data: SeqView, preserve_case, checker):
+@_coerce_to_seqview.register
+def _(data: SeqView, seqid, preserve_case, checker):
     return data
 
 
-@_coerce_seq.register
-def _(data: Sequence, preserve_case, checker):
-    return _coerce_seq(str(data), preserve_case, checker)
+@_coerce_to_seqview.register
+def _(data: Sequence, seqid, preserve_case, checker):
+    return _coerce_to_seqview(data._seq, seqid, preserve_case, checker)
 
 
-@_coerce_seq.register
-def _(data: ArraySequence, preserve_case, checker):
-    return _coerce_seq(str(data), preserve_case, checker)
+@_coerce_to_seqview.register
+def _(data: ArraySequence, seqid, preserve_case, checker):
+    return _coerce_to_seqview(str(data), seqid, preserve_case, checker)
 
 
-@_coerce_seq.register
-def _(data: str, preserve_case, checker):
+@_coerce_to_seqview.register
+def _(data: str, seqid, preserve_case, checker):
     if not preserve_case:
         data = data.upper()
     checker(data)
-    return SeqView(data)
+    return SeqView(data, seqid=seqid)
 
 
-@_coerce_seq.register
-def _(data: bytes, preserve_case, checker):
+@_coerce_to_seqview.register
+def _(data: bytes, seqid, preserve_case, checker):
     if not preserve_case:
         data = data.upper()
     data = data.decode("utf8")
     checker(data)
-    return SeqView(data)
+    return SeqView(data, seqid=seqid)
 
 
-@_coerce_seq.register
-def _(data: tuple, preserve_case, checker):
-    return _coerce_seq("".join(data), preserve_case, checker)
+@_coerce_to_seqview.register
+def _(data: tuple, seqid, preserve_case, checker):
+    return _coerce_to_seqview("".join(data), seqid, preserve_case, checker)
 
 
-@_coerce_seq.register
-def _(data: list, preserve_case, checker):
-    return _coerce_seq("".join(data), preserve_case, checker)
+@_coerce_to_seqview.register
+def _(data: list, seqid, preserve_case, checker):
+    return _coerce_to_seqview("".join(data), seqid, preserve_case, checker)


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index f5439cebd..70a331630 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -2658,7 +2658,7 @@ def test_to_rich_dict_not_alignment(cls):
 
     got = aln.to_rich_dict()
 
-    data = {k: SeqView(s).to_rich_dict() for k, s in data.items()}
+    data = {k: SeqView(s, seqid=k).to_rich_dict() for k, s in data.items()}
 
     expect = {
         "seqs": {
@@ -3165,7 +3165,7 @@ def test_init_duplicate_keys_raises(cls):
 )
 def test_construct_unaligned_seq(seq):
     moltype = get_moltype("dna")
-    got = _construct_unaligned_seq(seq, moltype=moltype)
+    got = _construct_unaligned_seq(seq, name="seq1", moltype=moltype)
     assert isinstance(got, (Sequence, ArraySequence))
 
 
@@ -3531,3 +3531,28 @@ def test_get_degapped_relative_to(cls):
 
     with pytest.raises(ValueError):
         aln.get_degapped_relative_to("nameX")
+
+
+@pytest.mark.parametrize("array_align", (True, False))
+@pytest.mark.parametrize(
+    "seq_moltype", (("ACCT--GT", "dna"), ("ACCT--GU", "rna"), ("MTST--T", "protein"))
+)
+def test_alignment_propogates_seqid_to_seqview(array_align, seq_moltype):
+    data = {"seq1": seq_moltype[0], "seq2": seq_moltype[0], "seq3": seq_moltype[0]}
+    aln = make_aligned_seqs(data, moltype=seq_moltype[1], array_align=array_align)
+
+    if array_align:
+        assert aln.seqs[0]._seq.seqid == "seq1"
+    else:
+        assert aln.seqs[0].data._seq.seqid == "seq1"
+
+
+@pytest.mark.parametrize("cls", (str, bytes))
+def test_construct_unaligned_seq_propogates_seqid(cls):
+    data = "ACGT"
+    if cls is bytes:
+        seq = cls(data, "utf8")
+    else:
+        seq = cls(data)
+    got = _construct_unaligned_seq(seq, name="seq1", moltype=DNA)
+    assert got._seq.seqid == "seq1"
diff --git a/tests/test_core/test_sequence.py b/tests/test_core/test_sequence.py
index 755120cb3..4abdb351c 100644
--- a/tests/test_core/test_sequence.py
+++ b/tests/test_core/test_sequence.py
@@ -16,6 +16,7 @@ from numpy.testing import assert_allclose, assert_equal
 import cogent3
 
 from cogent3._version import __version__
+from cogent3.core.alignment import Aligned
 from cogent3.core.moltype import (
     ASCII,
     BYTES,
@@ -39,6 +40,7 @@ from cogent3.core.sequence import (
     RnaSequence,
     Sequence,
     SeqView,
+    _coerce_to_seqview,
 )
 from cogent3.util.misc import get_object_provenance
 
@@ -2310,7 +2312,7 @@ def test_to_rich_dict(cls, with_offset):
     seq = "AAGGCC"
 
     if cls == Sequence:
-        seq = SeqView(seq).to_rich_dict()
+        seq = SeqView(seq, seqid="seq1").to_rich_dict()
 
     expect = {
         "name": "seq1",
@@ -2334,7 +2336,7 @@ def test_to_json(cls, with_offset):
 
     seq = "AAGGCC"
     if cls == Sequence:
-        seq = SeqView(seq).to_rich_dict()
+        seq = SeqView(seq, seqid="seq1").to_rich_dict()
 
     expect = {
         "name": "seq1",
@@ -2655,3 +2657,49 @@ def test_seqview_slice_propagates_seqid():
 
     copied_sliced_sv = sliced_sv.copy(sliced=True)
     assert copied_sliced_sv.seqid == "seq1"
+
+
+@pytest.mark.parametrize("cls", (Aligned, Sequence, SeqView, ArraySequence, str, bytes))
+def test_coerce_to_seqview(cls):
+    seq = "AC--GGTGGGAC"
+    seqid = "seq1"
+    if cls in (str, bytes):
+        got = _coerce_to_seqview(seq, seqid, preserve_case=True, checker=(lambda x: x))
+    elif cls is Aligned:
+        got = _coerce_to_seqview(
+            cls(*Sequence(seq).parse_out_gaps()),
+            seqid,
+            preserve_case=True,
+            checker=(lambda x: x),
+        )
+    else:
+        got = _coerce_to_seqview(
+            cls(seq), seqid, preserve_case=True, checker=(lambda x: x)
+        )
+    assert got.value == seq
+    assert isinstance(got, SeqView)
+
+
+def test_sequences_propogates_seqid():
+    # creating a name Sequence propagates the seqid to the SeqView.
+    seq = Sequence("ACGGTGGGAC", name="seq1")
+    assert seq._seq.seqid == "seq1"
+
+    # renaming the Sequence deosnt change the seqid of the SeqView.
+    seq.name = "seq2"
+    assert seq.name == "seq2"
+    assert seq._seq.seqid == "seq1"
+
+    # creating a name Aligned propagates the seqid to the SeqView.
+    seq = Aligned(*Sequence("ACG--G--GAC", name="seq1").parse_out_gaps())
+    assert seq.data._seq.seqid == "seq1"
+
+    # creating a Sequence with a seqview does not change the seqid of the SeqView.
+    seq = Sequence(SeqView("ACGGTGGGAC", seqid="parent_name"), name="seq_name")
+    assert seq.name == "seq_name"
+    assert seq._seq.seqid == "parent_name"
+
+    # creating a Sequence with an unnamed seqview does not name the SeqView.
+    seq = Sequence(SeqView("ACGGTGGGAC"), name="seq_name")
+    assert seq.name == "seq_name"
+    assert seq._seq.seqid == None