diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py
index 8bfa73f2032..23665a131dd 100644
--- a/lib/galaxy/tool_util/parser/xml.py
+++ b/lib/galaxy/tool_util/parser/xml.py
@@ -1,3 +1,4 @@
+import json
import logging
import re
import uuid
@@ -620,6 +621,10 @@ def __parse_test_attributes(output_elem, attrib, parse_elements=False, parse_dis
# File no longer required if an list of assertions was present.
attributes = {}
+
+ if 'value_json' in attrib:
+ attributes['object'] = json.loads(attrib.pop('value_json'))
+
# Method of comparison
attributes['compare'] = attrib.pop('compare', 'diff').lower()
# Number of lines to allow to vary in logs (for dates, etc)
@@ -654,7 +659,8 @@ def __parse_test_attributes(output_elem, attrib, parse_elements=False, parse_dis
has_checksum = md5sum or checksum
has_nested_tests = extra_files or element_tests or primary_datasets
- if not (assert_list or file or metadata or has_checksum or has_nested_tests):
+ has_object = 'object' in attributes
+ if not (assert_list or file or metadata or has_checksum or has_nested_tests or has_object):
raise Exception("Test output defines nothing to check (e.g. must have a 'file' check against, assertions to check, metadata or checksum tests, etc...)")
attributes['assert_list'] = assert_list
attributes['extra_files'] = extra_files
@@ -774,8 +780,11 @@ def __parse_param_elem(param_elem, i=0):
value = attrib['values'].split(',')
elif 'value' in attrib:
value = attrib['value']
+ elif 'value_json' in attrib:
+ value = json.loads(attrib['value_json'])
else:
value = None
+
children_elem = param_elem
if children_elem is not None:
# At this time, we can assume having children only
diff --git a/lib/galaxy/tool_util/verify/__init__.py b/lib/galaxy/tool_util/verify/__init__.py
index f39423ef44f..5ca692dce19 100644
--- a/lib/galaxy/tool_util/verify/__init__.py
+++ b/lib/galaxy/tool_util/verify/__init__.py
@@ -3,6 +3,7 @@
import difflib
import filecmp
import hashlib
+import json
import logging
import os
import os.path
@@ -88,7 +89,25 @@ def verify(
if attributes is None:
attributes = {}
- if filename is not None:
+ # expected object might be None, so don't pull unless available
+ has_expected_object = 'object' in attributes
+ if has_expected_object:
+ assert filename is None
+ expected_object = attributes.get('object')
+ actual_object = json.loads(output_content)
+
+ expected_object_type = type(expected_object)
+ actual_object_type = type(actual_object)
+
+ if expected_object_type != actual_object_type:
+ message = f"Type mismatch between expected object ({expected_object_type}) and actual object ({actual_object_type})"
+ raise AssertionError(message)
+
+ if expected_object != actual_object:
+ message = f"Expected object ({expected_object}) does not match actual object ({actual_object})"
+ raise AssertionError(message)
+
+ elif filename is not None:
temp_name = make_temp_fname(fname=filename)
with open(temp_name, 'wb') as f:
f.write(output_content)
diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd
index 9baca16c565..db7500bf0e2 100644
--- a/lib/galaxy/tool_util/xsd/galaxy.xsd
+++ b/lib/galaxy/tool_util/xsd/galaxy.xsd
@@ -1218,6 +1218,13 @@ associated input parameter (``param``).
values that can be assigned to an input parameter.
+
+
+ This variant of the value parameters can be
+used to load typed parameters. This string will be loaded as JSON and its type will
+attempt to be preserved through API requests to Galaxy.
+
+
This attribute name should be included
@@ -1307,6 +1314,16 @@ If specified, this value is the name of the output file stored in the target
``test-data`` directory which will be used to compare the results of executing
the tool via the functional test framework.
+]]>
+
+
+
+
+
diff --git a/lib/galaxy/tools/expressions/evaluation.py b/lib/galaxy/tools/expressions/evaluation.py
index 54066d5094b..c14332d9d3c 100644
--- a/lib/galaxy/tools/expressions/evaluation.py
+++ b/lib/galaxy/tools/expressions/evaluation.py
@@ -35,4 +35,5 @@ def evaluate(config, input):
message = "Expression engine returned non-zero exit code on evaluation of\n%s%s%s" % args
raise Exception(message)
- return json.loads(stdoutdata.decode("utf-8"))
+ rval_raw = stdoutdata.decode("utf-8")
+ return json.loads(rval_raw)
diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py
index 3dff9ccfcaa..76f53e4d3ab 100644
--- a/lib/galaxy/tools/parameters/basic.py
+++ b/lib/galaxy/tools/parameters/basic.py
@@ -17,6 +17,7 @@ from galaxy.tool_util.parser import get_input_source as ensure_input_source
from galaxy.util import (
sanitize_param,
string_as_bool,
+ string_as_bool_or_none,
unicodify,
XML,
)
@@ -515,19 +516,23 @@ class BooleanToolParameter(ToolParameter):
super().__init__(tool, input_source)
self.truevalue = input_source.get('truevalue', 'true')
self.falsevalue = input_source.get('falsevalue', 'false')
- self.checked = input_source.get_bool('checked', False)
+ nullable = input_source.get_bool('optional', False)
+ self.optional = nullable
+ self.checked = input_source.get_bool('checked', None if nullable else False)
def from_json(self, value, trans=None, other_values={}):
return self.to_python(value)
def to_python(self, value, app=None):
- return (value in [True, 'True', 'true'])
+ if not self.optional:
+ ret_val = string_as_bool(value)
+ else:
+ ret_val = string_as_bool_or_none(value)
+ return ret_val
def to_json(self, value, app, use_security):
- if self.to_python(value, app):
- return 'true'
- else:
- return 'false'
+ rval = json.dumps(self.to_python(value, app))
+ return rval
def get_initial_value(self, trans, other_values):
return self.checked
@@ -542,6 +547,7 @@ class BooleanToolParameter(ToolParameter):
d = super().to_dict(trans)
d['truevalue'] = self.truevalue
d['falsevalue'] = self.falsevalue
+ d['optional'] = self.optional
return d
@property
diff --git a/lib/galaxy/tools/parameters/wrapped_json.py b/lib/galaxy/tools/parameters/wrapped_json.py
index b65063c7262..70120ed8d1f 100644
--- a/lib/galaxy/tools/parameters/wrapped_json.py
+++ b/lib/galaxy/tools/parameters/wrapped_json.py
@@ -92,7 +92,10 @@ def _json_wrap_input(input, value_wrapper, profile, handle_files="skip"):
elif input_type == "integer":
json_value = _cast_if_not_none(value_wrapper, int, empty_to_none=True)
elif input_type == "boolean":
- json_value = _cast_if_not_none(value_wrapper, bool)
+ if input.optional and value_wrapper is not None and value_wrapper.value is None:
+ json_value = None
+ else:
+ json_value = _cast_if_not_none(value_wrapper, bool, empty_to_none=input.optional)
elif input_type == "select":
if input.multiple and packaging.version.parse(str(profile)) >= packaging.version.parse('20.05'):
json_value = [_ for _ in _cast_if_not_none(value_wrapper.value, list)]
diff --git a/lib/galaxy/tools/test.py b/lib/galaxy/tools/test.py
index 443fc828e70..9d843cf0073 100644
--- a/lib/galaxy/tools/test.py
+++ b/lib/galaxy/tools/test.py
@@ -8,6 +8,7 @@ import galaxy.tools.parameters.grouping
from galaxy.tool_util.verify.interactor import ToolTestDescription
from galaxy.util import (
string_as_bool,
+ string_as_bool_or_none,
unicodify,
)
@@ -271,7 +272,10 @@ def _process_bool_param_value(param, param_value):
elif param.falsevalue == param_value:
processed_value = False
else:
- processed_value = string_as_bool(param_value)
+ if param.optional:
+ processed_value = string_as_bool_or_none(param_value)
+ else:
+ processed_value = string_as_bool(param_value)
return [processed_value] if was_list else processed_value
diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py
index c819572daf8..c05ba515a94 100644
--- a/lib/galaxy/util/__init__.py
+++ b/lib/galaxy/util/__init__.py
@@ -958,7 +958,7 @@ def string_as_bool_or_none(string):
string = str(string).lower()
if string in ('true', 'yes', 'on'):
return True
- elif string == 'none':
+ elif string in ['none', 'null']:
return None
else:
return False
diff --git a/lib/galaxy_ext/expressions/handle_job.py b/lib/galaxy_ext/expressions/handle_job.py
index e35f8a7a793..5263f8109df 100644
--- a/lib/galaxy_ext/expressions/handle_job.py
+++ b/lib/galaxy_ext/expressions/handle_job.py
@@ -38,10 +38,16 @@ def run(environment_path=None):
del inputs["outputs"]
result = evaluate(None, inputs)
-
for output in outputs:
path = output["path"]
- from_expression = "$(" + output["from_expression"] + ")"
- output_value = expression.interpolate(from_expression, result)
+ from_expression = output["from_expression"]
+ # if it is just a simple value, short-cut all the interpolation
+ # interpolate seems to fail with None values so this worksaround
+ # that for now.
+ if from_expression in result:
+ output_value = result[from_expression]
+ else:
+ from_expression = f"$({from_expression})"
+ output_value = expression.interpolate(from_expression, result)
with open(path, "w") as f:
json.dump(output_value, f)
diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py
index 7d40cada9d4..6247bb7e446 100644
--- a/lib/galaxy_test/api/test_tools.py
+++ b/lib/galaxy_test/api/test_tools.py
@@ -336,6 +336,19 @@ class ToolsTestCase(ApiTestCase, TestsTools):
self._assert_has_keys(test_case, "inputs", "outputs", "output_collections", "required_files")
assert len(test_case["inputs"]) == 1, test_case
+ @skip_without_tool("expression_null_handling_1")
+ def test_test_data_null_boolean_inputs(self):
+ test_data_response = self._get("tools/%s/test_data" % "expression_null_handling_1")
+ assert test_data_response.status_code == 200
+ test_data = test_data_response.json()
+ assert len(test_data) == 3
+ test_case = test_data[2]
+ self._assert_has_keys(test_case, "inputs", "outputs", "output_collections", "required_files")
+ inputs = test_case["inputs"]
+ assert len(inputs) == 1, test_case
+ assert "bool_input" in inputs, inputs
+ assert inputs["bool_input"] is None, inputs
+
@skip_without_tool("simple_constructs_y")
def test_test_data_yaml_tools(self):
test_data_response = self._get("tools/%s/test_data" % "simple_constructs_y")
diff --git a/test/functional/tools/expression_null_handling_boolean.xml b/test/functional/tools/expression_null_handling_boolean.xml
new file mode 100644
index 00000000000..51eb7d2cfd1
--- /dev/null
+++ b/test/functional/tools/expression_null_handling_boolean.xml
@@ -0,0 +1,26 @@
+
+
+ {return {'bool_out': $job.bool_input};}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/samples_tool_conf.xml b/test/functional/tools/samples_tool_conf.xml
index 19bbad36333..05a1ed6efd7 100644
--- a/test/functional/tools/samples_tool_conf.xml
+++ b/test/functional/tools/samples_tool_conf.xml
@@ -71,7 +71,7 @@
-
+