Repository: valohai/valohai-cli

Issue:
Exec run messes multi parameters
When a step parameter is configured in valohai YAML with following properties
```yaml
name: "list-param"
default: ["one","two","three"]
multiple: separate
```
It should convert into a JSON payload with `exec run` command
```json
"parameters": {"list-param": ["one","two","three"]}
```
But currently it converts to 
```json
"parameters": {"list-param": "['one','two','three']"}
```
that is then end up in command as
```shell
python ./train.py '--list-param=['"'"'one'"'"', '"'"'two'"'"', '"'"'three'"'"']'
```
Related to https://github.com/valohai/roi/issues/3118


Comments:

Patch:
diff --git a/valohai_cli/commands/execution/run/dynamic_run_command.py b/valohai_cli/commands/execution/run/dynamic_run_command.py
index 1825836..8f14603 100644
--- a/valohai_cli/commands/execution/run/dynamic_run_command.py
+++ b/valohai_cli/commands/execution/run/dynamic_run_command.py
@@ -89,7 +89,8 @@ class RunCommand(click.Command):
         super().__init__(
             name=sanitize_option_name(step.name.lower()),
             callback=self.execute,
-            epilog='Multiple files per input: --my-input=myurl --my-input=myotherurl',
+            epilog='Multiple styled parameters: --my-parameter=value1 --my-parameter=value2\n\n'
+                   'Multiple files per input: --my-input=myurl --my-input=myotherurl',
             add_help_option=True,
         )
         self.params.append(click.Option(
@@ -121,12 +122,17 @@ class RunCommand(click.Command):
         Convert a Parameter into a click Option.
         """
         assert isinstance(parameter, Parameter)
+        help = parameter.description
+        is_multiple = parameter.multiple is not None
+        if is_multiple:
+            help = "(Multiple) " + (help if help else "")
         option = click.Option(
             param_decls=list(generate_sanitized_options(parameter.name)),
             required=False,  # This is done later
             default=parameter.default,
-            help=parameter.description,
+            help=help,
             type=self.parameter_type_map.get(parameter.type, click.STRING),
+            multiple=is_multiple,
         )
         option.name = f'~{parameter.name}'  # Tildify so we can pick these out of kwargs easily
         option.help_group = 'Parameter Options'  # type: ignore[attr-defined]


Test patch:
diff --git a/tests/commands/execution/test_run.py b/tests/commands/execution/test_run.py
index 53f195d..cb5eda1 100644
--- a/tests/commands/execution/test_run.py
+++ b/tests/commands/execution/test_run.py
@@ -107,6 +107,16 @@ def test_run_params(tmpdir, run_test_setup, pass_param):
         values['parameters']['learning_rate'] = 1700
     run_test_setup.values.update(values)
     run_test_setup.run()
+    payload = run_test_setup.run_api_mock.last_create_execution_payload
+    if pass_param == 'direct':
+        assert payload['parameters']['max_steps'] == 1801
+        assert payload['parameters']['learning_rate'] == 0.1337
+    if pass_param == 'file':
+        assert payload['parameters']['max_steps'] == 300
+        assert payload['parameters']['learning_rate'] == 1700.0
+    if pass_param == 'mix':
+        assert payload['parameters']['max_steps'] == 1801
+        assert payload['parameters']['learning_rate'] == 1700.0
 
 
 def test_param_type_validation_integer(runner, logged_in_and_linked, patch_git, default_run_api_mock):
@@ -183,6 +193,22 @@ def test_param_input_sanitization(runner, logged_in_and_linked, patch_git, defau
     assert '--ridiculously-complex-input-name' in output
 
 
+def test_multi_parameter_serialization(run_test_setup):
+    run_test_setup.run()
+    payload = run_test_setup.run_api_mock.last_create_execution_payload
+    assert payload['parameters']['multi-parameter'] == ["one", "two", "three"]
+
+
+def test_multi_parameter_command_line_argument(run_test_setup):
+    run_test_setup.args.append('--multi-parameter=four')
+    run_test_setup.args.append('--multi-parameter=5')
+    run_test_setup.args.append('--multi-parameter="six"')
+    run_test_setup.run()
+    payload = run_test_setup.run_api_mock.last_create_execution_payload
+
+    assert payload['parameters']['multi-parameter'] == ["four", "5", "\"six\""]
+
+
 def test_typo_check(runner, logged_in_and_linked, patch_git, default_run_api_mock):
     with open(get_project().get_config_filename(), 'w') as yaml_fp:
         yaml_fp.write(CONFIG_YAML)
diff --git a/tests/fixture_data.py b/tests/fixture_data.py
index 8c79224..eb964b8 100644
--- a/tests/fixture_data.py
+++ b/tests/fixture_data.py
@@ -462,6 +462,10 @@ CONFIG_YAML = """
         default: 0.1337
       - name: enable_mega_boost
         type: flag
+      - name: multi-parameter
+        default: ["one","two","three"]
+        type: string
+        multiple: separate
     environment-variables:
       - name: testenvvar
         default: 'test'