Skip to content

Commit

Permalink
implemented allow_lazy_super option to TypeParser and set by default …
Browse files Browse the repository at this point in the history
…in input/output specs
  • Loading branch information
tclose committed Sep 7, 2023
1 parent fc3fcd1 commit 1de1efd
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 3 deletions.
4 changes: 3 additions & 1 deletion pydra/engine/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,9 @@ def make_klass(spec):
**kwargs,
)
checker_label = f"'{name}' field of {spec.name}"
type_checker = TypeParser[newfield.type](newfield.type, label=checker_label)
type_checker = TypeParser[newfield.type](
newfield.type, label=checker_label, allow_lazy_super=True
)
if newfield.type in (MultiInputObj, MultiInputFile):
converter = attr.converters.pipe(ensure_list, type_checker)
elif newfield.type in (MultiOutputObj, MultiOutputFile):
Expand Down
14 changes: 13 additions & 1 deletion pydra/utils/tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,22 @@ def test_type_check_nested8():
with pytest.raises(TypeError, match="explicitly excluded"):
TypeParser(
ty.Tuple[int, ...],
not_coercible=[(ty.Sequence, ty.Tuple)],
not_coercible=[(ty.Sequence, ty.Tuple), (ty.Sequence, ty.List)],
)(lz(ty.List[float]))


def test_type_check_permit_superclass():
# Typical case as Json is subclass of File
TypeParser(ty.List[File])(lz(ty.List[Json]))
# Permissive super class, as File is superclass of Json
TypeParser(ty.List[Json], allow_lazy_super=True)(lz(ty.List[File]))
with pytest.raises(TypeError, match="Cannot coerce"):
TypeParser(ty.List[Json], allow_lazy_super=False)(lz(ty.List[File]))
# Fails because Yaml is neither sub or super class of Json
with pytest.raises(TypeError, match="Cannot coerce"):
TypeParser(ty.List[Json], allow_lazy_super=True)(lz(ty.List[Yaml]))


def test_type_check_fail1():
with pytest.raises(TypeError, match="Wrong number of type arguments in tuple"):
TypeParser(ty.Tuple[int, int, int])(lz(ty.Tuple[float, float, float, float]))
Expand Down
30 changes: 29 additions & 1 deletion pydra/utils/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import sys
import typing as ty
import logging
import attr
from ..engine.specs import (
LazyField,
Expand All @@ -19,6 +20,7 @@
# Python < 3.8
from typing_extensions import get_origin, get_args # type: ignore

logger = logging.getLogger("pydra")

NO_GENERIC_ISSUBCLASS = sys.version_info.major == 3 and sys.version_info.minor < 10

Expand Down Expand Up @@ -56,6 +58,9 @@ class TypeParser(ty.Generic[T]):
the tree of more complex nested container types. Overrides 'coercible' to enable
you to carve out exceptions, such as TypeParser(list, coercible=[(ty.Iterable, list)],
not_coercible=[(str, list)])
allow_lazy_super : bool
Allow lazy fields to pass the type check if their types are superclasses of the
specified pattern (instead of matching or being subclasses of the pattern)
label : str
the label to be used to identify the type parser in error messages. Especially
useful when TypeParser is used as a converter in attrs.fields
Expand All @@ -64,6 +69,7 @@ class TypeParser(ty.Generic[T]):
tp: ty.Type[T]
coercible: ty.List[ty.Tuple[TypeOrAny, TypeOrAny]]
not_coercible: ty.List[ty.Tuple[TypeOrAny, TypeOrAny]]
allow_lazy_super: bool
label: str

COERCIBLE_DEFAULT: ty.Tuple[ty.Tuple[type, type], ...] = (
Expand Down Expand Up @@ -107,6 +113,7 @@ def __init__(
not_coercible: ty.Optional[
ty.Iterable[ty.Tuple[TypeOrAny, TypeOrAny]]
] = NOT_COERCIBLE_DEFAULT,
allow_lazy_super: bool = False,
label: str = "",
):
def expand_pattern(t):
Expand Down Expand Up @@ -135,6 +142,7 @@ def expand_pattern(t):
)
self.not_coercible = list(not_coercible) if not_coercible is not None else []
self.pattern = expand_pattern(tp)
self.allow_lazy_super = allow_lazy_super

def __call__(self, obj: ty.Any) -> ty.Union[T, LazyField[T]]:
"""Attempts to coerce the object to the specified type, unless the value is
Expand All @@ -161,7 +169,27 @@ def __call__(self, obj: ty.Any) -> ty.Union[T, LazyField[T]]:
if obj is attr.NOTHING:
coerced = attr.NOTHING # type: ignore[assignment]
elif isinstance(obj, LazyField):
self.check_type(obj.type)
try:
self.check_type(obj.type)
except TypeError as e:
if self.allow_lazy_super:
try:
# Check whether the type of the lazy field isn't a superclass of
# the type to check against, and if so, allow it due to permissive
# typing rules.
TypeParser(obj.type).check_type(self.tp)
except TypeError:
raise e
else:
logger.info(
"Connecting lazy field %s to %s%s via permissive typing that "
"allows super-to-sub type connections",
obj,
self.tp,
self.label_str,
)
else:
raise e
coerced = obj # type: ignore
elif isinstance(obj, StateArray):
coerced = StateArray(self(o) for o in obj) # type: ignore[assignment]
Expand Down

0 comments on commit 1de1efd

Please sign in to comment.