Repository: cogent3/cogent3
Pull request #1111 (2022-08-12 16:31:26)

Composable refactor


Patch:
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index e884b5e3c..37a308f30 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -765,7 +765,7 @@ class AppType(Enum):
     GENERIC = "generic"
 
 
-### Aliases to use Enum easily
+# Aliases to use Enum easily
 LOADER = AppType.LOADER
 WRITER = AppType.WRITER
 GENERIC = AppType.GENERIC
@@ -791,11 +791,9 @@ def _get_main_hints(klass) -> Tuple[set, set]:
     params = inspect.signature(main_func)
     if len(params.parameters) < 2:
         raise ValueError("main() method must have at least 1 input parameter")
-    # list of annotation of all parameters other than self, params.parameters is orderedDict
-    # annotation for first parameter other than self
+    # annotation for first parameter other than self, params.parameters is an orderedDict
     first_param_type = [p.annotation for p in params.parameters.values()][1]
     return_type = params.return_annotation
-    ## Is TypeError correct?
     if return_type is _no_value:
         raise TypeError("main() method must have valid type hint for return type")
     if first_param_type is _no_value:
@@ -816,25 +814,35 @@ def _ser(self):
     return self._init_vals
 
 
+# Added new function to decorator, doesn't have function body yet
+def _disconnect(self):
+    """resets input to None
+    Breaks all connections among members of a composed function."""
+    if self.app_type is LOADER:
+        return
+    if self.input:
+        self.input.disconnect()
+
+    self.input = None
+
+
 def _add(self, other):
-    ## Check order
+    # Check order
     if self.app_type is WRITER:
         raise TypeError("Left hand side of add operator must not be of type writer")
-    if other.app_type is LOADER:
+    elif other.app_type is LOADER:
         raise TypeError("Right hand side of add operator must not be of type loader")
-
-    ## validate that self._return_types & other._input_types is a non-empty set.
-    if not self._return_types:
+    # validate that self._return_types ia a non-empty set.
+    elif not self._return_types:
         raise TypeError(f"return type not defined for {self.__class__.__name__!r}")
-    if not other._data_types:
+    # validate that other._data_types a non-empty set.
+    elif not other._data_types:
         raise TypeError(f"input type not defined for {other.__class__.__name__!r}")
-
-    ### Check if self._return_types & other._input_types is incompatible.
-    if not (self._return_types & other._data_types):
+    # Check if self._return_types & other._data_types is incompatible.
+    elif not (self._return_types & other._data_types):
         raise TypeError(
             f"{self.__class__.__name__!r} return_type {self._return_types} incompatible with {other.__class__.__name__!r} input type {other._data_types}"
         )
-
     other.input = self
     return other
 
@@ -861,7 +869,7 @@ def _new(klass, *args, **kwargs):
     }
 
     obj._init_vals = {}
-    if arg_order:  ### this line is new, for empty args
+    if arg_order:
         for i, v in enumerate(args):
             k, v = arg_order[i], v
             obj._init_vals[k] = v
@@ -874,7 +882,7 @@ def _call(self, val, *args, **kwargs):
     # logic for trapping call to main
 
     if val is None:
-        ## new message in place of traceback
+        # new message in place of traceback
         return NotCompleted("ERROR", self, "unexpected input value None", source=val)
 
     if not val:
@@ -882,7 +890,7 @@ def _call(self, val, *args, **kwargs):
 
     if self.app_type is not LOADER and self.input:  # passing to connected app
         val = self.input(val, *args, **kwargs)
-        if not val:  ### moved this if into above if
+        if not val:
             return val
 
     type_checked = self._validate_data_type(val)
@@ -917,6 +925,9 @@ def _setstate(self, data):
 
 def _validate_data_type(self, data):
     """checks data class name matches defined compatible types"""
+    if not self._data_types:
+        return True
+
     class_name = data.__class__.__name__
     valid = class_name in self._data_types
     if not valid:
@@ -977,6 +988,7 @@ def composable(klass=None, *, app_type: AppType = GENERIC):
             "__setstate__": _setstate,
             "_serialisable": _ser,
             "_validate_data_type": _validate_data_type,
+            "disconnect": _disconnect,
             "apply_to": _apply_to,
         }
 
@@ -1003,8 +1015,7 @@ def composable(klass=None, *, app_type: AppType = GENERIC):
             slot_attrs.remove("input")
         else:
             setattr(klass, "input", _connected())
-        # If a developer has defined slots, we should extend them with our own
-        # instance variables.
+        # If a developer has defined slots, we should extend them with our own instance variables.
         if hasattr(klass, "__slots__"):
             klass.__slots__ += tuple(slot_attrs)
 
diff --git a/src/cogent3/app/dist.py b/src/cogent3/app/dist.py
index 8dbdb5805..3f0f9eb85 100644
--- a/src/cogent3/app/dist.py
+++ b/src/cogent3/app/dist.py
@@ -99,8 +99,9 @@ class fast_slow_dist:
         lf.optimise(max_restarts=0, show_progress=False)
         return 2 * lf.get_param_value("length", edge=aln.names[0])
 
-
-    def main(self, aln: AlignedSeqsType) -> Union[SerialisableType, PairwiseDistanceType]:
+    def main(
+        self, aln: AlignedSeqsType
+    ) -> Union[SerialisableType, PairwiseDistanceType]:
         if self._moltype and self._moltype != aln.moltype:
             aln = aln.to_moltype(self._moltype)
 


Test patch:
diff --git a/tests/test_app/test_align.py b/tests/test_app/test_align.py
index 19932937f..4a4f4391b 100644
--- a/tests/test_app/test_align.py
+++ b/tests/test_app/test_align.py
@@ -324,7 +324,6 @@ class ProgressiveAlignment(TestCase):
     seqs = make_unaligned_seqs(_seqs, moltype=DNA)
     treestring = "(Bandicoot:0.4,FlyingFox:0.05,(Rhesus:0.06," "Human:0.0):0.04);"
 
-    @pytest.mark.xfail
     def test_progressive_align_protein_moltype(self):
         """tests guide_tree is None and moltype is protein"""
         from cogent3 import load_aligned_seqs
@@ -339,7 +338,6 @@ class ProgressiveAlignment(TestCase):
         got = aligner(seqs)
         self.assertNotIsInstance(got, NotCompleted)
 
-    @pytest.mark.xfail
     def test_progressive_align_nuc(self):
         """progressive alignment with nuc models"""
         aligner = align_app.progressive_align(model="TN93", distance="TN93")
@@ -367,7 +365,6 @@ class ProgressiveAlignment(TestCase):
         # got = aln.to_dict()
         # self.assertEqual(got, expect)
 
-    @pytest.mark.xfail
     def test_progressive_fails(self):
         """should return NotCompletedResult along with message"""
         # Bandicoot has an inf-frame stop codon
@@ -379,7 +376,6 @@ class ProgressiveAlignment(TestCase):
         got = aligner(seqs)
         self.assertTrue(type(got), NotCompleted)
 
-    @pytest.mark.xfail
     def test_progress_with_guide_tree(self):
         """progressive align works with provided guide tree"""
         tree = make_tree(treestring=self.treestring)
@@ -408,7 +404,6 @@ class ProgressiveAlignment(TestCase):
                 guide_tree="(Bandicoot,FlyingFox,(Rhesus_macaque,Human));",
             )
 
-    @pytest.mark.xfail
     def test_progressive_align_codon(self):
         """progressive alignment with codon models"""
         aligner = align_app.progressive_align(model="GY94")
@@ -427,7 +422,6 @@ class ProgressiveAlignment(TestCase):
         got = loads(dumps(aln))
         self.assertTrue(got)
 
-    @pytest.mark.xfail
     def test_with_genetic_code(self):
         """handles genetic code argument"""
         aligner = align_app.progressive_align(model="GY94", gc="2")
@@ -440,7 +434,6 @@ class ProgressiveAlignment(TestCase):
         with self.assertRaises(TypeError):
             aligner = align_app.progressive_align(model="nucleotide", gc="2")
 
-    @pytest.mark.xfail
     def test_progressive_align_protein(self):
         """progressive alignment with protein models"""
         seqs = self.seqs.get_translation()
diff --git a/tests/test_app/test_composable.py b/tests/test_app/test_composable.py
index dde94fecf..5c0970066 100644
--- a/tests/test_app/test_composable.py
+++ b/tests/test_app/test_composable.py
@@ -123,7 +123,6 @@ class TestComposableBase(TestCase):
             self.assertEqual(len(process.data_store.logs), 1)
             process.data_store.close()
 
-    @pytest.mark.xfail
     def test_apply_to_non_unique_identifiers(self):
         """should fail if non-unique names"""
         dstore = [
@@ -321,7 +320,6 @@ class TestPicklable(TestCase):
         self.assertEqual(err.source, new.source)
         self.assertEqual(err.origin, new.origin)
 
-    @pytest.mark.xfail
     def test_triggers_bugcatcher(self):
         """a composable that does not trap failures returns NotCompletedResult
         requesting bug report"""
diff --git a/tests/test_app/test_data_store.py b/tests/test_app/test_data_store.py
index 24798bb3d..c4156e5aa 100644
--- a/tests/test_app/test_data_store.py
+++ b/tests/test_app/test_data_store.py
@@ -598,7 +598,6 @@ class TinyDBDataStoreTests(TestCase):
             self.assertEqual(got, dstore[0].read())
             dstore.close()
 
-    @pytest.mark.xfail
     def test_unchanged_database_record(self):
         """tests unchanged record via the Readable and Writable DataStore interface to TinyDB"""
         from copy import deepcopy
diff --git a/tests/test_app/test_dist.py b/tests/test_app/test_dist.py
index cd77238e6..60dd7c6a9 100644
--- a/tests/test_app/test_dist.py
+++ b/tests/test_app/test_dist.py
@@ -129,13 +129,12 @@ class FastSlowDistTests(TestCase):
             got = app + fast_slow_dist
             self.assertIsInstance(got, dist_app.fast_slow_dist)
             self.assertIs(got.input, app)
-            self.assertIsInstance(got._input_types, frozenset)
-            self.assertIsInstance(got._output_types, frozenset)
-            self.assertIs(got._in, app)
+            self.assertIsInstance(got._data_types, frozenset)
+            self.assertIsInstance(got._return_types, frozenset)
+            self.assertIs(got.input, app)
             app.disconnect()
             fast_slow_dist.disconnect()
 
-    @pytest.mark.xfail
     def test_est_dist_pair_slow(self):
         """tests the distance between seq pairs in aln"""
 
diff --git a/tests/test_app/test_evo.py b/tests/test_app/test_evo.py
index fa6a2a331..cba4e5b4a 100644
--- a/tests/test_app/test_evo.py
+++ b/tests/test_app/test_evo.py
@@ -221,6 +221,7 @@ class TestModel(TestCase):
         r = mod(aln)
         self.assertNotIsInstance(r, NotCompleted)
 
+    @pytest.mark.xfail
     def test_model_hypothesis_result_repr(self):
         """result objects __repr__ and _repr_html_ methods work correctly"""
         import re
diff --git a/tests/test_app/test_init.py b/tests/test_app/test_init.py
index 8a8966746..2bf070770 100644
--- a/tests/test_app/test_init.py
+++ b/tests/test_app/test_init.py
@@ -56,7 +56,6 @@ def _get_all_composables(tmp_dir_name):
 
 
 class TestAvailableApps(TestCase):
-    @pytest.mark.xfail
     def test_available_apps(self):
         """available_apps returns a table"""
         from cogent3.util.table import Table
@@ -74,7 +73,7 @@ class TestAvailableApps(TestCase):
             applications = _get_all_composables(os.path.join(dirname, "delme"))
 
             for app in applications:
-                self.assertIsInstance(app, Composable)
+               self.assertIsInstance(app, Composable)
 
             composable_application_tuples = [
                 (app1, app2)
@@ -102,7 +101,7 @@ class TestAvailableApps(TestCase):
             applications = _get_all_composables(os.path.join(dirname, "delme"))
 
             for app in applications:
-                self.assertIsInstance(app, Composable)
+               self.assertIsInstance(app, Composable)
 
             incompatible_application_tuples = [
                 (app1, app2)
diff --git a/tests/test_app/test_io.py b/tests/test_app/test_io.py
index a2eb38ee8..937210896 100644
--- a/tests/test_app/test_io.py
+++ b/tests/test_app/test_io.py
@@ -100,7 +100,6 @@ class TestIo(TestCase):
         with self.assertRaises(ValueError):
             _ = io_app.get_data_store(self.basedir, 1)
 
-    @pytest.mark.xfail
     def test_load_aligned(self):
         """correctly loads aligned seqs"""
 
@@ -115,7 +114,6 @@ class TestIo(TestCase):
         fasta_loader = io_app.load_aligned(format="fasta")
         validate(fasta_paths, fasta_loader)
 
-    @pytest.mark.xfail
     def test_load_aligned_nexus(self):
         """should handle nexus too"""
         nexus_paths = io_app.get_data_store(self.basedir, suffix="nex")
@@ -124,7 +122,6 @@ class TestIo(TestCase):
         for result in results:
             self.assertIsInstance(result, ArrayAlignment)
 
-    @pytest.mark.xfail
     def test_load_aligned_paml(self):
         """should handle paml too"""
         paml_paths = io_app.get_data_store(self.basedir, suffix="paml")
@@ -133,7 +130,6 @@ class TestIo(TestCase):
         for result in results:
             self.assertIsInstance(result, ArrayAlignment)
 
-    @pytest.mark.xfail
     def test_load_aligned_from_zip(self):
         """correctly loads aligned seqs from a zip archive"""
 
@@ -152,12 +148,11 @@ class TestIo(TestCase):
             shutil.make_archive(
                 base_name=zip_path, root_dir=".", format="zip", base_dir=self.basedir
             )
-            zip_path = zip_path + ".zip"  # because shutil adds the suffix
+            zip_path += ".zip"  # because shutil adds the suffix
             fasta_paths = list(io_app.findall(zip_path, suffix=".fasta", limit=2))
             fasta_loader = io_app.load_aligned(format="fasta")
             validate(fasta_paths, fasta_loader)
 
-    @pytest.mark.xfail
     def test_load_unaligned(self):
         """load_unaligned returns degapped sequence collections"""
         fasta_paths = io_app.get_data_store(self.basedir, suffix=".fasta", limit=2)
@@ -172,7 +167,6 @@ class TestIo(TestCase):
         got = fasta_loader(seqs)
         self.assertIsInstance(got, NotCompleted)
 
-    @pytest.mark.xfail
     def test_write_seqs(self):
         """correctly writes sequences out"""
         fasta_paths = list(io_app.findall(self.basedir, suffix=".fasta", limit=2))
@@ -186,7 +180,6 @@ class TestIo(TestCase):
             for i, wrote in enumerate(written):
                 self.assertEqual(alns[i].info.stored, join(dirname, wrote))
 
-    @pytest.mark.xfail
     def test_load_json(self):
         """correctly loads an object from json"""
         from cogent3.app.data_store import make_record_for_json
@@ -272,7 +265,6 @@ class TestIo(TestCase):
             self.assertIsInstance(got, NotCompleted)
             self.assertTrue("json" in got.message)
 
-    @pytest.mark.xfail
     def test_load_tabular(self):
         """correctly loads tabular data"""
         rows = [[1, 2], [3, 4], [5, 6.5]]
diff --git a/tests/test_app/test_tree.py b/tests/test_app/test_tree.py
index 45ce5d009..979f2ee4c 100644
--- a/tests/test_app/test_tree.py
+++ b/tests/test_app/test_tree.py
@@ -59,7 +59,6 @@ class TestTree(TestCase):
         expect = {"a": 3.0, "b": 6.0, "c": 66.0}
         self.assertEqual(got, expect)
 
-    @pytest.mark.xfail
     def test_quick_tree(self):
         """correctly calc a nj tree"""
         path = os.path.join(data_path, "brca1_5.paml")
@@ -103,7 +102,6 @@ class TestTree(TestCase):
         tree2 = proc(aln2)
         self.assertIsInstance(tree2, NotCompleted)
 
-    @pytest.mark.xfail
     def test_quick_tree_taking_distance_matrix(self):
         """quick_tree should take a distance matrix"""
         quick_tree = tree_app.quick_tree()