Skip to content

Commit

Permalink
Merge bf72658 into 1e59170
Browse files Browse the repository at this point in the history
  • Loading branch information
mlin committed Feb 2, 2020
2 parents 1e59170 + bf72658 commit 3d9f587
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 18 deletions.
25 changes: 17 additions & 8 deletions WDL/StdLib.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,15 @@ def _parse_map(s: str) -> Value.Map:


def _parse_json(s: str) -> Value.Base:
# TODO: parse int/float/boolean inside map or list as such
# TODO: handle nested map/array types...tricky as don't know the expected WDL type
j = json.loads(s)
if isinstance(j, dict):
ans = []
for k in j:
ans.append((Value.String(str(k)), Value.String(str(j[k]))))
return Value.Map((Type.String(), Type.String()), ans)
ans.append((Value.String(str(k)), Value.from_json(Type.Any(), j[k])))
return Value.Map((Type.String(), Type.Any()), ans)
if isinstance(j, list):
return Value.Array(Type.String(), [Value.String(str(v)) for v in j])
return Value.Array(Type.Any(), [Value.from_json(Type.Any(), v) for v in j])
if isinstance(j, bool):
return Value.Boolean(j)
if isinstance(j, int):
Expand Down Expand Up @@ -407,6 +407,9 @@ def infer_type(self, expr: "Expr.Apply") -> Type.Base:
rhs, lhs.type.item_type[0], rhs.type, "Map key"
) from None
return lhs.type.item_type[1]
if isinstance(lhs.type, Type.Any):
# e.g. read_json(): assume lhs is Array[Any] or Map[String,Any]
return Type.Any()
raise Error.NotAnArray(lhs)

def _call_eager(self, expr: "Expr.Apply", arguments: List[Value.Base]) -> Value.Base:
Expand All @@ -415,19 +418,25 @@ def _call_eager(self, expr: "Expr.Apply", arguments: List[Value.Base]) -> Value.
rhs = arguments[1]
if isinstance(lhs, Value.Map):
mty = expr.arguments[0].type
assert isinstance(mty, Type.Map)
key = rhs.coerce(mty.item_type[0])
key = rhs
if isinstance(mty, Type.Map):
key = key.coerce(mty.item_type[0])
ans = None
for k, v in lhs.value:
if rhs == k:
if key == k:
ans = v
if ans is None:
raise Error.OutOfBounds(expr.arguments[1]) # TODO: KeyNotFound
return ans
else:
lhs = lhs.coerce(Type.Array(Type.Any()))
rhs = rhs.coerce(Type.Int())
if rhs.value < 0 or rhs.value >= len(lhs.value):
if (
not isinstance(lhs, Value.Array)
or not isinstance(rhs, Value.Int)
or rhs.value < 0
or rhs.value >= len(lhs.value)
):
raise Error.OutOfBounds(expr.arguments[1])
return lhs.value[rhs.value]

Expand Down
15 changes: 8 additions & 7 deletions WDL/Value.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ def __str__(self) -> str:

def coerce(self, desired_type: Optional[Type.Base] = None) -> "Base":
"""
Coerce the value to the desired type and return it
The result is undefined if the coercion is not valid. Types should be
Coerce the value to the desired type and return it. Types should be
checked statically on ``WDL.Expr.Base`` prior to evaluation.
:raises: ReferenceError for a null value and non-optional type
Expand All @@ -61,6 +59,9 @@ def coerce(self, desired_type: Optional[Type.Base] = None) -> "Base":
# coercion of T to Array[T] (x to [x])
# if self is an Array, then Array.coerce precludes this path
return Array(desired_type, [self.coerce(desired_type.item_type)], self.expr)
if desired_type and not self.type.coerces(desired_type):
# owing to static type-checking, this path should arise only rarely e.g. read_json()
raise Error.InputError(f"cannot coerce {str(self.type)} to {str(desired_type)}")
return self

def expect(self, desired_type: Optional[Type.Base] = None) -> "Base":
Expand Down Expand Up @@ -360,15 +361,15 @@ def from_json(type: Type.Base, value: Any) -> Base:
:raise WDL.Error.InputError: if the given value isn't coercible to the specified type
"""
if isinstance(type, Type.Boolean) and value in [True, False]:
if isinstance(type, (Type.Boolean, Type.Any)) and value in [True, False]:
return Boolean(value)
if isinstance(type, Type.Int) and isinstance(value, int):
if isinstance(type, (Type.Int, Type.Any)) and isinstance(value, int):
return Int(value)
if isinstance(type, Type.Float) and isinstance(value, (float, int)):
if isinstance(type, (Type.Float, Type.Any)) and isinstance(value, (float, int)):
return Float(float(value))
if isinstance(type, Type.File) and isinstance(value, str):
return File(value)
if isinstance(type, Type.String) and isinstance(value, str):
if isinstance(type, (Type.String, Type.Any)) and isinstance(value, str):
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])
Expand Down
82 changes: 79 additions & 3 deletions tests/test_5stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,28 +477,104 @@ def test_read_json(self):
task hello {
command <<<
echo '{"foo": "bar", "bas": "baz"}' > object.json
echo '["foo", "bar", "bas", "baz"]' > list.json
echo '[1, 2, 3, 4, 5]' > list.json
echo 42 > int.json
echo 3.14159 > float.json
echo true > bool.json
echo null > null.json
>>>
output {
Map[String,String] map = read_json("object.json")
Array[String] array = read_json("list.json")
Array[Int] array = read_json("list.json")
Int int = read_json("int.json")
Float float = read_json("float.json")
Boolean bool = read_json("bool.json")
String? null = read_json("null.json")
# issue #320
String baz1 = read_json("object.json")["bas"]
Int three = read_json("list.json")[2]
}
}
""")
self.assertEqual(outputs["map"], {"foo": "bar", "bas": "baz"})
self.assertEqual(outputs["array"], ["foo", "bar", "bas", "baz"])
self.assertEqual(outputs["array"], [1, 2, 3, 4, 5])
self.assertEqual(outputs["int"], 42)
self.assertAlmostEqual(outputs["float"], 3.14159)
self.assertEqual(outputs["bool"], True)
self.assertEqual(outputs["null"], None)
self.assertEqual(outputs["baz1"], "baz")
self.assertEqual(outputs["three"], 3)

outputs = self._test_task(R"""
version 1.0
task test {
command <<<
echo '["foo", "bar"]'
>>>
output {
Array[String] my_array = read_json(stdout())
}
}
""")
self.assertEqual(outputs["my_array"], ["foo", "bar"])

self._test_task(R"""
version 1.0
task test {
command <<<
echo '{"foo":"bar"}'
>>>
output {
Array[String] my_array = read_json(stdout())
}
}
""", expected_exception=WDL.Error.InputError)

outputs = self._test_task(R"""
version 1.0
task test {
command <<<
echo '{"foo":"bar"}'
>>>
output {
Map[String, String] my_map = read_json(stdout())
}
}
""")
self.assertEqual(outputs["my_map"], {"foo": "bar"})

self._test_task(R"""
version 1.0
task test {
command <<<
echo '["foo", "bar"]'
>>>
output {
Map[String, String] my_map = read_json(stdout())
}
}
""", expected_exception=WDL.Error.InputError)

def test_read_map_ints(self):
outputs = self._test_task(R"""
version 1.0
task test {
command <<<
python <<CODE
for i in range(3):
print("key_{idx}\t{idx}".format(idx=i))
CODE
>>>
output {
Map[String, Int] my_ints = read_map(stdout())
}
runtime {
docker: "continuumio/miniconda3"
}
}
""")
self.assertEqual(outputs["my_ints"], {"key_0": 0, "key_1": 1, "key_2": 2})

def test_write(self):
outputs = self._test_task(R"""
Expand Down

0 comments on commit 3d9f587

Please sign in to comment.