Repository: kubeflow/pipelines

Issue:
Handling of boolean input types
I have a pipeline that has a boolean parameter:

```
def test(
    param: dsl.PipelineParam = dsl.PipelineParam(name='param', value=True, param_type=TypeMeta('bool')),
) -> None:
    pass
```

When starting a new pipeline via the UI, the input field is a textbox, prefilled with the input string 'True'. Given that the input value is evaluated using `bool(value)`, the only way to start the pipeline with a `False` value is to leave the input textbox empty.

It seems that currently the pipeline does not gracefully handle boolean parameters?


Discussion:
@jingzhang36, can you see whether it's addressable at UI layer?
Thanks for the report.
I'll be releasing support for boolean input types very soon.

P.S. You do not have to use PipelineParam anywhere in your code. Just write the pythonic function signature:

```python
def my_pipeline(
    param : bool = True,
):
    pass
```

Resolving patch:
diff --git a/sdk/python/kfp/components/_data_passing.py b/sdk/python/kfp/components/_data_passing.py
--- a/sdk/python/kfp/components/_data_passing.py
+++ b/sdk/python/kfp/components/_data_passing.py
@@ -35,10 +35,20 @@
 ])
 
 
+def _deserialize_bool(s) -> bool:
+    from distutils.util import strtobool
+    return strtobool(s) == 1
+
+
+_bool_deserializer_definitions = inspect.getsource(_deserialize_bool)
+_bool_deserializer_code = _deserialize_bool.__name__
+
+
 _converters = [
     Converter([str], ['String', 'str'], str, 'str', None),
     Converter([int], ['Integer', 'int'], str, 'int', None),
     Converter([float], ['Float', 'float'], str, 'float', None),
+    Converter([bool], ['Boolean', 'bool'], str, _bool_deserializer_code, _bool_deserializer_definitions),
 ]
 
 


Test patch:
diff --git a/sdk/python/tests/components/test_python_op.py b/sdk/python/tests/components/test_python_op.py
--- a/sdk/python/tests/components/test_python_op.py
+++ b/sdk/python/tests/components/test_python_op.py
@@ -52,18 +52,15 @@ def module_func_with_deps(a: float, b: float) -> float:
 
 
 class PythonOpTestCase(unittest.TestCase):
-    def helper_test_2_in_1_out_component_using_local_call(self, func, op):
-        arg1 = float(3)
-        arg2 = float(5)
-
-        expected = func(arg1, arg2)
+    def helper_test_2_in_1_out_component_using_local_call(self, func, op, arguments=[3., 5.]):
+        expected = func(arguments[0], arguments[1])
         if isinstance(expected, tuple):
             expected = expected[0]
         expected_str = str(expected)
 
         with tempfile.TemporaryDirectory() as temp_dir_name:
             with components_local_output_dir_context(temp_dir_name):
-                task = op(arg1, arg2)
+                task = op(arguments[0], arguments[1])
 
             full_command = task.command + task.arguments
             subprocess.run(full_command, check=True)
@@ -217,7 +214,7 @@ def my_func( # noqa: F722
                 InputSpec(name='int_param', type='Integer', default='42', optional=True),
                 InputSpec(name='float_param', type='Float', default='3.14', optional=True),
                 InputSpec(name='str_param', type='String', default='string', optional=True),
-                InputSpec(name='bool_param', type='bool', default='True', optional=True),
+                InputSpec(name='bool_param', type='Boolean', default='True', optional=True),
                 InputSpec(name='none_param', optional=True), # No default='None'
                 InputSpec(name='custom_type_param', type='Custom type', optional=True),
             ]
@@ -228,7 +225,7 @@ def my_func( # noqa: F722
                 OutputSpec(name='int_param', type='Integer'),
                 OutputSpec(name='float_param', type='Float'),
                 OutputSpec(name='str_param', type='String'),
-                OutputSpec(name='bool_param', type='bool'),
+                OutputSpec(name='bool_param', type='Boolean'),
                 #OutputSpec(name='custom_type_param', type='Custom type', default='None'),
                 OutputSpec(name='custom_type_param', type='CustomType'),
             ]
@@ -245,7 +242,7 @@ def my_func( # noqa: F722
                     {'name': 'int_param', 'type': 'Integer', 'default': '42', 'optional': True},
                     {'name': 'float_param', 'type': 'Float', 'default': '3.14', 'optional': True},
                     {'name': 'str_param', 'type': 'String', 'default': 'string', 'optional': True},
-                    {'name': 'bool_param', 'type': 'bool', 'default': 'True', 'optional': True},
+                    {'name': 'bool_param', 'type': 'Boolean', 'default': 'True', 'optional': True},
                     {'name': 'none_param', 'optional': True}, # No default='None'
                     {'name': 'custom_type_param', 'type': 'Custom type', 'optional': True},
                 ],
@@ -253,7 +250,7 @@ def my_func( # noqa: F722
                     {'name': 'int_param', 'type': 'Integer'},
                     {'name': 'float_param', 'type': 'Float'},
                     {'name': 'str_param', 'type': 'String'},
-                    {'name': 'bool_param', 'type': 'bool'},
+                    {'name': 'bool_param', 'type': 'Boolean'},
                     {'name': 'custom_type_param', 'type': 'CustomType'},
                 ]
             }
@@ -375,6 +372,20 @@ def assert_values_are_default(
         self.helper_test_2_in_1_out_component_using_local_call(func, op)
 
 
+    def test_handling_boolean_arguments(self):
+        def assert_values_are_true_false(
+            bool1 : bool,
+            bool2 : bool,
+        ) -> int:
+            assert bool1 is True
+            assert bool2 is False
+            return 1
+
+        func = assert_values_are_true_false
+        op = comp.func_to_container_op(func)
+        self.helper_test_2_in_1_out_component_using_local_call(func, op, arguments=[True, False])
+
+
     def test_end_to_end_python_component_pipeline_compilation(self):
         import kfp.components as comp