Skip to content

Commit 0ccc223

Browse files
committed
implemented allow_lazy_super option to TypeParser and set by default in input/output specs
1 parent f31f61a commit 0ccc223

File tree

3 files changed

+45
-3
lines changed

3 files changed

+45
-3
lines changed

pydra/engine/helpers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ def make_klass(spec):
263263
**kwargs,
264264
)
265265
checker_label = f"'{name}' field of {spec.name}"
266-
type_checker = TypeParser[newfield.type](newfield.type, label=checker_label)
266+
type_checker = TypeParser[newfield.type](
267+
newfield.type, label=checker_label, allow_lazy_super=True
268+
)
267269
if newfield.type in (MultiInputObj, MultiInputFile):
268270
converter = attr.converters.pipe(ensure_list, type_checker)
269271
elif newfield.type in (MultiOutputObj, MultiOutputFile):

pydra/utils/tests/test_typing.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,22 @@ def test_type_check_nested8():
160160
with pytest.raises(TypeError, match="explicitly excluded"):
161161
TypeParser(
162162
ty.Tuple[int, ...],
163-
not_coercible=[(ty.Sequence, ty.Tuple)],
163+
not_coercible=[(ty.Sequence, ty.Tuple), (ty.Sequence, ty.List)],
164164
)(lz(ty.List[float]))
165165

166166

167+
def test_type_check_permit_superclass():
168+
# Typical case as Json is subclass of File
169+
TypeParser(ty.List[File])(lz(ty.List[Json]))
170+
# Permissive super class, as File is superclass of Json
171+
TypeParser(ty.List[Json], allow_lazy_super=True)(lz(ty.List[File]))
172+
with pytest.raises(TypeError, match="Cannot coerce"):
173+
TypeParser(ty.List[Json], allow_lazy_super=False)(lz(ty.List[File]))
174+
# Fails because Yaml is neither sub or super class of Json
175+
with pytest.raises(TypeError, match="Cannot coerce"):
176+
TypeParser(ty.List[Json], allow_lazy_super=True)(lz(ty.List[Yaml]))
177+
178+
167179
def test_type_check_fail1():
168180
with pytest.raises(TypeError, match="Wrong number of type arguments in tuple"):
169181
TypeParser(ty.Tuple[int, int, int])(lz(ty.Tuple[float, float, float, float]))

pydra/utils/typing.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import sys
66
import typing as ty
7+
import logging
78
import attr
89
from ..engine.specs import (
910
LazyField,
@@ -19,6 +20,7 @@
1920
# Python < 3.8
2021
from typing_extensions import get_origin, get_args # type: ignore
2122

23+
logger = logging.getLogger("pydra")
2224

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

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

6975
COERCIBLE_DEFAULT: ty.Tuple[ty.Tuple[type, type], ...] = (
@@ -107,6 +113,7 @@ def __init__(
107113
not_coercible: ty.Optional[
108114
ty.Iterable[ty.Tuple[TypeOrAny, TypeOrAny]]
109115
] = NOT_COERCIBLE_DEFAULT,
116+
allow_lazy_super: bool = False,
110117
label: str = "",
111118
):
112119
def expand_pattern(t):
@@ -135,6 +142,7 @@ def expand_pattern(t):
135142
)
136143
self.not_coercible = list(not_coercible) if not_coercible is not None else []
137144
self.pattern = expand_pattern(tp)
145+
self.allow_lazy_super = allow_lazy_super
138146

139147
def __call__(self, obj: ty.Any) -> ty.Union[T, LazyField[T]]:
140148
"""Attempts to coerce the object to the specified type, unless the value is
@@ -161,7 +169,27 @@ def __call__(self, obj: ty.Any) -> ty.Union[T, LazyField[T]]:
161169
if obj is attr.NOTHING:
162170
coerced = attr.NOTHING # type: ignore[assignment]
163171
elif isinstance(obj, LazyField):
164-
self.check_type(obj.type)
172+
try:
173+
self.check_type(obj.type)
174+
except TypeError as e:
175+
if self.allow_lazy_super:
176+
try:
177+
# Check whether the type of the lazy field isn't a superclass of
178+
# the type to check against, and if so, allow it due to permissive
179+
# typing rules.
180+
TypeParser(obj.type).check_type(self.tp)
181+
except TypeError:
182+
raise e
183+
else:
184+
logger.info(
185+
"Connecting lazy field %s to %s%s via permissive typing that "
186+
"allows super-to-sub type connections",
187+
obj,
188+
self.tp,
189+
self.label_str,
190+
)
191+
else:
192+
raise e
165193
coerced = obj # type: ignore
166194
elif isinstance(obj, StateArray):
167195
coerced = StateArray(self(o) for o in obj) # type: ignore[assignment]

0 commit comments

Comments
 (0)