Repository: kedro-org/kedro

Issue:
Possible Incorrect Node argument ordering on Exception
## Description
I've noticed that the order of the node arguments that are logged when a node raises an exception does not the order in which they are defined in the pipeline. 

## Context
The logged exception is useful for resuming the pipeline from the node that failed. If the arguments are not in the correct order, then it's likely that resuming the pipeline using 

```bash
kedro run --from-nodes ...
```
will still result in an error. It could be that the arguments are passed in as keyword arguments, but this is unclear from the error log.

## Steps to Reproduce
The following script 
```python
from kedro.io import DataCatalog, MemoryDataSet
from kedro.pipeline import node, Pipeline
from kedro.runner import SequentialRunner

import pandas as pd
from sklearn.dummy import DummyClassifier

import logging

log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

def split_data(df):
    X = df.iloc[:,0]
    y = df.iloc[:,1]
    return X, y

def create_model():
    return DummyClassifier()

def train_model(model, X_train, y_train):
    raise ValueError #force exception
    model.fit(X_train, y_train)
    return model

data_catalog = DataCatalog({"data": MemoryDataSet(data=pd.DataFrame({"a":[0,1,2],"b":[True, False, True]}))})
pipeline = Pipeline([
    node(split_data, inputs="data", outputs=["X","y"]),
    node(create_model, inputs=None, outputs="model"),
    node(train_model, inputs=["model","X","y"], outputs="trained_model")
])

runner = SequentialRunner()
print(runner.run(pipeline, data_catalog))
```

## Expected Result
On exception, the logged message should preserve the order of the inputs

```
ERROR:kedro.pipeline.node:Node `train_model([X,model,y]) -> [trained_model]` failed with error: 
WARNING:kedro.runner.sequential_runner:There are 1 nodes that have not run.
You can resume the pipeline run by adding the following argument to your previous command:
  --from-nodes "train_model([model,X, y]) -> [trained_model]"
```

## Actual Result
The arguments appear to be ordered alphabetically, with capital letters taking precedence over lowercase

```
ERROR:kedro.pipeline.node:Node `train_model([X,model,y]) -> [trained_model]` failed with error: 
WARNING:kedro.runner.sequential_runner:There are 1 nodes that have not run.
You can resume the pipeline run by adding the following argument to your previous command:
  --from-nodes "train_model([X,model,y]) -> [trained_model]"
```


## Your Environment
Include as many relevant details about the environment in which you experienced the bug:

* Kedro version used (`pip show kedro` or `kedro -V`): kedro, version 0.17.5
* Python version used (`python -V`): Python 3.8.10
* Operating system and version: Windows 10 Version 20H2 (OS Build 19042.928)



Discussion:
Hi @jstammers, thanks very much for pointing this. I had actually noticed the same thing happens during log `INFO` messages, which was also a bit irritating. I will double check there's no good reason we put these in alphabetical order (I don't see why we should, it seems wrong to me) and then open a PR for it.
So it turns out that even if we change the order of these the `--from-nodes` (and also `--to-nodes` and `--node`) arguments won't work correctly with any node name that contains a comma. This will be the case for any node that you do not give an explicit name to and has more than one input/output. I've logged a ticket to fix this because it's not completely trivial to fix immediately I'm afraid.

In the mean time, your best bet is to just explicitly give the node a `name` when you define it. This makes it much easier to refer to it directly and should always work. Sorry for the inconvenience.

Resolving patch:
diff --git a/kedro/pipeline/node.py b/kedro/pipeline/node.py
--- a/kedro/pipeline/node.py
+++ b/kedro/pipeline/node.py
@@ -147,6 +147,9 @@ def _logger(self):
     def _unique_key(self):
         def hashable(value):
             if isinstance(value, dict):
+                # we sort it because a node with inputs/outputs
+                # {"arg1": "a", "arg2": "b"} is equivalent to
+                # a node with inputs/outputs {"arg2": "b", "arg1": "a"}
                 return tuple(sorted(value.items()))
             if isinstance(value, list):
                 return tuple(value)
@@ -168,11 +171,11 @@ def __hash__(self):
         return hash(self._unique_key)
 
     def __str__(self):
-        def _sorted_set_to_str(xset):
-            return f"[{','.join(sorted(xset))}]"
+        def _set_to_str(xset):
+            return f"[{','.join(xset)}]"
 
-        out_str = _sorted_set_to_str(self.outputs) if self._outputs else "None"
-        in_str = _sorted_set_to_str(self.inputs) if self._inputs else "None"
+        out_str = _set_to_str(self.outputs) if self._outputs else "None"
+        in_str = _set_to_str(self.inputs) if self._inputs else "None"
 
         prefix = self._name + ": " if self._name else ""
         return prefix + f"{self._func_name}({in_str}) -> {out_str}"
@@ -686,7 +689,7 @@ def _to_list(element: Union[None, str, Iterable[str], Dict[str, str]]) -> List[s
     if isinstance(element, str):
         return [element]
     if isinstance(element, dict):
-        return sorted(element.values())
+        return list(element.values())
     return list(element)
 
 


Test patch: