Repository: cogent3/cogent3
Pull request #1696 (2024-01-17 03:15:17)

DOC: Add docstring examples for app.sample.omit_gap_pos
#1636


Patch:
diff --git a/src/cogent3/app/sample.py b/src/cogent3/app/sample.py
index 20d40e41d..e47ad759d 100644
--- a/src/cogent3/app/sample.py
+++ b/src/cogent3/app/sample.py
@@ -108,7 +108,7 @@ class concat:
             series of alignment instances
         """
         if not data:
-            raise ValueError("no data")
+            return NotCompleted("ERROR", self, message="no data")
 
         names = []
         for aln in data:
@@ -137,8 +137,10 @@ class concat:
                 collated[name].append(seqs[name])
 
         combined = {n: self._join_seq.join(collated[n]) for n in names}
-        aln = ArrayAlignment(data=combined, moltype=self._moltype)
-        return aln
+        if aln := ArrayAlignment(data=combined, moltype=self._moltype):
+            return aln
+        else:
+            return NotCompleted("FAIL", self, message="result is empty")
 
 
 @define_app
@@ -147,7 +149,10 @@ class omit_degenerates:
     reading frame."""
 
     def __init__(
-        self, moltype: str = None, gap_is_degen: bool = True, motif_length: int = 1
+        self,
+        moltype: Optional[str] = None,
+        gap_is_degen: bool = True,
+        motif_length: int = 1,
     ):
         """
         Parameters
@@ -214,16 +219,16 @@ class omit_degenerates:
             moltype = get_moltype(moltype)
             assert moltype.label.lower() in ("dna", "rna"), "Invalid moltype"
 
-        self.moltype = moltype
+        self._moltype = moltype
         self._allow_gap = not gap_is_degen
         self._motif_length = motif_length
 
     T = Union[SerialisableType, AlignedSeqsType]
 
     def main(self, aln: AlignedSeqsType) -> T:
-        if self.moltype and aln.moltype != self.moltype:
+        if self._moltype and aln.moltype != self._moltype:
             # try converting
-            aln = aln.to_moltype(self.moltype)
+            aln = aln.to_moltype(self._moltype)
 
         return aln.no_degenerates(
             motif_length=self._motif_length, allow_gap=self._allow_gap
@@ -235,7 +240,12 @@ class omit_gap_pos:
     """Excludes gapped alignment columns meeting a threshold. Can accomodate
     reading frame."""
 
-    def __init__(self, allowed_frac=0.99, motif_length=1, moltype=None):
+    def __init__(
+        self,
+        allowed_frac: float = 0.99,
+        motif_length: int = 1,
+        moltype: Optional[str] = None,
+    ):
         """
         Parameters
         ----------
@@ -246,21 +256,63 @@ class omit_gap_pos:
             sequences split into non-overlapping tuples of this size.
         moltype : str
             molecular type, must be either DNA or RNA
+
+        Examples
+        --------
+
+        Create a sample alignment and an app that excludes highly gapped sites.
+        Sites with over 99% gaps are excluded by default.
+
+        >>> from cogent3 import make_aligned_seqs, get_app
+        >>> aln = make_aligned_seqs({"s1": "ACGA-GA-CG", "s2": "GATGATG-AT"})
+
+        >>> app = get_app("omit_gap_pos", moltype="dna")
+        >>> result = app(aln)
+        >>> print(result.to_pretty(name_order=["s1", "s2"]))
+        s1    ACGA-GACG
+        s2    GATGATGAT
+
+        Create an app that excludes all aligned sites with over 49% gaps.
+
+        >>> app = get_app("omit_gap_pos", allowed_frac=0.49, moltype="dna")
+        >>> result = app(aln)
+        >>> print(result.to_pretty(name_order=["s1", "s2"]))
+        s1    ACGAGACG
+        s2    GATGTGAT
+
+        To eliminate any codon columns (where a column is a triple of
+        nucleotides) that contain a gap character, we use the motif_length
+        argument.
+
+        >>> app = get_app("omit_gap_pos", allowed_frac=0, motif_length=3, moltype="dna")
+        >>> result = app(aln)
+        >>> print(result.to_pretty(name_order=["s1", "s2"]))
+        s1    ACG
+        s2    GAT
+
+        A NotCompleted object (see https://cogent3.org/doc/app/not-completed.html)
+        is returned if all sites are excluded.
+
+        >>> aln = make_aligned_seqs({"s1": "ACGA------", "s2": "----ATG-AT"})
+        >>> app = get_app("omit_gap_pos", allowed_frac=0, motif_length=3, moltype="dna")
+        >>> result = app(aln)
+        >>> result.message
+        'all columns exceeded gap threshold'
         """
         if moltype:
             moltype = get_moltype(moltype)
             assert moltype.label.lower() in ("dna", "rna"), "Invalid moltype"
 
-        self.moltype = moltype
+        self._moltype = moltype
         self._allowed_frac = allowed_frac
         self._motif_length = motif_length
 
     T = Union[SerialisableType, AlignedSeqsType]
 
     def main(self, aln: AlignedSeqsType) -> T:
-        if self.moltype and aln.moltype != self.moltype:
+        if self._moltype and aln.moltype != self._moltype:
             # try converting
-            aln = aln.to_moltype(self.moltype)
+            aln = aln.to_moltype(self._moltype)
 
         return aln.omit_gap_pos(
             allowed_gap_frac=self._allowed_frac, motif_length=self._motif_length


Test patch:
diff --git a/tests/test_app/test_sample.py b/tests/test_app/test_sample.py
index 80b70d467..eb1ca31dc 100644
--- a/tests/test_app/test_sample.py
+++ b/tests/test_app/test_sample.py
@@ -1,5 +1,7 @@
 from unittest import TestCase
 
+import pytest
+
 from cogent3 import DNA, make_aligned_seqs, make_unaligned_seqs
 from cogent3.app import composable, sample
 from cogent3.app.composable import NotCompleted
@@ -449,10 +451,6 @@ class TranslateTests(TestCase):
         got = ccat(data[::-1])
         self.assertIsInstance(got, composable.NotCompleted)
 
-        # triggered by no data
-        got = ccat([])
-        self.assertIsInstance(got, composable.NotCompleted)
-
     def test_trim_stop_codons(self):
         """trims stop codons using the specified genetic code"""
         trimmer = sample.trim_stop_codons()  # defaults to standard code
@@ -559,3 +557,11 @@ def test_concat_coerced_moltype():
     aln2 = make_aligned_seqs({"s1": "GCG", "s2": "GGG", "s3": "GGT"})
     result = concat([aln1, aln2])
     assert result.moltype.label == "dna"
+
+
+@pytest.mark.parametrize("data", ([], [make_aligned_seqs({"s1": "", "s2": ""})]))
+def test_concat_empty(data):
+    # triggered by empty alignment
+    ccat = sample.concat()
+    got = ccat(data)
+    assert isinstance(got, NotCompleted)