Repository: kedro-org/kedro

Issue:
Fix comma splitting for node re-run suggestion
## Description
Running kedro with the command `kedro run --from-nodes "two_inputs([A0,B0]) -> [C1]"` will cause an error: `Pipeline does not contain nodes named ['B0]) -> [C1]', 'two_inputs([A0'].`

Kedro has incorrectly split the name of the intended Node into the names of two Nodes that do not exist.

This bug prevents the user from re-running kedro from any Node with a comma in its name. This is the case for any Node that is not explicitly given a name and has more than one input or output.

### Steps to Reproduce
1. Create a kedro project
2. Attempt to run the pipeline from a node with a comma in its name (`kedro run --from-nodes 'My node, a good node'`)
3. Receive the error `Pipeline does not contain nodes named ['a good node', 'My node']`.

## Context
https://github.com/kedro-org/kedro/issues/1828

## Implementation
@jmholzer idea of a fine-state machine csv-parsing type solution to do the string splitting rather than just blindly splitting on `,`. This would basically mean that `,` inside `()` would not split the string. 



Discussion:
I have an implementation of this ready to go. Now that I have started manually testing my changes, I'm starting to think that once https://github.com/kedro-org/kedro/issues/2013 is merged, we'll never see `Node`s with names that contain commas. If you try to instantiate one using `node` (or `Node`), you'll see the following error:


![Image](https://user-images.githubusercontent.com/37243923/204901729-419ed2e3-3c45-42a0-9ad6-786dbb92eca7.png)


Is there a way to instantiate a node with such a name that I am not thinking of?
> I have an implementation of this ready to go. Now that I have started manually testing my changes, I'm starting to think that once #2013 is merged, we'll never see `Node`s with names that contain commas. If you try to instantiate one using `node` (or `Node`), you'll see the following error:
> 
> ![Image](https://user-images.githubusercontent.com/37243923/204901729-419ed2e3-3c45-42a0-9ad6-786dbb92eca7.png)
> 
> Is there a way to instantiate a node with such a name that I am not thinking of?

This will still need to be done for the Kedro `0.18.x` series. But you're right that from `0.19.0` onwards we shouldn't be seeing `Node` names with commas anymore. 

Resolving patch:
diff --git a/kedro/framework/cli/project.py b/kedro/framework/cli/project.py
--- a/kedro/framework/cli/project.py
+++ b/kedro/framework/cli/project.py
@@ -21,6 +21,7 @@
     env_option,
     forward_command,
     python_call,
+    split_node_names,
     split_string,
 )
 from kedro.framework.session import KedroSession
@@ -316,16 +317,28 @@ def activate_nbstripout(
 
 @project_group.command()
 @click.option(
-    "--from-inputs", type=str, default="", help=FROM_INPUTS_HELP, callback=split_string
+    "--from-inputs",
+    type=str,
+    default="",
+    help=FROM_INPUTS_HELP,
+    callback=split_string,
 )
 @click.option(
-    "--to-outputs", type=str, default="", help=TO_OUTPUTS_HELP, callback=split_string
+    "--to-outputs",
+    type=str,
+    default="",
+    help=TO_OUTPUTS_HELP,
+    callback=split_string,
 )
 @click.option(
-    "--from-nodes", type=str, default="", help=FROM_NODES_HELP, callback=split_string
+    "--from-nodes",
+    type=str,
+    default="",
+    help=FROM_NODES_HELP,
+    callback=split_node_names,
 )
 @click.option(
-    "--to-nodes", type=str, default="", help=TO_NODES_HELP, callback=split_string
+    "--to-nodes", type=str, default="", help=TO_NODES_HELP, callback=split_node_names
 )
 @click.option("--node", "-n", "node_names", type=str, multiple=True, help=NODE_ARG_HELP)
 @click.option(
diff --git a/kedro/framework/cli/utils.py b/kedro/framework/cli/utils.py
--- a/kedro/framework/cli/utils.py
+++ b/kedro/framework/cli/utils.py
@@ -294,6 +294,41 @@ def split_string(ctx, param, value):  # pylint: disable=unused-argument
     return [item.strip() for item in value.split(",") if item.strip()]
 
 
+# pylint: disable=unused-argument,missing-param-doc,missing-type-doc
+def split_node_names(ctx, param, to_split: str) -> List[str]:
+    """Split string by comma, ignoring commas enclosed by square parentheses.
+    This avoids splitting the string of nodes names on commas included in
+    default node names, which have the pattern
+    <function_name>([<input_name>,...]) -> [<output_name>,...])
+
+    Note:
+        - `to_split` will have such commas if and only if it includes a
+        default node name. User-defined node names cannot include commas
+        or square brackets.
+        - This function will no longer be necessary from Kedro 0.19.*,
+        in which default node names will no longer contain commas
+
+    Args:
+        to_split: the string to split safely
+
+    Returns:
+        A list containing the result of safe-splitting the string.
+    """
+    result = []
+    argument, match_state = "", 0
+    for char in to_split + ",":
+        if char == "[":
+            match_state += 1
+        elif char == "]":
+            match_state -= 1
+        if char == "," and match_state == 0 and argument:
+            result.append(argument)
+            argument = ""
+        else:
+            argument += char
+    return result
+
+
 def env_option(func_=None, **kwargs):
     """Add `--env` CLI option to a function."""
     default_args = dict(type=str, default=None, help=ENV_HELP)


Test patch:
diff --git a/tests/framework/cli/test_cli.py b/tests/framework/cli/test_cli.py
--- a/tests/framework/cli/test_cli.py
+++ b/tests/framework/cli/test_cli.py
@@ -719,6 +719,51 @@ def test_fail_reformat_load_versions(self, fake_project_cli, fake_metadata):
         )
         assert expected_output in result.output
 
+    @mark.parametrize(
+        "from_nodes, expected",
+        [
+            (["--from-nodes", "A,B,C"], ["A", "B", "C"]),
+            (
+                ["--from-nodes", "two_inputs([A0,B0]) -> [C1]"],
+                ["two_inputs([A0,B0]) -> [C1]"],
+            ),
+            (
+                ["--from-nodes", "two_outputs([A0]) -> [B1,C1]"],
+                ["two_outputs([A0]) -> [B1,C1]"],
+            ),
+            (
+                ["--from-nodes", "multi_in_out([A0,B0]) -> [C1,D1]"],
+                ["multi_in_out([A0,B0]) -> [C1,D1]"],
+            ),
+            (
+                ["--from-nodes", "two_inputs([A0,B0]) -> [C1],X,Y,Z"],
+                ["two_inputs([A0,B0]) -> [C1]", "X", "Y", "Z"],
+            ),
+        ],
+    )
+    def test_safe_split_option_arguments(
+        self,
+        fake_project_cli,
+        fake_metadata,
+        fake_session,
+        mocker,
+        from_nodes,
+        expected,
+    ):
+        CliRunner().invoke(fake_project_cli, ["run", *from_nodes], obj=fake_metadata)
+
+        fake_session.run.assert_called_once_with(
+            tags=(),
+            runner=mocker.ANY,
+            node_names=(),
+            from_nodes=expected,
+            to_nodes=[],
+            from_inputs=[],
+            to_outputs=[],
+            load_versions={},
+            pipeline_name=None,
+        )
+
     def test_run_with_alternative_conf_source(self, fake_project_cli, fake_metadata):
         # check that Kedro runs successfully with an alternative conf_source
         rename("conf", "alternate_conf")