Repository: cogent3/cogent3
Pull request #2309 (2025-05-10 00:51:04)

MAINT: cast AlignedSeqDataView now returns the gapped sequence
[CHANGED] Now array(view) returns the value for gapped_seq_array.
    The same for str(view) and bytes(view). This should always
    have been the case.

## Summary by Sourcery

Update AlignedSeqDataView to consistently return gapped sequence representations for array, str, and bytes conversions

Bug Fixes:
- Fixed inconsistent behavior of array(), str(), and bytes() methods to always return gapped sequence representations

Tests:
- Added parametrized tests to verify gapped sequence conversion for different sequence types and mol types


Patch:
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index 068489896..7c5299f7a 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -536,16 +536,16 @@ def _count_records_sql(
 
     Parameters
     ----------
-    table_name : str
+    table_name
         containing the data to be selected from
-    columns : Tuple[str]
+    columns
         values to select
-    conditions : dict
+    conditions
         the WHERE conditions
-    start, stop : OptionalInt
+    start, stop
         select records whose (start, stop) values lie between start and stop,
         or overlap them if (allow_partial is True)
-    allow_partial : bool, optional
+    allow_partial
         if False, only records within start, stop are included. If True,
         all records that overlap the segment defined by start, stop are included.
 
@@ -786,19 +786,19 @@ class SqliteAnnotationDbMixin:
 
         Parameters
         ----------
-        seqid : str
+        seqid
             name of the sequence feature resides on
-        biotype : str
+        biotype
             biological type of the record
-        name : str
+        name
             the name of a record, an identifier
-        spans : typing.List[typing.Tuple[int, int]], optional
+        spans
             this will be sorted
-        strand : str, optional
+        strand
             either +, -. Defaults to '+'
-        attributes : str, optional
+        attributes
             additional attributes as a string
-        on_alignment : bool, optional
+        on_alignment
             whether the annotation is an alignment annotation
         """
         spans = numpy.array(sorted(sorted(coords) for coords in spans), dtype=int)
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index fab6116da..7dbd49dc8 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -4306,20 +4306,20 @@ class AlignedDataView(new_sequence.SeqViewABC):
         return self.gapped_str_value.encode("utf8")
 
     def __str__(self) -> str:
-        return self.str_value
+        return self.gapped_str_value
 
     def __array__(
         self,
         dtype: numpy.dtype | None = None,
         copy: bool | None = None,
     ) -> numpy.ndarray[int]:
-        arr = self.array_value
+        arr = self.gapped_array_value
         if dtype:
             arr = arr.astype(dtype)
         return arr
 
     def __bytes__(self) -> bytes:
-        return self.bytes_value
+        return self.gapped_bytes_value
 
     def __getitem__(self, segment) -> typing_extensions.Self:
         return self.__class__(
@@ -5841,7 +5841,7 @@ class Alignment(SequenceCollection):
         *,
         biotype: str,
         name: str,
-        spans: List[Tuple[int, int]],
+        spans: list[tuple[int, int]],
         seqid: OptStr = None,
         parent_id: OptStr = None,
         strand: str = "+",
diff --git a/src/cogent3/util/warning.py b/src/cogent3/util/warning.py
index 4ca446a80..33ac49fc2 100644
--- a/src/cogent3/util/warning.py
+++ b/src/cogent3/util/warning.py
@@ -84,9 +84,9 @@ def deprecated_args(
         format, e.g. 'YYYY.MM'
     reason : str
         Reason for deprecation or guidance on what to do
-    old-new : List[Tuple[str, str]]
+    old-new : list[tuple[str, str]]
         A list of deprecated old and replacement new argument names.
-    discontinued : List[str]
+    discontinued : list[str]
         Names of single or multiple arguments to be discontinued. This should
         only be applied to arguments that have no effect.
 


Test patch:
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 32569b37c..d01c2e144 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -3321,7 +3321,7 @@ def test_aligned_seqs_data_getitem(seqid, index, aligned_array_dict, moltype, re
     got_with_seqid = numpy.array(ad[seqid])
     got_with_index = numpy.array(ad[index])
 
-    expect = aligned_array_dict[seqid][aligned_array_dict[seqid] != 4]
+    expect = aligned_array_dict[seqid]
 
     assert numpy.array_equal(got_with_seqid, expect)
     assert numpy.array_equal(got_with_index, expect)
@@ -3359,6 +3359,29 @@ def test_aligned_seqs_data_get_gapped_seq_array(
     assert numpy.array_equal(got, expect)
 
 
+@pytest.mark.parametrize("seqid", ["seq1", "seq2", "seq3", "seq4"])
+@pytest.mark.parametrize("moltype", ["dna_moltype", "rna_moltype"])
+def test_aligned_seqs_data_view_cast(
+    aligned_array_dict,
+    seqid,
+    moltype,
+    request,
+):
+    moltype = request.getfixturevalue(moltype)
+    alpha = moltype.degen_gapped_alphabet
+    ad = new_alignment.AlignedSeqsData.from_seqs(
+        data=aligned_array_dict,
+        alphabet=alpha,
+    )
+    got = ad[seqid]
+    # the default conversion by array, str or bytes is of the gapped sequence
+    expect_array = aligned_array_dict[seqid]
+    assert numpy.array_equal(numpy.array(got), expect_array)
+    expect_str = alpha.from_indices(expect_array)
+    assert str(got) == expect_str
+    assert bytes(got) == expect_str.encode("utf-8")
+
+
 @pytest.mark.parametrize("seqid", ["seq1", "seq2", "seq3", "seq4"])
 @pytest.mark.parametrize("moltype", ["dna_moltype", "rna_moltype"])
 def test_aligned_seqs_data_get_seq_str(aligned_array_dict, seqid, moltype, request):
@@ -3482,7 +3505,7 @@ def test_aligned_seqs_data_add_seqs_duplicate_keys_raises(dna_alphabet):
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2", "seq3", "seq4"])
 def test_aligned_seqs_data_get_aligned_view(aligned_dict, seqid, dna_alphabet):
-    # str on an ADV should return the ungapped sequence
+    # str on an ADV should return the gapped sequence
     ad = new_alignment.AlignedSeqsData.from_seqs(
         data=aligned_dict,
         alphabet=dna_alphabet,
@@ -3490,7 +3513,7 @@ def test_aligned_seqs_data_get_aligned_view(aligned_dict, seqid, dna_alphabet):
     got = ad.get_view(seqid)
     assert got.parent == ad
     assert got.parent_len == ad.align_len
-    assert str(got) == aligned_dict[seqid].replace("-", "")
+    assert str(got) == aligned_dict[seqid]
 
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2", "seq3", "seq4"])
@@ -3501,24 +3524,16 @@ def test_aligned_data_view_array(aligned_array_dict, dna_alphabet, seqid):
     )
     view = ad.get_view(seqid)
     got = numpy.array(view)
-    expect = aligned_array_dict[seqid][aligned_array_dict[seqid] != 4]  # remove gaps
-    assert numpy.array_equal(got, expect)
-
-    # directly accessing .array_value property should return the same result
-    got = view.array_value
+    expect = aligned_array_dict[seqid]
     assert numpy.array_equal(got, expect)
 
-
-@pytest.mark.parametrize("seqid", ["seq1", "seq2", "seq3", "seq4"])
-def test_aligned_data_view_gapped_array(aligned_array_dict, dna_alphabet, seqid):
-    ad = new_alignment.AlignedSeqsData.from_seqs(
-        data=aligned_array_dict,
-        alphabet=dna_alphabet,
-    )
-    view = ad.get_view(seqid)
+    # directly accessing .gapped_array_value property should return the same result
     got = view.gapped_array_value
-    expect = aligned_array_dict[seqid]
     assert numpy.array_equal(got, expect)
+    # checking ungapped variant
+    assert numpy.array_equal(
+        view.array_value, expect[aligned_array_dict[seqid] != 4]
+    )  # remove gaps
 
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2", "seq3", "seq4"])
@@ -3529,22 +3544,11 @@ def test_aligned_data_view_str(aligned_dict, dna_alphabet, seqid):
     )
     view = ad.get_view(seqid)
     got = str(view)
-    expect = aligned_dict[seqid].replace("-", "")
-    assert got == expect
-
-    # directly accessing .str_value property should return the same result
-    got = view.str_value
+    expect = aligned_dict[seqid]
     assert got == expect
 
-
-def test_aligned_data_view_gapped_str_value(aligned_dict, dna_alphabet):
-    ad = new_alignment.AlignedSeqsData.from_seqs(
-        data=aligned_dict,
-        alphabet=dna_alphabet,
-    )
-    view = ad.get_view("seq1")
+    # directly accessing .gapped_str_value property should return the same result
     got = view.gapped_str_value
-    expect = aligned_dict["seq1"]
     assert got == expect
 
 
@@ -3554,27 +3558,17 @@ def test_aligned_data_view_bytes(aligned_array_dict, dna_alphabet, seqid):
         data=aligned_array_dict,
         alphabet=dna_alphabet,
     )
-    view = ad.get_view(seqid)
+    view = ad[seqid]
     got = bytes(view)
-    expect = aligned_array_dict[seqid][aligned_array_dict[seqid] != 4]  # remove gaps
+    expect = aligned_array_dict[seqid]
     expect = dna_alphabet.array_to_bytes(expect)  # convert to bytes
-    assert numpy.array_equal(got, expect)
-
-    # directly accessing .bytes_value property should return the same result
-    got = view.bytes_value
-    assert numpy.array_equal(got, expect)
-
+    assert got == expect
 
-@pytest.mark.parametrize("seqid", ["seq1", "seq2", "seq3", "seq4"])
-def test_aligned_data_view_gapped_bytes_value(aligned_array_dict, dna_alphabet, seqid):
-    ad = new_alignment.AlignedSeqsData.from_seqs(
-        data=aligned_array_dict,
-        alphabet=dna_alphabet,
-    )
-    view = ad.get_view(seqid)
+    # directly accessing .gapped_bytes_value property should return the same result
     got = view.gapped_bytes_value
-    expect = dna_alphabet.array_to_bytes(aligned_array_dict[seqid])
-    assert numpy.array_equal(got, expect)
+    assert got == expect
+    got = view.bytes_value
+    assert got == expect.replace(b"-", b"")
 
 
 @pytest.fixture