Repository: cogent3/cogent3
Pull request #2016 (2024-09-06 01:37:51)

Fix a regression


Patch:
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index 5f3039946..756ed446f 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -8,13 +8,13 @@ import types
 from copy import deepcopy
 from enum import Enum
 from pathlib import Path
-from typing import Any, Generator, Tuple
+from typing import Any, Generator, Tuple, Union
 from uuid import uuid4
 
 from scitrack import CachingLogger
 
 from cogent3._version import __version__
-from cogent3.app.typing import get_constraint_names, type_tree
+from cogent3.app import typing as c3_typing
 from cogent3.util import parallel as PAR
 from cogent3.util import progress_display as UI
 from cogent3.util.misc import docstring_to_summary_rest, get_object_provenance
@@ -148,11 +148,11 @@ def _get_raw_hints(main_func, min_params):
         "{} type {} nesting level exceeds 2 for {}"
         "we suggest using a custom type, e.g. a dataclass"
     )
-    depth, _ = type_tree(first_param_type)
+    depth, _ = c3_typing.type_tree(first_param_type)
     if depth > 2:
         raise TypeError(msg.format("first_param", first_param_type, depth))
 
-    depth, _ = type_tree(return_type)
+    depth, _ = c3_typing.type_tree(return_type)
     if depth > 2:
         raise TypeError(msg.format("return_type", return_type, depth))
 
@@ -190,8 +190,8 @@ def _get_main_hints(klass) -> Tuple[set, set]:
         raise ValueError(f"must define a callable main() method in {klass.__name__!r}")
 
     first_param_type, return_type = _get_raw_hints(main_func, 2)
-    first_param_type = get_constraint_names(first_param_type)
-    return_type = get_constraint_names(return_type)
+    first_param_type = c3_typing.get_constraint_names(first_param_type)
+    return_type = c3_typing.get_constraint_names(return_type)
 
     return frozenset(first_param_type), frozenset(return_type)
 
@@ -666,7 +666,12 @@ def _proxy_input(dstore):
     return inputs
 
 
-def _source_wrapped(self, value: source_proxy) -> source_proxy:
+def _source_wrapped(
+    self, value: Union[source_proxy, c3_typing.HasSource]
+) -> c3_typing.HasSource:
+    if not isinstance(value, source_proxy):
+        return self(value)
+
     value.set_obj(self(value.obj))
     return value
 
diff --git a/src/cogent3/app/typing.py b/src/cogent3/app/typing.py
index a5f990f90..535f5d51a 100644
--- a/src/cogent3/app/typing.py
+++ b/src/cogent3/app/typing.py
@@ -6,7 +6,7 @@ from __future__ import annotations
 import inspect
 import re
 import sys
-from typing import ForwardRef, TypeVar, Union
+from typing import Any, ForwardRef, Protocol, TypeVar, Union, runtime_checkable
 
 from typing_extensions import get_args, get_origin
 
@@ -17,6 +17,19 @@ if sys.version_info.minor >= 10:
 else:
     NESTED_HINTS = (Union, list, tuple, set)
 
+
+@runtime_checkable
+class HasSource(Protocol):
+    @property
+    def source(self) -> Any: ...
+
+
+@runtime_checkable
+class HasInfo(Protocol):
+    @property
+    def info(self) -> HasSource: ...
+
+
 AlignedSeqsType = TypeVar("AlignedSeqsType", "Alignment", "ArrayAlignment")
 UnalignedSeqsType = TypeVar("UnalignedSeqsType", bound="SequenceCollection")
 SeqsCollectionType = Union[AlignedSeqsType, UnalignedSeqsType]


Test patch:
diff --git a/tests/test_app/test_composable.py b/tests/test_app/test_composable.py
index 230a17364..6966b284a 100644
--- a/tests/test_app/test_composable.py
+++ b/tests/test_app/test_composable.py
@@ -3,7 +3,7 @@ import pickle
 import shutil
 from pathlib import Path
 from pickle import dumps, loads
-from typing import Set, Tuple
+from typing import Set, Tuple, Union
 from unittest.mock import Mock
 
 import pytest
@@ -22,7 +22,6 @@ from cogent3.app.composable import (
     _add,
     _get_raw_hints,
     define_app,
-    get_object_provenance,
     is_app,
     is_app_composable,
 )
@@ -39,6 +38,7 @@ from cogent3.app.translate import select_translatable
 from cogent3.app.tree import quick_tree
 from cogent3.app.typing import AlignedSeqsType, PairwiseDistanceType
 from cogent3.core.alignment import Alignment, SequenceCollection
+from cogent3.util.union_dict import UnionDict
 
 
 @pytest.fixture(scope="function")
@@ -309,6 +309,24 @@ def test_as_completed_empty_data(data):
     assert got == []
 
 
+@pytest.mark.parametrize(
+    "data",
+    [
+        dict(a=2),
+        UnionDict(a=2, source="blah.txt"),
+        Alignment(data=dict(a="ACGT"), info=dict(source="blah.txt")),
+    ],
+)
+def test_as_completed_w_wout_source(data):
+    @define_app
+    def pass_through(val: Union[dict, UnionDict, Alignment]) -> dict:
+        return val
+
+    app = pass_through()  # pylint: disable=not-callable,no-value-for-parameter
+    got = list(app.as_completed([data], show_progress=False))
+    assert bool(got), got
+
+
 @pytest.mark.parametrize("klass", (DataStoreDirectory, DataStoreSqlite))
 @pytest.mark.parametrize("cast", (str, Path))
 def test_apply_to_strings(DATA_DIR, tmp_dir, klass, cast):