Repository: cogent3/cogent3
Pull request #2137 (2024-11-26 00:44:02)

ENH: deepcopy of an Alignment retains reversal information
## Summary by Sourcery

Enhance the deepcopy functionality of Alignment to retain reversal information by adjusting the slice_record. Add tests to verify correct behavior of slicing with plus attributes and alignment copy/deepcopy operations.

Enhancements:
- Ensure deepcopy of an Alignment retains reversal information by modifying the slice_record appropriately.

Tests:
- Add tests for slicing with plus attributes and reversing to match native Python slicing.
- Add tests for alignment copy and deepcopy to verify retention of reversal information.


Patch:
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 4df43ebc1..3e14912b6 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -6166,7 +6166,13 @@ class Alignment(SequenceCollection):
             alphabet=self.moltype.most_degen_alphabet(),
         )
         kwargs["seqs_data"] = new_seqs_data
-        kwargs["slice_record"] = None
+        # we have modified the underlying data with the plus attributes only,
+        # so, if reversed, we need to retain that information in the slice_record
+        kwargs["slice_record"] = (
+            new_sequence.SliceRecord(parent_len=new_seqs_data.align_len, step=-1)
+            if self._slice_record.is_reversed
+            else None
+        )
         return self.__class__(
             **kwargs,
         )
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index b859978f9..08daaf114 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -2804,11 +2804,12 @@ class SeqView(SeqViewABC):
                 slice_record=self.slice_record.copy(),
                 parent_len=self.parent_len,
             )
-        # the plus start and stop are applied, but the step is not applied
-        sr = SliceRecord(parent_len=len(self), step=self.slice_record.step)
+        # we slice parent with the plus attributes only, so, if reversed, we need to
+        # retain that information in the slice_record
+        sr = SliceRecord(parent_len=len(self), step=-1) if self.is_reversed else None
         return self.__class__(
             parent=self.parent[
-                self.slice_record.plus_start : self.slice_record.plus_stop
+                self.slice_record.plus_start : self.slice_record.plus_stop : self.slice_record.plus_step
             ],
             parent_len=len(self),
             seqid=self.seqid,


Test patch:
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index e8bd69342..a9f834736 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -4999,6 +4999,14 @@ def test_alignment_copy(simple_aln):
     assert str(copied.seqs["A"]) == str(renamed.seqs["A"])
 
 
+def test_alignment_copy_rc(simple_aln):
+    rc = simple_aln.rc()
+    got = rc.copy()
+    assert got.to_dict() == rc.to_dict()
+    assert got._is_reversed == rc._is_reversed
+    assert got._slice_record is rc._slice_record
+
+
 def test_alignment_deepcopy(simple_aln):
     got = simple_aln.deepcopy()
     # all data structures should be different IDs
@@ -5021,6 +5029,14 @@ def test_alignment_deepcopy(simple_aln):
     assert str(copied.seqs["A"]) == str(renamed.seqs["A"])
 
 
+def test_alignment_deepcopy_rc(simple_aln):
+    rc = simple_aln.rc()
+    got = rc.deepcopy()
+    assert got.to_dict() == rc.to_dict()
+    assert got._is_reversed == rc._is_reversed
+    assert got._slice_record is not simple_aln._slice_record
+
+
 @pytest.mark.parametrize(
     "mk_cls",
     [
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index 7a6883a35..9e25b77dd 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -2673,6 +2673,25 @@ def test_seqview_attrs_non_modulo(rev):
     assert sr.plus_stop == 0
 
 
+@pytest.mark.parametrize("start", (0, 1, 10, -1, -10))
+@pytest.mark.parametrize("stop", (0, 10, 8, 1, -1, -11))
+@pytest.mark.parametrize("step", (1, 2, -1, -2))
+def test_plus_attrs_slicing(start, stop, step):
+    """slicing with plus attributes then reversing should match native python slicing"""
+    data = "0123456789"
+    sr = new_sequence.SliceRecord(start=start, stop=stop, step=step, parent_len=10)
+
+    plus_start, plus_stop, plus_step = sr.plus_start, sr.plus_stop, sr.plus_step
+    got = (
+        data[plus_start:plus_stop:plus_step][::-1]
+        if step < 0
+        else data[plus_start:plus_stop:plus_step]
+    )
+    expect = data[start:stop:step]
+    print(start, stop, step, plus_start, plus_stop, plus_step)
+    assert got == expect
+
+
 @pytest.mark.parametrize("rev", (False, True))
 def test_seqview_attrs_zero_slice(rev):
     sr = new_sequence.SliceRecord(start=0, stop=0, step=1, parent_len=0)