Repository: cogent3/cogent3
Pull request #1230 (2022-12-15 01:05:40)

Enhance the define_app decorator


Patch:
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index 3f840fd62..0d9a3fade 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -986,7 +986,7 @@ def _call(self, val, *args, **kwargs):
     if val is None:
         val = NotCompleted("ERROR", self, "unexpected input value None", source=val)
 
-    if isinstance(val, NotCompleted):
+    if isinstance(val, NotCompleted) and self._skip_not_completed:
         return val
 
     # todo we should get the source information from val here
@@ -1075,7 +1075,9 @@ def _class_from_func(func):
     return result
 
 
-def define_app(klass=None, *, app_type: AppType = GENERIC):
+def define_app(
+    klass=None, *, app_type: AppType = GENERIC, skip_not_completed: bool = True
+):
     """decorator for building callable apps
 
     Parameters
@@ -1085,6 +1087,9 @@ def define_app(klass=None, *, app_type: AppType = GENERIC):
         class with the function bound as a static method.
     app_type
         what type of app, typically you just want GENERIC.
+    skip_not_completed
+        if True (default), NotCompleted instances are returned without being
+        passed to the app main() method.
 
     Notes
     -----
@@ -1220,6 +1225,7 @@ def define_app(klass=None, *, app_type: AppType = GENERIC):
         setattr(klass, "_data_types", arg_hints)
         setattr(klass, "_return_types", return_hint)
         setattr(klass, "app_type", app_type)
+        setattr(klass, "_skip_not_completed", skip_not_completed)
 
         if app_type is not LOADER:
             setattr(klass, "input", None)
diff --git a/src/cogent3/app/io_new.py b/src/cogent3/app/io_new.py
index 7ecc7954c..d078ca063 100644
--- a/src/cogent3/app/io_new.py
+++ b/src/cogent3/app/io_new.py
@@ -176,17 +176,17 @@ def open_data_store(
     return klass(base_path, suffix=suffix, limit=limit)
 
 
-@define_app
+@define_app(skip_not_completed=False)
 def pickle_it(data: SerialisableType) -> bytes:
     return pickle.dumps(data)
 
 
-@define_app
+@define_app(skip_not_completed=False)
 def unpickle_it(data: bytes) -> SerialisableType:
     return pickle.loads(data)
 
 
-@define_app
+@define_app(skip_not_completed=False)
 class compress:
     def __init__(self, compressor: callable = gzip_compress):
         """
@@ -201,7 +201,7 @@ class compress:
         return self.compressor(data)
 
 
-@define_app
+@define_app(skip_not_completed=False)
 class decompress:
     def __init__(self, decompressor: callable = gzip_decompress):
         """
@@ -223,7 +223,7 @@ def _as_dict(obj) -> dict:
     return obj
 
 
-@define_app
+@define_app(skip_not_completed=False)
 class to_primitive:
     """convert an object to primitive python types suitable for serialisation"""
 
@@ -235,7 +235,7 @@ class to_primitive:
         return self.convertor(data)
 
 
-@define_app
+@define_app(skip_not_completed=False)
 class from_primitive:
     """deserialises from primitive python types"""
 
@@ -559,9 +559,12 @@ class write_db:
         identifier
         """
         identifier = identifier or get_data_source(data)
-        data = self.serialiser(data)
+        blob = self.serialiser(data)
 
         if isinstance(data, NotCompleted):
-            return self.data_store.write_incomplete(identifier, data)
+            return self.data_store.write_not_completed(unique_id=identifier, data=blob)
 
-        return self.data_store.write(unique_id=identifier, data=data)
+        if self.data_store.record_type is None:
+            self.data_store.record_type = data
+
+        return self.data_store.write(unique_id=identifier, data=blob)


Test patch:
diff --git a/tests/test_app/test_composable.py b/tests/test_app/test_composable.py
index 25ad01ddf..f29ca1672 100644
--- a/tests/test_app/test_composable.py
+++ b/tests/test_app/test_composable.py
@@ -19,6 +19,7 @@ from cogent3.app import io as io_app
 from cogent3.app import io_new as io_app_new
 from cogent3.app import sample as sample_app
 from cogent3.app import translate, tree
+from cogent3.app import typing as c3types
 from cogent3.app.composable import (
     NON_COMPOSABLE,
     WRITER,
@@ -1131,5 +1132,17 @@ def test_apply_to_only_appends(half_dstore1, half_dstore2):
     _ = process2.apply_to(dstore1, id_from_source=get_data_source)
 
 
+def test_skip_not_completed():
+    @define_app(skip_not_completed=False)
+    def takes_not_completed(val: c3types.SerialisableType) -> dict:
+        return val.to_rich_dict()
+
+    app = takes_not_completed()
+    got = app(NotCompleted("ERROR", "test", "for tracing", source="blah"))
+    assert isinstance(got, dict)
+
+    __app_registry.pop(get_object_provenance(takes_not_completed), None)
+
+
 if __name__ == "__main__":
     main()
diff --git a/tests/test_app/test_io_new.py b/tests/test_app/test_io_new.py
index f6df14f87..82fc60828 100644
--- a/tests/test_app/test_io_new.py
+++ b/tests/test_app/test_io_new.py
@@ -137,7 +137,8 @@ def test_load_unaligned():
 
 
 @pytest.mark.parametrize(
-    "loader", (io_app.load_aligned, io_app.load_unaligned, io_app.load_tabular)
+    "loader",
+    (io_app.load_aligned, io_app.load_unaligned, io_app.load_tabular, io_app.load_db),
 )
 def test_load_nonpath(loader):
     # returns NotCompleted when it's given an alignment/sequence
@@ -445,3 +446,15 @@ def test_write_db_load_db(fasta_dir, tmp_dir):
         m = writer(orig, identifier=m.unique_id)
         read = reader(m)
         assert orig == read
+
+
+def test_write_db_not_completed(tmp_dir):
+    from cogent3.app.sqlite_data_store import DataStoreSqlite
+
+    nc = NotCompleted("ERROR", "test", "for tracing", source="blah")
+    data_store = DataStoreSqlite(tmp_dir / "test.sqlitedb", mode="w")
+    writer = io_app.write_db(data_store=data_store)
+    assert len(data_store.not_completed) == 0
+    writer.main(nc, identifier="blah")
+    assert len(data_store.not_completed) == 1
+    reader = io_app.load_db()