Repository: cogent3/cogent3
Pull request #2122 (2024-11-21 00:16:33)

ENH: add get_projected_features
## Summary by Sourcery

Add the 'get_projected_features' method to project features from other sequences onto a specified sequence ID and include corresponding tests to validate its functionality.

New Features:
- Introduce the 'get_projected_features' method to project all features from other sequences onto a specified sequence ID.

Tests:
- Add tests for the 'get_projected_features' method to ensure correct feature projection onto specified sequence IDs.


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 75d6fd974..0b5676d57 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -4716,7 +4716,9 @@ class Alignment(AlignmentI, SequenceCollection):
         self.annotation_db.add_feature(**feature.to_dict())
         return result
 
-    def get_projected_features(self, *, seqid: str, **kwargs) -> list[Feature]:
+    def get_projected_features(
+        self, *, seqid: str, **kwargs
+    ) -> list[Feature]:  # ported
         """projects all features from other sequences onto seqid"""
         # todo gah should there be a generator version,
         # iter_projected_features()?
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 5636362c8..9e5992198 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -5373,6 +5373,15 @@ class Alignment(SequenceCollection):
         self.annotation_db.add_feature(**feature.to_dict())
         return result
 
+    def get_projected_features(self, *, seqid: str, **kwargs) -> list[Feature]:
+        """projects all features from other sequences onto seqid"""
+        annots = []
+        for name in self.names:
+            if name == seqid:
+                continue
+            annots.extend(list(self.get_features(seqid=name, **kwargs)))
+        return [self.get_projected_feature(seqid=seqid, feature=a) for a in annots]
+
     def get_drawables(
         self, *, biotype: Optional[str, typing.Iterable[str]] = None
     ) -> dict:


Test patch:
diff --git a/tests/test_core/test_new_aln_annotation.py b/tests/test_core/test_new_aln_annotation.py
index 5b2fb53f1..9cdd81b64 100644
--- a/tests/test_core/test_new_aln_annotation.py
+++ b/tests/test_core/test_new_aln_annotation.py
@@ -502,3 +502,30 @@ def test_copy_annotations_no_db(gff_db, mk_cls):
 
     seq_coll.copy_annotations(gff_db)
     assert seq_coll.annotation_db.num_matches() == gff_db.num_matches()
+
+
+def test_project_features_onto_specified_seqid():
+    db = GffAnnotationDb()
+    aln = new_alignment.make_aligned_seqs(
+        {"x": "-AAAAAAAAA", "y": "TTTT--TTTT"}, moltype="dna"
+    )
+    db.add_feature(seqid="x", biotype="exon", name="fred", spans=[(3, 8)])
+    aln.annotation_db = db
+
+    exons = aln.get_projected_features(seqid="y", biotype="exon")
+    assert len(exons) == 1
+    assert str(aln.get_seq("y")[exons[0].map.without_gaps()]), "TTT"
+    assert "biotype='exon', name='fred', map=[-2-, 4:7]/8" in str(exons[0])
+
+
+def test_project_features_no_features_for_specified_seqid():
+    db = GffAnnotationDb()
+    aln = new_alignment.make_aligned_seqs(
+        {"seq1": "ATGCGT", "seq2": "AT--GT"}, moltype="dna"
+    )
+    db.add_feature(seqid="seq1", biotype="gene", name="gene1", spans=[(0, 6)])
+    aln.annotation_db = db
+
+    projected_features = aln.get_projected_features(seqid="seq2", biotype="exon")
+
+    assert len(projected_features) == 0