Skip to content

Commit

Permalink
WDL.Type.unify: recurse on compound type parameters (#408)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlin committed Jul 9, 2020
1 parent 5a2f97b commit 3282880
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 6 deletions.
29 changes: 24 additions & 5 deletions WDL/Type.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ def coerces(self, rhs: Base, check_quant: bool = True) -> bool:
return False


def unify(types: List[Base], check_quant: bool = True, force_string: bool = False,) -> Base:
def unify(types: List[Base], check_quant: bool = True, force_string: bool = False) -> Base:
"""
Given a list of types, compute a type to which they're all coercible, or :class:`WDL.Type.Any`
if no more-specific inference is possible.
Expand All @@ -496,20 +496,38 @@ def unify(types: List[Base], check_quant: bool = True, force_string: bool = Fals

# begin with first type; or if --no-quant-check, the first array type (as we can try to promote
# other T to Array[T])
t = types[0]
t = next((t for t in types if not isinstance(t, Any)), types[0])
if not check_quant:
t = next((a for a in types if isinstance(a, Array)), t)
t = next((a for a in types if isinstance(a, Array) and not isinstance(a.item_type, Any)), t)
t = t.copy() # pyre-ignore

# potentially promote/generalize t to other types seen
optional = False
all_nonempty = True
all_stringifiable = True
for t2 in types:
# recurse on parameters of compound types
t_was_array_any = isinstance(t, Array) and isinstance(t.item_type, Any)
if isinstance(t, Array) and isinstance(t2, Array) and not isinstance(t2.item_type, Any):
t.item_type = unify([t.item_type, t2.item_type], check_quant, force_string)
if isinstance(t, Pair) and isinstance(t2, Pair):
t.left_type = unify([t.left_type, t2.left_type], check_quant, force_string)
t.right_type = unify([t.right_type, t2.right_type], check_quant, force_string)
if isinstance(t, Map) and isinstance(t2, Map):
t.item_type = ( # pyre-ignore
unify([t.item_type[0], t2.item_type[0]], check_quant, force_string), # pyre-ignore
unify([t.item_type[1], t2.item_type[1]], check_quant, force_string), # pyre-ignore
)
if not t_was_array_any and next((pt for pt in t.parameters if isinstance(pt, Any)), False):
return Any()

# Int/Float, String/File
if isinstance(t, Int) and isinstance(t2, Float):
t = Float()
if isinstance(t, String) and isinstance(t2, File):
t = File()

# String
if (
isinstance(t2, String)
and not isinstance(t2, File)
Expand All @@ -520,9 +538,10 @@ def unify(types: List[Base], check_quant: bool = True, force_string: bool = Fals
if not t2.coerces(String(optional=True), check_quant=check_quant):
all_stringifiable = False

if t2.optional:
# optional/nonempty
if t.optional or t2.optional:
optional = True
if isinstance(t2, Array) and not t2.nonempty:
if isinstance(t, Array) and not t.nonempty or isinstance(t2, Array) and not t2.nonempty:
all_nonempty = False

if isinstance(t, Array):
Expand Down
6 changes: 6 additions & 0 deletions WDL/Value.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,12 @@ def from_json(type: Type.Base, value: Any) -> Base:
return String(value)
if isinstance(type, Type.Array) and isinstance(value, list):
return Array(type, [from_json(type.item_type, item) for item in value])
if isinstance(type, Type.Pair) and isinstance(value, list) and len(value) == 2:
return Pair(
type.left_type,
type.right_type,
(from_json(type.left_type, value[0]), from_json(type.right_type, value[1])),
)
if (
isinstance(type, Type.Map)
and type.item_type[0] == Type.String()
Expand Down
1 change: 1 addition & 0 deletions tests/test_0eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ def test_json(self):
(WDL.Type.Map((WDL.Type.String(), WDL.Type.Int())), {"cats": 42, "dogs": 99}),
(pty, {"name": "Alyssa", "age": 42, "pets": None}),
(pty, {"name": "Alyssa", "age": 42, "pets": {"cats": 42, "dogs": 99}}),
(WDL.Type.Array(WDL.Type.Pair(WDL.Type.String(), WDL.Type.Int())), [["a",0],["b",1]]),

(WDL.Type.Boolean(), 42, WDL.Error.InputError),
(WDL.Type.Float(), "your president", WDL.Error.InputError),
Expand Down
13 changes: 13 additions & 0 deletions tests/test_1doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,16 +490,29 @@ def test_unify(self):
workflow unify {
String s
File? f2
Array[Int] a1 = [1]
Array[Int?]? a2 = []
Array[Pair[String,String]] ap = [(0,1),(2,3)]
Array[Map[String,String]] am = [{ "a": 0, "b": 1 }, { "a": "x", "b": "y" }, { 1: 2, 3: 4 }]
output {
Array[File?] a = [s, f2]
Array[Array[Int?]?] a3 = [a1, a2]
Map[String, File?] m = { "foo": s, "bar": f2 }
Map[Float, File?] m2 = { 1: s, 2.0: f2 }
}
}
""")
doc.typecheck()

with self.assertRaises(WDL.Error.ValidationError):
doc = WDL.parse_document("""
workflow unify {
Array[Pair[String,String]] bogus = [("a","b"), ("c",("d","e"))]
}
""")
doc.typecheck()

class TestDoc(unittest.TestCase):
def test_count_foo(self):
doc = r"""#foo
Expand Down
2 changes: 1 addition & 1 deletion tests/test_3corpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ class ViralNGS(unittest.TestCase):
@wdl_corpus(
["test_corpi/ENCODE-DCC/chip-seq-pipeline2/**"],
expected_lint={
"StringCoercion": 192,
"StringCoercion": 208,
"FileCoercion": 154,
"NameCollision": 16,
"OptionalCoercion": 64,
Expand Down

0 comments on commit 3282880

Please sign in to comment.