Repository: cogent3/cogent3
Pull request #1072 (2022-07-04 01:24:05)

ENH: initiate conversion composable apps to musing type hints
[NEW] the cogent3/app/typing.py module contain type hints for
    app interchange. These will wind up being applied to Composable._in
    and Composable.._out attributes.


Patch:
diff --git a/src/cogent3/app/align.py b/src/cogent3/app/align.py
index d67fa5e9c..3bb6e6d49 100644
--- a/src/cogent3/app/align.py
+++ b/src/cogent3/app/align.py
@@ -15,14 +15,9 @@ from cogent3.core.location import gap_coords_to_map
 from cogent3.core.moltype import get_moltype
 from cogent3.evolve.models import get_model
 
-from .composable import (
-    ALIGNED_TYPE,
-    SEQUENCE_TYPE,
-    SERIALISABLE_TYPE,
-    ComposableSeq,
-    NotCompleted,
-)
+from .composable import ComposableSeq, NotCompleted
 from .tree import quick_tree, scale_branches
+from .typing import ALIGNED_TYPE, SEQUENCE_TYPE, SERIALISABLE_TYPE
 
 
 __author__ = "Gavin Huttley"
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index f0ae0f99a..183894a95 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -122,21 +122,6 @@ class NotCompleted(int):
         return json.dumps(self.to_rich_dict())
 
 
-ALIGNED_TYPE = "aligned"
-IDENTIFIER_TYPE = "identifier"
-PAIRWISE_DISTANCE_TYPE = "pairwise_distances"
-SEQUENCE_TYPE = "sequences"
-SERIALISABLE_TYPE = "serialisable"
-TABULAR_TYPE = "tabular"
-TREE_TYPE = "tree"
-
-BOOTSTRAP_RESULT_TYPE = "bootstrap_result"
-HYPOTHESIS_RESULT_TYPE = "hypothesis_result"
-MODEL_RESULT_TYPE = "model_result"
-RESULT_TYPE = "result"
-TABULAR_RESULT_TYPE = "tabular_result"
-
-
 class ComposableType:
     _type = None
 
diff --git a/src/cogent3/app/dist.py b/src/cogent3/app/dist.py
index 41c85a360..a7c53db57 100644
--- a/src/cogent3/app/dist.py
+++ b/src/cogent3/app/dist.py
@@ -7,12 +7,12 @@ from cogent3.evolve.fast_distance import (
 )
 from cogent3.evolve.models import get_model
 
-from .composable import (
+from .composable import ComposableDistance
+from .typing import (
     ALIGNED_TYPE,
     PAIRWISE_DISTANCE_TYPE,
     SERIALISABLE_TYPE,
     TABULAR_TYPE,
-    ComposableDistance,
 )
 
 
diff --git a/src/cogent3/app/evo.py b/src/cogent3/app/evo.py
index b7645f0a0..33bb05d4c 100644
--- a/src/cogent3/app/evo.py
+++ b/src/cogent3/app/evo.py
@@ -8,13 +8,6 @@ from cogent3.evolve.models import get_model
 from cogent3.util import parallel
 
 from .composable import (
-    ALIGNED_TYPE,
-    BOOTSTRAP_RESULT_TYPE,
-    HYPOTHESIS_RESULT_TYPE,
-    MODEL_RESULT_TYPE,
-    RESULT_TYPE,
-    SERIALISABLE_TYPE,
-    TABULAR_RESULT_TYPE,
     ComposableHypothesis,
     ComposableModel,
     ComposableTabular,
@@ -27,6 +20,15 @@ from .result import (
     model_result,
     tabular_result,
 )
+from .typing import (
+    ALIGNED_TYPE,
+    BOOTSTRAP_RESULT_TYPE,
+    HYPOTHESIS_RESULT_TYPE,
+    MODEL_RESULT_TYPE,
+    RESULT_TYPE,
+    SERIALISABLE_TYPE,
+    TABULAR_RESULT_TYPE,
+)
 
 
 __author__ = "Gavin Huttley"
diff --git a/src/cogent3/app/io.py b/src/cogent3/app/io.py
index f56117804..ceebad018 100644
--- a/src/cogent3/app/io.py
+++ b/src/cogent3/app/io.py
@@ -21,13 +21,6 @@ from cogent3.util.deserialise import deserialise_object
 from cogent3.util.table import Table
 
 from .composable import (
-    ALIGNED_TYPE,
-    IDENTIFIER_TYPE,
-    PAIRWISE_DISTANCE_TYPE,
-    SEQUENCE_TYPE,
-    SERIALISABLE_TYPE,
-    TABULAR_RESULT_TYPE,
-    TABULAR_TYPE,
     Composable,
     ComposableAligned,
     ComposableSeq,
@@ -49,6 +42,15 @@ from .data_store import (
     load_record_from_json,
     make_record_for_json,
 )
+from .typing import (
+    ALIGNED_TYPE,
+    IDENTIFIER_TYPE,
+    PAIRWISE_DISTANCE_TYPE,
+    SEQUENCE_TYPE,
+    SERIALISABLE_TYPE,
+    TABULAR_RESULT_TYPE,
+    TABULAR_TYPE,
+)
 
 
 __author__ = "Gavin Huttley"
diff --git a/src/cogent3/app/sample.py b/src/cogent3/app/sample.py
index d3414f90a..a95732e4b 100644
--- a/src/cogent3/app/sample.py
+++ b/src/cogent3/app/sample.py
@@ -7,15 +7,9 @@ from cogent3.core.alignment import Alignment, ArrayAlignment
 from cogent3.core.genetic_code import get_code
 from cogent3.core.moltype import get_moltype
 
-from .composable import (
-    ALIGNED_TYPE,
-    SEQUENCE_TYPE,
-    SERIALISABLE_TYPE,
-    ComposableAligned,
-    ComposableSeq,
-    NotCompleted,
-)
+from .composable import ComposableAligned, ComposableSeq, NotCompleted
 from .translate import get_fourfold_degenerate_sets
+from .typing import ALIGNED_TYPE, SEQUENCE_TYPE, SERIALISABLE_TYPE
 
 
 __author__ = "Gavin Huttley"
diff --git a/src/cogent3/app/translate.py b/src/cogent3/app/translate.py
index 980b6a827..e4b2a8ae7 100644
--- a/src/cogent3/app/translate.py
+++ b/src/cogent3/app/translate.py
@@ -4,12 +4,8 @@ from cogent3.core.alignment import SequenceCollection
 from cogent3.core.genetic_code import get_code
 from cogent3.core.moltype import get_moltype
 
-from .composable import (
-    ALIGNED_TYPE,
-    SEQUENCE_TYPE,
-    ComposableSeq,
-    NotCompleted,
-)
+from .composable import ComposableSeq, NotCompleted
+from .typing import ALIGNED_TYPE, SEQUENCE_TYPE
 
 
 __author__ = "Gavin Huttley"
diff --git a/src/cogent3/app/tree.py b/src/cogent3/app/tree.py
index 628b95615..7e566ef20 100644
--- a/src/cogent3/app/tree.py
+++ b/src/cogent3/app/tree.py
@@ -1,12 +1,8 @@
 from cogent3 import make_tree
 from cogent3.phylo.nj import gnj
 
-from .composable import (
-    PAIRWISE_DISTANCE_TYPE,
-    SERIALISABLE_TYPE,
-    TREE_TYPE,
-    ComposableTree,
-)
+from .composable import ComposableTree
+from .typing import PAIRWISE_DISTANCE_TYPE, SERIALISABLE_TYPE, TREE_TYPE
 
 
 __author__ = "Gavin Huttley"
diff --git a/src/cogent3/app/typing.py b/src/cogent3/app/typing.py
new file mode 100644
index 000000000..b84e2e6f2
--- /dev/null
+++ b/src/cogent3/app/typing.py
@@ -0,0 +1,104 @@
+"""defined type hints for app composability"""
+# todo write more extensive docstring explaining limited use of these types
+from typing import (
+    ForwardRef,
+    Iterable,
+    TypeVar,
+    Union,
+    get_args,
+    get_origin,
+    get_type_hints,
+)
+
+
+__author__ = "Gavin Huttley"
+__copyright__ = "Copyright 2007-2022, The Cogent Project"
+__credits__ = ["Gavin Huttley"]
+__license__ = "BSD-3"
+__version__ = "2022.5.25a1"
+__maintainer__ = "Gavin Huttley"
+__email__ = "Gavin.Huttley@anu.edu.au"
+__status__ = "Alpha"
+
+AlignedSeqsType = TypeVar("AlignedSeqsType", "Alignment", "ArrayAlignment")
+UnalignedSeqsType = TypeVar("UnalignedSeqsType", bound="SequenceCollection")
+SeqsCollectionType = Union[AlignedSeqsType, UnalignedSeqsType]
+SeqType = TypeVar("SeqType", bound="Sequence")
+PairwiseDistanceType = TypeVar("PairwiseDistanceType", bound="DistanceMatrix")
+TabularType = TypeVar("TabularType", "Table", "DictArray", "DistanceMatrix")
+TreeType = TypeVar("TreeType", "TreeNode", "PhyloNode")
+SerialisableType = TypeVar("SerialisableType")
+BootstrapResultType = TypeVar("BootstrapResultType", bound="bootstrap_result")
+HypothesisResultType = TypeVar("HypothesisResultType", bound="hypothesis_result")
+ModelResultType = TypeVar("ModelResultType", bound="model_result")
+TabularResultType = TypeVar("TabularResultType", bound="tabular_result")
+GenericResultType = TypeVar("GenericResultType", bound="generic_result")
+ResultType = Union[
+    GenericResultType,
+    BootstrapResultType,
+    HypothesisResultType,
+    ModelResultType,
+    TabularResultType,
+]
+
+# todo the following should have bound values of an abstract base class
+IdentifierType = TypeVar("IdentifierType")
+
+# the following constants are deprecated
+ALIGNED_TYPE = "aligned"
+IDENTIFIER_TYPE = "identifier"
+PAIRWISE_DISTANCE_TYPE = "pairwise_distances"
+SEQUENCE_TYPE = "sequences"
+SERIALISABLE_TYPE = "serialisable"
+TABULAR_TYPE = "tabular"
+TREE_TYPE = "tree"
+BOOTSTRAP_RESULT_TYPE = "bootstrap_result"
+HYPOTHESIS_RESULT_TYPE = "hypothesis_result"
+MODEL_RESULT_TYPE = "model_result"
+RESULT_TYPE = "result"
+TABULAR_RESULT_TYPE = "tabular_result"
+
+_mappings = {
+    TABULAR_TYPE: TabularType,
+    ALIGNED_TYPE: AlignedSeqsType,
+    SEQUENCE_TYPE: SeqsCollectionType,
+    IDENTIFIER_TYPE: IdentifierType,
+    PAIRWISE_DISTANCE_TYPE: PairwiseDistanceType,
+    SERIALISABLE_TYPE: SerialisableType,
+    TREE_TYPE: TreeType,
+    BOOTSTRAP_RESULT_TYPE: BootstrapResultType,
+    HYPOTHESIS_RESULT_TYPE: HypothesisResultType,
+}
+
+
+def get_constraint_names(*hints) -> set[str, ...]:
+    """returns the set of named constraints of a type hint"""
+    all_hints = set()
+    for hint in hints:
+        if getattr(hint, "__bound__", None):
+            all_hints.add(hint.__bound__)
+            continue
+
+        if getattr(hint, "__constraints__", None):
+            all_hints.update(hint.__constraints__)
+            continue
+
+        if get_origin(hint) == Union:
+            all_hints.update(get_constraint_names(*get_args(hint)))
+
+        if type(hint) == type:
+            all_hints.add(hint.__name__)
+
+    all_hints = {h.__forward_arg__ if type(h) == ForwardRef else h for h in all_hints}
+    return all_hints
+
+
+def hints_from_strings(*strings: Iterable[str]) -> list:
+    """returns list of type hints corresponding to string values"""
+    types = []
+    for string in strings:
+        string = string.lower()
+        if string not in _mappings:
+            raise ValueError(f"{string!r} not a known type constant")
+        types.append(_mappings[string])
+    return types


Test patch:
diff --git a/tests/test_app/test_composable.py b/tests/test_app/test_composable.py
index 26e169e85..692b5e573 100644
--- a/tests/test_app/test_composable.py
+++ b/tests/test_app/test_composable.py
@@ -11,7 +11,6 @@ from scitrack import CachingLogger
 from cogent3.app import io as io_app
 from cogent3.app import sample as sample_app
 from cogent3.app.composable import (
-    SERIALISABLE_TYPE,
     ComposableSeq,
     NotCompleted,
     appify,
@@ -20,6 +19,7 @@ from cogent3.app.composable import (
 from cogent3.app.sample import min_length, omit_degenerates
 from cogent3.app.translate import select_translatable
 from cogent3.app.tree import quick_tree
+from cogent3.app.typing import SERIALISABLE_TYPE
 from cogent3.core.alignment import ArrayAlignment
 
 
diff --git a/tests/test_app/test_typing.py b/tests/test_app/test_typing.py
new file mode 100644
index 000000000..539e25289
--- /dev/null
+++ b/tests/test_app/test_typing.py
@@ -0,0 +1,73 @@
+import pytest
+
+from cogent3.app.typing import (
+    AlignedSeqsType,
+    SeqsCollectionType,
+    SerialisableType,
+    TabularType,
+    UnalignedSeqsType,
+    get_constraint_names,
+    hints_from_strings,
+)
+
+
+__author__ = "Gavin Huttley"
+__copyright__ = "Copyright 2007-2022, The Cogent Project"
+__credits__ = ["Gavin Huttley"]
+__license__ = "BSD-3"
+__version__ = "2022.5.25a1"
+__maintainer__ = "Gavin Huttley"
+__email__ = "Gavin.Huttley@anu.edu.au"
+__status__ = "Production"
+
+
+def test_get_constraint_names():
+    """returns the correct names"""
+    from cogent3.core.alignment import (
+        Alignment,
+        ArrayAlignment,
+        SequenceCollection,
+    )
+    from cogent3.evolve.fast_distance import DistanceMatrix
+    from cogent3.util.dict_array import DictArray
+    from cogent3.util.table import Table
+
+    got = get_constraint_names(AlignedSeqsType)
+    assert got == {obj.__name__ for obj in (Alignment, ArrayAlignment)}
+    got = get_constraint_names(UnalignedSeqsType)
+    assert got == {SequenceCollection.__name__}
+
+    got = get_constraint_names(SeqsCollectionType)
+    assert got == {
+        obj.__name__ for obj in (Alignment, ArrayAlignment, SequenceCollection)
+    }
+
+    got = get_constraint_names(TabularType)
+
+    assert got == {obj.__name__ for obj in (Table, DictArray, DistanceMatrix)}
+
+
+def test_get_constraint_names_builtins():
+    """handles built-in types"""
+    from typing import Union
+
+    got = get_constraint_names(Union[str, bytes])
+    assert got == {"str", "bytes"}
+
+
+def test_get_constraint_names_no_constraint():
+    """SerialisableType does not define any compatible types"""
+
+    got = get_constraint_names(SerialisableType)
+    assert got == set()
+
+
+def test_hints_from_strings_invalid():
+    """raise an exception if unknown string"""
+    with pytest.raises(ValueError):
+        hints_from_strings("abcde")
+
+
+def test_hints_from_strings():
+    got = hints_from_strings("serialisable", "aligned")
+    assert got == [SerialisableType, AlignedSeqsType]