Repository: cogent3/cogent3
Pull request #2124 (2024-11-21 03:43:20)

ENH: add Alignment copy methods
## Summary by Sourcery

Add copy and deepcopy methods to the Alignment class to support creating shallow and deep copies of alignment instances, and include tests to verify their functionality.

New Features:
- Introduce copy and deepcopy methods to the Alignment class, allowing for shallow and deep copying of alignment instances.

Tests:
- Add tests for the new copy and deepcopy methods in the Alignment class to ensure correct functionality and behavior.


Patch:
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 5636362c8..4369e019a 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -6047,6 +6047,51 @@ class Alignment(SequenceCollection):
         init_args["annotation_db"] = other.annotation_db
         return self.__class__(**init_args)
 
+    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):
+        """returns deep copy of self
+
+        Notes
+        -----
+        Reduced to sliced sequences in self, kwargs are ignored.
+        Annotation db is not copied if the alignment has been sliced.
+        """
+        import copy
+
+        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
+            else copy.deepcopy(self.annotation_db)
+        )
+        new_seqs_data = {
+            n: self._seqs_data.get_gapped_seq_array(
+                seqid=n,
+                start=self._slice_record.plus_start,
+                stop=self._slice_record.plus_stop,
+                step=self._slice_record.plus_step,
+            )
+            for n in self._name_map.values()
+        }
+        new_seqs_data = self._seqs_data.from_seqs(
+            data=new_seqs_data,
+            alphabet=self.moltype.most_degen_alphabet(),
+        )
+        kwargs["seqs_data"] = new_seqs_data
+        kwargs["slice_record"] = None
+        return self.__class__(
+            **kwargs,
+        )
+
 
 @singledispatch
 def make_aligned_seqs(


Test patch:
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 501fb9ca8..fb4a313cf 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -4985,3 +4985,47 @@ def test_alignment_apply_scaled_gaps_codon2aa_invalid_moltype(codon_and_aa_alns)
     codon = codon.to_moltype("text")
     with pytest.raises(ValueError):
         codon.apply_scaled_gaps(ungapped, aa_to_codon=False)
+
+
+def test_alignment_copy(simple_aln):
+    got = simple_aln.copy()
+    # mutable data structures should be different IDs
+    assert got._name_map is not simple_aln._name_map
+    assert got.info is not simple_aln.info
+    assert got.annotation_db is not simple_aln.annotation_db
+    # immutable data structures should be the same object
+    assert got._slice_record is simple_aln._slice_record
+    assert got._seqs_data is simple_aln._seqs_data
+    # sliced data should have same underlying length
+    sl = simple_aln[:2].copy()
+    assert sl._seqs_data.align_len == simple_aln._seqs_data.align_len
+
+    # renamed seqs should be propagated
+    renamed = simple_aln.rename_seqs(renamer=lambda x: x.upper())
+    copied = renamed.copy()
+    assert set(renamed.names) != set(simple_aln.names)
+    assert set(copied.names) == set(renamed.names)
+    # and can get a sequence with a new name
+    assert str(copied.seqs["A"]) == str(renamed.seqs["A"])
+
+
+def test_alignment_deepcopy(simple_aln):
+    got = simple_aln.deepcopy()
+    # all data structures should be different IDs
+    assert got._name_map is not simple_aln._name_map
+    assert got.info is not simple_aln.info
+    assert got.annotation_db is not simple_aln.annotation_db
+    assert got._slice_record is not simple_aln._slice_record
+    assert got._seqs_data is not simple_aln._seqs_data
+
+    # sliced data should have different underlying data length
+    sl = simple_aln[:2].deepcopy()
+    assert sl._seqs_data.align_len == 2 != simple_aln._seqs_data.align_len
+
+    # renamed seqs should be propagated
+    renamed = simple_aln.rename_seqs(renamer=lambda x: x.upper())
+    copied = renamed.deepcopy()
+    assert set(renamed.names) != set(simple_aln.names)
+    assert set(copied.names) == set(renamed.names)
+    # and can get a sequence with a new name
+    assert str(copied.seqs["A"]) == str(renamed.seqs["A"])