Repository: cogent3/cogent3
Pull request #1121 (2022-08-17 01:33:35)

Composable refactor
Nearly finalised porting writers to new decorator approach


Patch:
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index a392d2eba..17a7729bc 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -14,9 +14,11 @@ from typing import Tuple
 
 from scitrack import CachingLogger
 
-from cogent3 import make_aligned_seqs, make_unaligned_seqs
-from cogent3.app.typing import get_constraint_names
-from cogent3.core.alignment import SequenceCollection
+from cogent3.app.typing import (
+    IdentifierType,
+    SerialisableType,
+    get_constraint_names,
+)
 from cogent3.util import parallel as PAR
 from cogent3.util import progress_display as UI
 from cogent3.util.misc import (
@@ -564,7 +566,7 @@ class ComposableDistance(Composable):
         )
 
 
-class _checkpointable(Composable):
+class _checkpointable:
     def __init__(
         self,
         data_path,
@@ -591,15 +593,11 @@ class _checkpointable(Composable):
         writer_class : type
             constructor for writer
         """
-        super(_checkpointable, self).__init__(**kwargs)
-        self._formatted_params()
-
         data_path = str(data_path)
 
         if data_path.endswith(".tinydb") and not self.__class__.__name__.endswith("db"):
             raise ValueError("tinydb suffix reserved for write_db")
 
-        self._checkpointable = True
         if_exists = if_exists.lower()
         assert if_exists in (
             SKIP,
@@ -848,7 +846,7 @@ def _add(self, other):
 
 
 def _repr(self):
-    val = f"{self.input!r} + " if self.input else ""
+    val = f"{self.input!r} + " if self.app_type is not LOADER and self.input else ""
     data = ", ".join(f"{k}={v!r}" for k, v in self._init_vals.items())
     data = f"{val}{self.__class__.__name__}({data})"
     data = textwrap.fill(data, width=80, break_long_words=False, break_on_hyphens=False)
@@ -925,7 +923,8 @@ def _setstate(self, data):
 
 def _validate_data_type(self, data):
     """checks data class name matches defined compatible types"""
-    if not self._data_types:
+    # todo when move to python 3.8 define protocol checks for the two singular types
+    if not self._data_types or self._data_types <= {SerialisableType, IdentifierType}:
         return True
 
     class_name = data.__class__.__name__
@@ -936,17 +935,6 @@ def _validate_data_type(self, data):
     return valid
 
 
-class _connected:
-    def __init__(self):
-        self.storage = {}
-
-    def __get__(self, instance, owner):
-        return self.storage.get(id(instance), None)
-
-    def __set__(self, instance, value):
-        self.storage[id(instance)] = value
-
-
 __app_registry = {}
 
 
@@ -970,22 +958,14 @@ def define_app(klass=None, *, app_type: AppType = GENERIC, composable: bool = Tr
         for meth in method_list:
             if composable and inspect.isfunction(getattr(klass, meth, None)):
                 raise TypeError(
-                    f"remove {meth!r} method in {klass.__name__!r}, this functionality provided by composable"
+                    f"remove {meth!r} method in {klass.__name__!r}, this functionality provided by define_app"
                 )
         if composable and getattr(klass, "input", None):
             raise TypeError(
-                f"remove 'input' attribute in {klass.__name__!r}, this functionality provided by composable"
+                f"remove 'input' attribute in {klass.__name__!r}, this functionality provided by define_app"
             )
 
         for meth, func in __mapping.items():
-            # check if the developer implements a __getstate__ or
-            # __setstate__ don't introduce our own.
-            if (
-                meth in ["__getstate__", "__setstate__"]
-                and hasattr(klass, meth)
-                and inspect.isfunction(meth)
-            ):
-                continue
             func.__name__ = meth
             setattr(klass, meth, func)
 
@@ -995,11 +975,11 @@ def define_app(klass=None, *, app_type: AppType = GENERIC, composable: bool = Tr
         setattr(klass, "_return_types", return_hint)
         setattr(klass, "app_type", app_type)
 
-        slot_attrs = ["_data_types", "_return_types", "input", "_init_vals"]
+        slot_attrs = ["_data_types", "_return_types", "input", "_init_vals", "__dict__"]
         if app_type == LOADER:
             slot_attrs.remove("input")
         else:
-            setattr(klass, "input", _connected())
+            setattr(klass, "input", None)
         # If a developer has defined slots, we should extend them with our own instance variables.
         if hasattr(klass, "__slots__"):
             klass.__slots__ += tuple(slot_attrs)
@@ -1189,8 +1169,6 @@ __mapping = {
     "__add__": _add,
     "__call__": _call,
     "__repr__": _repr,  # str(obj) calls __repr__ if __str__ missing
-    "__getstate__": _getstate,
-    "__setstate__": _setstate,
     "_serialisable": _ser,
     "_validate_data_type": _validate_data_type,
     "disconnect": _disconnect,
diff --git a/src/cogent3/app/io.py b/src/cogent3/app/io.py
index f6e004f8a..4d34a3393 100644
--- a/src/cogent3/app/io.py
+++ b/src/cogent3/app/io.py
@@ -20,7 +20,13 @@ from cogent3.parse.sequence import PARSERS
 from cogent3.util.deserialise import deserialise_object
 from cogent3.util.table import Table
 
-from .composable import LOADER, WRITER, NotCompleted, _checkpointable, define_app
+from .composable import (
+    LOADER,
+    WRITER,
+    NotCompleted,
+    _checkpointable,
+    define_app,
+)
 from .data_store import (
     IGNORE,
     OVERWRITE,
@@ -41,6 +47,7 @@ from .typing import (
     SEQUENCE_TYPE,
     AlignedSeqsType,
     IdentifierType,
+    SeqsCollectionType,
     SerialisableType,
     TabularType,
     UnalignedSeqsType,
@@ -351,7 +358,7 @@ class load_tabular:
 
 
 @define_app(app_type=WRITER)
-class write_tabular:
+class write_tabular(_checkpointable):
     """writes tabular data"""
 
     def __init__(
@@ -374,6 +381,13 @@ class write_tabular:
             behaviour if output exists. Either 'skip', 'raise' (raises an
             exception), 'overwrite'
         """
+        super().__init__(
+            data_path=data_path,
+            name_callback=name_callback,
+            create=create,
+            if_exists=if_exists,
+            suffix=format,
+        )
         self._format = format
 
     def main(self, data: TabularType, identifier=None) -> IdentifierType:
@@ -391,13 +405,10 @@ class write_tabular:
         return self.data_store.write(identifier, output)
 
 
+@define_app(app_type=WRITER)
 class write_seqs(_checkpointable):
     """Writes sequences to text files in standard format."""
 
-    _input_types = (SEQUENCE_TYPE, ALIGNED_TYPE)
-    _output_types = (SEQUENCE_TYPE, ALIGNED_TYPE, IDENTIFIER_TYPE)
-    _data_types = ("ArrayAlignment", "Alignment", "SequenceCollection")
-
     def __init__(
         self,
         data_path,
@@ -426,17 +437,13 @@ class write_seqs(_checkpointable):
             behaviour if output exists. Either 'skip', 'raise' (raises an
             exception), 'overwrite'
         """
-        super(write_seqs, self).__init__(
-            input_types=self._input_types,
-            output_types=self._output_types,
-            data_types=self._data_types,
+        super().__init__(
             data_path=data_path,
             name_callback=name_callback,
             create=create,
             if_exists=if_exists,
             suffix=suffix,
         )
-        self._formatted_params()
         self._format = format
         self._formatter = FORMATTERS[format]
 
@@ -445,7 +452,7 @@ class write_seqs(_checkpointable):
         loader = loader(format=self._format)
         self._load_checkpoint = loader
 
-    def main(self, data, identifier=None):
+    def main(self, data: SeqsCollectionType, identifier=None) -> IdentifierType:
         from cogent3.app.composable import NotCompleted
 
         if isinstance(data, NotCompleted):
@@ -475,9 +482,6 @@ class load_json:
     """Loads json serialised cogent3 objects from a json file.
     Returns whatever object type was stored."""
 
-    def __init__(self):
-        pass
-
     def main(self, path: IdentifierType) -> SerialisableType:
         """returns object deserialised from json at path"""
         if type(path) == str:
@@ -499,20 +503,38 @@ class load_json:
 
 
 @define_app(app_type=WRITER)
-class write_json:
+class write_json(_checkpointable):
     """Writes json serialised objects to individual json files."""
 
-    def __init__(
-        self, data_path, name_callback=None, create=False, if_exists=SKIP, suffix="json"
-    ):
-        pass
+    def __init__(self, data_path, name_callback=None, create=False, if_exists=SKIP):
+        """
+        Parameters
+        ----------
+        data_path
+            path to write output, if ends with .zip will be a compressed zip
+            archive
+        name_callback
+            function that takes the data object and returns a base
+            file name
+        create : bool
+            whether to create the output directory
+        if_exists : str
+            behaviour if output exists. Either 'skip', 'raise' (raises an
+            exception), 'overwrite'
+        """
+        super().__init__(
+            data_path=data_path,
+            name_callback=name_callback,
+            create=create,
+            if_exists=if_exists,
+            suffix="json",
+        )
+        self._format = "json"
 
     def _set_checkpoint_loader(self):
         self._load_checkpoint = self
 
-    T = Union[SerialisableType, IdentifierType]
-
-    def main(self, data: SerialisableType, identifier=None) -> T:
+    def main(self, data: SerialisableType, identifier=None) -> IdentifierType:
         if isinstance(data, NotCompleted):
             return self.data_store.write_incomplete(identifier, data)
 
@@ -568,13 +590,18 @@ class load_db:
 
 
 @define_app(app_type=WRITER)
-class write_db:
+class write_db(_checkpointable):
     """Writes json serialised objects to a TinyDB instance."""
 
-    def __init__(
-        self, data_path, name_callback=None, create=False, if_exists=SKIP, suffix="json"
-    ):
-        pass
+    def __init__(self, data_path, name_callback=None, create=False, if_exists=SKIP):
+        super().__init__(
+            data_path=data_path,
+            name_callback=name_callback,
+            create=create,
+            if_exists=if_exists,
+            suffix="json",
+            writer_class=WritableTinyDbDataStore,
+        )
 
     def _set_checkpoint_loader(self):
         self._load_checkpoint = self
diff --git a/src/cogent3/app/sample.py b/src/cogent3/app/sample.py
index aa890b6c5..727c5e352 100644
--- a/src/cogent3/app/sample.py
+++ b/src/cogent3/app/sample.py
@@ -129,7 +129,6 @@ class omit_degenerates:
             assert moltype.label.lower() in ("dna", "rna"), "Invalid moltype"
 
         self.moltype = moltype
-        self._no_degen = omit_degenerates
         self._allow_gap = not gap_is_degen
         self._motif_length = motif_length
 
diff --git a/src/cogent3/app/typing.py b/src/cogent3/app/typing.py
index 3ff524d58..3a127ad95 100644
--- a/src/cogent3/app/typing.py
+++ b/src/cogent3/app/typing.py
@@ -40,7 +40,7 @@ ResultType = Union[
     TabularResultType,
 ]
 
-# todo the following should have bound values of an abstract base class
+# todo when move to python 3.8 define protocols for IdentifierType and SerialisableType
 IdentifierType = TypeVar("IdentifierType")
 
 # the following constants are deprecated
@@ -74,6 +74,10 @@ 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 hint in (SerialisableType, IdentifierType):
+            all_hints.add(hint)
+            continue
+
         if getattr(hint, "__bound__", None):
             all_hints.add(hint.__bound__)
             continue


Test patch:
diff --git a/tests/test_app/test_composable.py b/tests/test_app/test_composable.py
index aeffa1d59..4c5905b4c 100644
--- a/tests/test_app/test_composable.py
+++ b/tests/test_app/test_composable.py
@@ -1,5 +1,6 @@
 import os
 import pathlib
+import pickle
 
 from pickle import dumps, loads
 from tempfile import TemporaryDirectory
@@ -107,7 +108,6 @@ class TestComposableBase(TestCase):
         with self.assertRaises(ValueError):
             proc.apply_to(["", ""])
 
-    @pytest.mark.xfail
     def test_apply_to_strings(self):
         """apply_to handles strings as paths"""
         dstore = io_app.get_data_store("data", suffix="fasta", limit=3)
@@ -136,7 +136,6 @@ class TestComposableBase(TestCase):
             with self.assertRaises(ValueError):
                 process.apply_to(dstore)
 
-    @pytest.mark.xfail
     def test_apply_to_logging(self):
         """correctly creates log file"""
         dstore = io_app.get_data_store("data", suffix="fasta", limit=3)
@@ -151,7 +150,6 @@ class TestComposableBase(TestCase):
             self.assertEqual(len(process.data_store.logs), 1)
             process.data_store.close()
 
-    @pytest.mark.xfail
     def test_apply_to_logger(self):
         """correctly uses user provided logger"""
         dstore = io_app.get_data_store("data", suffix="fasta", limit=3)
@@ -166,7 +164,6 @@ class TestComposableBase(TestCase):
             self.assertEqual(len(process.data_store.logs), 1)
             process.data_store.close()
 
-    @pytest.mark.xfail
     def test_apply_to_invalid_logger(self):
         """incorrect logger value raises TypeError"""
         dstore = io_app.get_data_store("data", suffix="fasta", limit=3)
@@ -181,7 +178,6 @@ class TestComposableBase(TestCase):
                     process.apply_to(dstore, show_progress=False, logger=logger_val)
                 process.data_store.close()
 
-    @pytest.mark.xfail
     def test_apply_to_not_completed(self):
         """correctly creates notcompleted"""
         dstore = io_app.get_data_store("data", suffix="fasta", limit=3)
@@ -196,7 +192,6 @@ class TestComposableBase(TestCase):
             self.assertEqual(len(process.data_store.incomplete), 3)
             process.data_store.close()
 
-    @pytest.mark.xfail
     def test_apply_to_not_partially_done(self):
         """correctly applies process when result already partially done"""
         dstore = io_app.get_data_store("data", suffix="fasta")
@@ -519,5 +514,16 @@ def test_app_is_not_composable():
     assert not is_composable(foo)
 
 
+def test_composed_func_pickleable():
+    from cogent3.app.sample import min_length, omit_degenerates
+
+    ml = min_length(100)
+    no_degen = omit_degenerates(moltype="dna")
+    app = ml + no_degen
+
+    unpickled = pickle.loads(pickle.dumps((app)))
+    assert unpickled.input is not None
+
+
 if __name__ == "__main__":
     main()
diff --git a/tests/test_app/test_dist.py b/tests/test_app/test_dist.py
index 8bc5354a7..be809856e 100644
--- a/tests/test_app/test_dist.py
+++ b/tests/test_app/test_dist.py
@@ -11,6 +11,7 @@ from cogent3 import DNA, PROTEIN, make_unaligned_seqs
 from cogent3.app import align
 from cogent3.app import dist as dist_app
 from cogent3.app import io, sample
+from cogent3.app.composable import WRITER
 from cogent3.evolve.fast_distance import HammingPair, TN93Pair
 
 
@@ -119,21 +120,23 @@ class FastSlowDistTests(TestCase):
         with self.assertRaises(ValueError):
             fast_slow_dist = dist_app.fast_slow_dist(fast_calc="GTR")
 
-    @pytest.mark.xfail
     def test_composable_apps(self):
         """tests two composable apps"""
         composable_apps = _get_all_composable_apps()
-        fast_slow_dist = dist_app.fast_slow_dist(fast_calc="hamming", moltype="dna")
+        calc_dist = dist_app.fast_slow_dist(fast_calc="hamming", moltype="dna")
         for app in composable_apps:
+            if app.app_type == WRITER:
+                # cannot have a WRITER before a GENERIC
+                continue
             # Compose two composable applications, there should not be exceptions.
-            got = app + fast_slow_dist
+            got = app + calc_dist
             self.assertIsInstance(got, dist_app.fast_slow_dist)
             self.assertIs(got.input, app)
             self.assertIsInstance(got._data_types, frozenset)
             self.assertIsInstance(got._return_types, frozenset)
             self.assertIs(got.input, app)
             app.disconnect()
-            fast_slow_dist.disconnect()
+            calc_dist.disconnect()
 
     def test_est_dist_pair_slow(self):
         """tests the distance between seq pairs in aln"""
@@ -213,7 +216,6 @@ class FastSlowDistTests(TestCase):
             dist_calc = dist_app.fast_slow_dist(distance="hamming", moltype="protein")
             _ = dist_calc + writer
 
-    @pytest.mark.xfail
     def test_functions_as_composable(self):
         """works as a composable app"""
         from pathlib import Path
diff --git a/tests/test_app/test_evo.py b/tests/test_app/test_evo.py
index 8775baac5..506dc223b 100644
--- a/tests/test_app/test_evo.py
+++ b/tests/test_app/test_evo.py
@@ -849,7 +849,6 @@ class TestBootstrap(TestCase):
         # correct message being relayed
         self.assertTrue("ValueError: '-' at" in result.message)
 
-    @pytest.mark.xfail
     def test_bstrap_parallel(self):
         """exercising bootstrap with parallel"""
         aln = load_aligned_seqs(join(data_dir, "brca1.fasta"), moltype="dna")
diff --git a/tests/test_app/test_init.py b/tests/test_app/test_init.py
index 9d2d78a90..320ee48b6 100644
--- a/tests/test_app/test_init.py
+++ b/tests/test_app/test_init.py
@@ -73,13 +73,13 @@ class TestAvailableApps(TestCase):
             applications = _get_all_composables(os.path.join(dirname, "delme"))
 
             for app in applications:
-                self.assertTrue(is_composable(app))
+                self.assertTrue(is_composable(app), msg=app)
 
             composable_application_tuples = [
                 (app1, app2)
                 for app1 in applications
                 for app2 in applications
-                if app1 != app2 and app1._output_types & app2._input_types != set()
+                if app1 != app2 and (app1._return_types & app2._data_types != set())
             ]
 
             for composable_application_tuple in composable_application_tuples:
diff --git a/tests/test_app/test_io.py b/tests/test_app/test_io.py
index 937210896..9cd537d41 100644
--- a/tests/test_app/test_io.py
+++ b/tests/test_app/test_io.py
@@ -210,7 +210,6 @@ class TestIo(TestCase):
             self.assertIsInstance(got, DNA.__class__)
             self.assertEqual(got, DNA)
 
-    @pytest.mark.xfail
     def test_write_db_load_db(self):
         """correctly write/load from tinydb"""
         # straight directory
@@ -229,7 +228,6 @@ class TestIo(TestCase):
             self.assertIsInstance(got["dna"], DNA.__class__)
             self.assertEqual(got["dna"], DNA)
 
-    @pytest.mark.xfail
     def test_write_db_load_db2(self):
         """correctly write/load built-in python from tinydb"""
         with TemporaryDirectory(dir=".") as dirname:
@@ -244,7 +242,6 @@ class TestIo(TestCase):
             dstore.close()
             self.assertEqual(got, data)
 
-    @pytest.mark.xfail
     def test_load_db_failure_json_file(self):
         """informative load_db error message when given a json file path"""
         # todo this test has a trapped exception about being unable to delete
@@ -286,7 +283,6 @@ class TestIo(TestCase):
             result = load_table(outpath)
             self.assertIsInstance(result, NotCompleted)
 
-    @pytest.mark.xfail
     def test_write_tabular_motif_counts_array(self):
         """correctly writes tabular data for MotifCountsArray"""
 
@@ -312,7 +308,6 @@ class TestIo(TestCase):
             }
             self.assertEqual(expected, new.to_dict())
 
-    @pytest.mark.xfail
     def test_write_tabular_motif_freqs_array(self):
         """correctly writes tabular data for MotifFreqsArray"""
 
@@ -337,7 +332,6 @@ class TestIo(TestCase):
             }
             self.assertEqual(expected, new.to_dict())
 
-    @pytest.mark.xfail
     def test_write_tabular_pssm(self):
         """correctly writes tabular data for PSSM"""
 
@@ -365,7 +359,6 @@ class TestIo(TestCase):
                     numpy.isclose(new.array[i][2], expected[j][i - j], atol=0.0001)
                 )
 
-    @pytest.mark.xfail
     def test_write_tabular_distance_matrix(self):
         """correctly writes tabular data for DistanceMatrix"""
         data = {(0, 0): 0, (0, 1): 4, (1, 0): 4, (1, 1): 0}
@@ -385,7 +378,6 @@ class TestIo(TestCase):
             }
             self.assertEqual(expected, new.to_dict())
 
-    @pytest.mark.xfail
     def test_write_tabular_table(self):
         """correctly writes tabular data"""
         rows = [[1, 2], [3, 4], [5, 6.5]]
@@ -398,7 +390,6 @@ class TestIo(TestCase):
             new = loader(outpath)
             self.assertEqual(table.to_dict(), new.to_dict())
 
-    @pytest.mark.xfail
     def test_load_tabular_motif_counts_array(self):
         """correctly loads tabular data for MotifCountsArray"""
 
@@ -412,7 +403,6 @@ class TestIo(TestCase):
             new = loader(outpath)
             self.assertEqual(mca.to_dict(), new.to_dict())
 
-    @pytest.mark.xfail
     def test_load_tabular_motif_freqs_array(self):
         """correctly loads tabular data for MotifFreqsArray"""
 
@@ -426,7 +416,6 @@ class TestIo(TestCase):
             new = loader(outpath)
             self.assertEqual(mfa.to_dict(), new.to_dict())
 
-    @pytest.mark.xfail
     def test_load_tabular_pssm(self):
         """correctly loads tabular data for PSSM"""
 
@@ -447,7 +436,6 @@ class TestIo(TestCase):
             new = loader(outpath)
             assert_allclose(pssm.array, new.array, atol=0.0001)
 
-    @pytest.mark.xfail
     def test_load_tabular_distance_matrix(self):
         """correctly loads tabular data for DistanceMatrix"""
         data = {(0, 0): 0, (0, 1): 4, (1, 0): 4, (1, 1): 0}
@@ -460,7 +448,6 @@ class TestIo(TestCase):
             new = loader(outpath)
             self.assertEqual(matrix.to_dict(), new.to_dict())
 
-    @pytest.mark.xfail
     def test_load_tabular_table(self):
         """correctly loads tabular data"""
         rows = [[1, 2], [3, 4], [5, 6.5]]
@@ -473,7 +460,6 @@ class TestIo(TestCase):
             new = loader(outpath)
             self.assertEqual(table.to_dict(), new.to_dict())
 
-    @pytest.mark.xfail
     def test_write_json_with_info(self):
         """correctly writes an object with info attribute from json"""
         # create a mock object that pretends like it's been derived from
@@ -493,7 +479,6 @@ class TestIo(TestCase):
             got.deserialised_values()
             self.assertEqual(got["dna"], DNA)
 
-    @pytest.mark.xfail
     def test_write_json_no_info(self):
         """correctly writes an object with out an info attribute from json"""
         # create a mock object that pretends like it's been derived from
@@ -508,7 +493,6 @@ class TestIo(TestCase):
             got.deserialised_values()
             self.assertEqual(got["dna"], DNA)
 
-    @pytest.mark.xfail
     def test_restricted_usage_of_tinydb_suffix(self):
         """can only use tinydb in a load_db, write_db context"""
         with TemporaryDirectory(dir=".") as dirname:
@@ -524,7 +508,6 @@ class TestIo(TestCase):
             w = io_app.write_db(outdir, create=True, if_exists="skip")
             w.data_store.close()
 
-    @pytest.mark.xfail
     def test_write_db_parallel(self):
         """writing with overwrite in parallel should reset db"""
         with TemporaryDirectory(dir=".") as dirname:
@@ -541,15 +524,14 @@ class TestIo(TestCase):
             r = process.apply_to(
                 members, show_progress=False, parallel=True, cleanup=True
             )
-
             expect = [str(m) for m in process.data_store]
             process.data_store.close()
 
             # now get read only and check what's in there
             result = io_app.get_data_store(outdir)
             got = [str(m) for m in result]
+            self.assertNotEqual(got, [])
             result.close()
-
             self.assertEqual(got, expect)
 
 
diff --git a/tests/test_app/test_typing.py b/tests/test_app/test_typing.py
index a51b70c70..a15d4ebec 100644
--- a/tests/test_app/test_typing.py
+++ b/tests/test_app/test_typing.py
@@ -55,11 +55,11 @@ def test_get_constraint_names_builtins():
     assert got == {"str", "bytes"}
 
 
-def test_get_constraint_names_no_constraint():
+def test_get_constraint_names_serilisable():
     """SerialisableType does not define any compatible types"""
 
     got = get_constraint_names(SerialisableType)
-    assert got == set()
+    assert got == {SerialisableType}
 
 
 def test_hints_from_strings_invalid():
@@ -71,6 +71,3 @@ def test_hints_from_strings_invalid():
 def test_hints_from_strings():
     got = hints_from_strings("serialisable", "aligned")
     assert got == [SerialisableType, AlignedSeqsType]
-
-
-test_get_constraint_names_no_constraint()