Repository: cogent3/cogent3
Pull request #1496 (2023-07-01 01:43:58)

MAINT: improve app path handling robustness


Patch:
diff --git a/src/cogent3/app/data_store.py b/src/cogent3/app/data_store.py
index 7150da1b1..87f9552ac 100644
--- a/src/cogent3/app/data_store.py
+++ b/src/cogent3/app/data_store.py
@@ -371,7 +371,7 @@ class DataStoreDirectory(DataStoreABC):
     def __init__(
         self,
         source: Union[str, Path],
-        mode: Mode = READONLY,
+        mode: Union[Mode, str] = READONLY,
         suffix: Optional[str] = None,
         limit: int = None,
         verbose=False,
@@ -598,7 +598,7 @@ class ReadOnlyDataStoreZipped(DataStoreABC):
     def __init__(
         self,
         source: Union[str, Path],
-        mode: Mode = READONLY,
+        mode: Union[Mode, str] = READONLY,
         suffix: Optional[str] = None,
         limit: int = None,
         verbose=False,
diff --git a/src/cogent3/app/io.py b/src/cogent3/app/io.py
index 457a258a0..0b5417513 100644
--- a/src/cogent3/app/io.py
+++ b/src/cogent3/app/io.py
@@ -1,9 +1,11 @@
 import contextlib
 import json
+import os
 import pickle
 import zipfile
 
 from enum import Enum
+from functools import singledispatch
 from gzip import compress as gzip_compress
 from gzip import decompress as gzip_decompress
 from pathlib import Path
@@ -252,18 +254,26 @@ class tabular(Enum):
     pssm = "pssm"
 
 
-def _read_it(path):
-    path = Path(path) if isinstance(path, str) else path
+@singledispatch
+def _read_it(path) -> str:
     try:
         data = path.read()
     except AttributeError:
-        try:
-            data = path.read_text()
-        except AttributeError:
-            raise IOError(f"unexpected type {type(path)}")
+        raise IOError(f"unexpected type {type(path)}")
     return data
 
 
+@_read_it.register
+def _(path: os.PathLike) -> str:
+    path = path.expanduser().absolute()
+    return path.read_text()
+
+
+@_read_it.register
+def _(path: str) -> os.PathLike:
+    return _read_it(Path(path))
+
+
 def _load_seqs(path, klass, parser, moltype):
     data = _read_it(path)
     data = data.splitlines()
diff --git a/src/cogent3/app/sqlite_data_store.py b/src/cogent3/app/sqlite_data_store.py
index 97ad089ac..3215c221d 100644
--- a/src/cogent3/app/sqlite_data_store.py
+++ b/src/cogent3/app/sqlite_data_store.py
@@ -98,7 +98,7 @@ class DataStoreSqlite(DataStoreABC):
     def __init__(
         self,
         source,
-        mode: Mode = READONLY,
+        mode: Union[Mode, str] = READONLY,
         limit=None,
         verbose=False,
     ):
diff --git a/src/cogent3/evolve/likelihood_tree.py b/src/cogent3/evolve/likelihood_tree.py
index 7cd8137b5..a20d97218 100644
--- a/src/cogent3/evolve/likelihood_tree.py
+++ b/src/cogent3/evolve/likelihood_tree.py
@@ -10,7 +10,6 @@ from . import likelihood_tree_numba as likelihood_tree
 numpy.seterr(all="ignore")
 
 
-
 class _LikelihoodTreeEdge(object):
     def __init__(self, children, edge_name, alignment=None):
         self.edge_name = edge_name
@@ -55,10 +54,7 @@ class _LikelihoodTreeEdge(object):
 
         # For faster math, a contiguous index array for each child
         self.indexes = numpy.ascontiguousarray(
-            [
-                numpy.array(list(ch), int)
-                for ch in numpy.transpose(self.uniq)
-            ]
+            [numpy.array(list(ch), int) for ch in numpy.transpose(self.uniq)]
         )
 
         # If this is the root it will need to weight the total
@@ -154,7 +150,6 @@ class _LikelihoodTreeEdge(object):
 class LikelihoodTreeEdge(_LikelihoodTreeEdge):
     # Should be a subclass of regular tree edge?
 
-
     # For scaling very very small numbers
     BASE = 2.0**100
     LOG_BASE = numpy.log(BASE)
@@ -190,8 +185,6 @@ class LikelihoodTreeEdge(_LikelihoodTreeEdge):
         return likelihood_tree.get_log_sum_across_sites(lhs, self.counts)
 
 
-
-
 def _indexed(values):
     # >>> _indexed(['a', 'b', 'c', 'a', 'a'])
     # (['a', 'b', 'c'], [3, 1, 1], [0, 1, 2, 0, 0])


Test patch:
diff --git a/tests/test_app/test_io.py b/tests/test_app/test_io.py
index abe245d5d..01d21f35f 100644
--- a/tests/test_app/test_io.py
+++ b/tests/test_app/test_io.py
@@ -5,6 +5,7 @@ import os
 import pathlib
 import pickle
 import shutil
+import tempfile
 
 from pathlib import Path
 
@@ -640,3 +641,23 @@ def test_open_zipped(zipped_full):
     got = open_data_store(zipped_full.source, mode="r", suffix="fasta")
     assert len(got) == len(zipped_full)
     assert isinstance(got, type(zipped_full))
+
+
+@pytest.fixture(scope="function")
+def relpath():
+    # express the data path as relative to user home
+    # have to make a tempdir for this to work in github actions
+    data = (DATA_DIR / "brca1_5.paml").read_text()
+    user = pathlib.Path("~")
+    with tempfile.TemporaryDirectory(dir=user.expanduser()) as dirname:
+        out_path = pathlib.Path(dirname) / "brca1_5.paml"
+        out_path.write_text(data)
+        yield str(user / out_path.parent.name / "brca1_5.paml")
+
+
+@pytest.mark.parametrize("type_", (str, pathlib.Path))
+def test_expand_user(relpath, type_):
+    loader = get_app("load_aligned", format="paml")
+    # define path using the "~" prefix
+    seqs = loader(type_(relpath))
+    assert isinstance(seqs, ArrayAlignment)