Repository: cogent3/cogent3
Pull request #2323 (2025-05-20 02:01:20)

ENH: cogent3.load_annotations() supports loading from json
[NEW] any json file with a registered deserialiser is supported
    for recreating an annotation database

## Summary by Sourcery

Enable load_annotations to load annotation databases directly from JSON files by invoking the registered deserialiser and ensure write() properly expands user paths.

New Features:
- Allow load_annotations to accept and reconstruct annotation databases from JSON files

Enhancements:
- Expand user home in write() path handling
- Update load_annotations docstring to include JSON support

Tests:
- Add test to verify loading annotations from JSON serialization


Patch:
diff --git a/changelog.d/20250520_120130_Gavin.Huttley.md b/changelog.d/20250520_120130_Gavin.Huttley.md
new file mode 100644
index 000000000..beb4dd47e
--- /dev/null
+++ b/changelog.d/20250520_120130_Gavin.Huttley.md
@@ -0,0 +1,40 @@
+<!--
+A new scriv changelog fragment.
+
+Uncomment the section that is right (remove the HTML comment wrapper).
+-->
+
+
+### Contributors
+
+- @GavinHuttley
+
+### Enhancements
+
+- Now support loading an annotation database that has been serialised using json and
+  has a registered deserialiser.
+
+<!--
+### Bug fixes
+
+- A bullet item for the Bug fixes category.
+
+-->
+<!--
+### Documentation
+
+- A bullet item for the Documentation category.
+
+-->
+<!--
+### Deprecations
+
+- A bullet item for the Deprecations category.
+
+-->
+<!--
+### Discontinued
+
+- A bullet item for the Discontinued category.
+
+-->
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index 423096cfb..35295807e 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -19,7 +19,7 @@ from cogent3._version import __version__
 from cogent3.core.location import Strand
 from cogent3.core.table import Table
 from cogent3.parse.gff import merged_gff_records
-from cogent3.util.deserialise import register_deserialiser
+from cogent3.util.deserialise import deserialise_object, register_deserialiser
 from cogent3.util.io import PathType, iter_line_blocks
 from cogent3.util.misc import extend_docstring_from, get_object_provenance
 from cogent3.util.progress_display import display_wrap
@@ -1258,6 +1258,7 @@ class SqliteAnnotationDbMixin:
 
     def write(self, path: PathType) -> None:
         """writes db as bytes to path"""
+        path = pathlib.Path(path).expanduser()
         backup = sqlite3.connect(path)
         with self.db:
             self.db.backup(backup)
@@ -1851,7 +1852,8 @@ def load_annotations(
     Parameters
     ----------
     path
-        path to a plain text file containing features
+        path to a plain text file containing annotations, or a json file
+        of a serialised cogent3 annotation db object
     seqids
         only features whose seqid matches a provided identifier are returned,
         the default is all features.
@@ -1874,6 +1876,9 @@ def load_annotations(
     if seqids is not None:
         seqids = {seqids} if isinstance(seqids, str) else set(seqids)
     path = pathlib.Path(path).expanduser()
+    if any(s.lower() == ".json" for s in path.suffixes):
+        return deserialise_object(path)
+
     return (
         _db_from_genbank(
             path,
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 19417c886..11c2bcae8 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -1500,7 +1500,7 @@ class SequenceCollection:
         width: int = 500,
         title: OptStr = None,
         rc: bool = False,
-        biotype: str | tuple[str] = "gene",
+        biotype: str | tuple[str] | None = None,
         show_progress: bool = False,
     ):
         """make a dotplot between specified sequences. Random sequences


Test patch:
diff --git a/tests/test_core/test_annotation_db.py b/tests/test_core/test_annotation_db.py
index 86da1dc0f..e97dac9c6 100644
--- a/tests/test_core/test_annotation_db.py
+++ b/tests/test_core/test_annotation_db.py
@@ -1221,6 +1221,19 @@ def test_load_anns_with_write(DATA_DIR, tmp_dir):
     assert got_data["tables"] == expect_data["tables"]
 
 
+def test_load_anns_from_json(DATA_DIR, tmp_dir):
+    inpath = DATA_DIR / "simple.gff"
+    orig = load_annotations(path=inpath)
+
+    outpath = tmp_dir / "simple.json"
+    with cogent3.open_(outpath, "w") as json_file:
+        json_file.write(orig.to_json())
+
+    got = load_annotations(path=outpath)
+    assert len(got) == len(orig)
+    assert got.to_rich_dict() == orig.to_rich_dict()
+
+
 def test_gff_end_renamed_to_stop(gff_db, tmp_path):
     bad_col_path = tmp_path / "bad_col.gffdb"
 
diff --git a/tests/test_draw/test_new_draw_integration.py b/tests/test_draw/test_new_draw_integration.py
index b3c41dbe7..ea3f7c62b 100644
--- a/tests/test_draw/test_new_draw_integration.py
+++ b/tests/test_draw/test_new_draw_integration.py
@@ -174,7 +174,7 @@ def test_dotplot_annotated(annotated_seq, with_annotations, mk_cls):
 
     coll = mk_cls({"c_elegans": annotated_seq}, moltype="dna")
     aligned = mk_cls == new_alignment.make_aligned_seqs
-    fig = coll.dotplot().figure
+    fig = coll.dotplot(biotype="gene").figure
     base = {"Alignment", "+ strand"} if aligned else {"+ strand"}
     expect = base | {"gene"} if with_annotations else base
     got = {tr["name"] for tr in fig.data}