Repository: kedro-org/kedro

Issue:
Validate arguments in conf file when passed to CLI
## Description
When using `kedro run --config` no errors are thrown if the arguments in the provided run config file are wrong, e.g.

```yml
run:
  from-nodes: bla # this should be `from_nodes`
```

doesn't result in an error, but if you run `kedro run --from_nodes bla` (this should be `kedro run --from-nodes`) it will say:

```Error: No such option: --from_nodes (Possible options: --from-nodes, --nodes, --to-nodes)```

## Context
https://github.com/kedro-org/kedro/issues/1485

## Task

Validate the provided arguments are the expected arguments from the run function, optionally print out what's the argument it has parsed. Currently, if we provide an invalid argument it will still run but doesn't do anything, it is hard to debug, especially the CLI argument can override the YAML config. Some investigation needs to be done to check how feasible this is and what the current behaviour is (e.g. will it throw an error if you misspell a node in `--from-nodes my_node1` or is it ignored?) to make sure it's consistent.



Discussion:
Adding some more context here:
I think this is specifically related to the `--config` argument, where it doesn't fail if you provide an arbitrary key. That's part of the reason why `from_node` is confusing because if people passing `from-node` it would still run but without any effect.

* This could be mitigated with the `kedro compile` proposal, or a [similar discussion here](#1691) to output a compiled version of kedro parameters/catalog, I think this is becoming more relevant with [experiment-tracking](https://github.com/kedro-org/kedro-viz/issues/1020), where you want to have a diff of your parameters. 
* It's still nice to have validation from the `kedro` side, we may validate a hard-coded list of keywords (may not work well if the user has a custom run command, maybe a rare case though I actually did this before), or we can actually access the argument through `click`'s context.
> Adding some more context here: I think this is specifically related to the `--config` argument, where it doesn't fail if you provide an arbitrary key. That's part of the reason why `from_node` is confusing because if people passing `from-node` it would still run but without any effect.

On the specific note w.r.t. hyphens/underscores, I just came across https://jwodder.github.io/kbits/posts/click-config/, which says:

> Note that, with this code, parameters in the config file must be named using the same name & spelling as the parameter’s corresponding argument to the command callback. For example, the `--integer` option must be written integer, not `--integer` or `-i` or `i`; any entries in the config file with an invalid spelling will be ignored. For options with medial hyphens on the command line, like `--log-level`, the hyphens must become underscores in the configuration file, like `log_level`. If you want to support the spelling `log-level` as well, insert the following line after `cfg = ConfigParser()` to make the `ConfigParser` object convert hyphens in option names to underscores:

```python
cfg.optionxform = lambda s: s.replace('-', '_')
```


Resolving patch:
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
@@ -259,21 +259,25 @@ class KedroCliError(click.exceptions.ClickException):
     """
 
     VERBOSE_ERROR = False
+    VERBOSE_EXISTS = True
 
     def show(self, file=None):
-        if file is None:
-            # noqa: protected-access
-            file = click._compat.get_text_stderr()
         if self.VERBOSE_ERROR:
             click.secho(traceback.format_exc(), nl=False, fg="yellow")
-        else:
+        elif self.VERBOSE_EXISTS:
             etype, value, _ = sys.exc_info()
             formatted_exception = "".join(traceback.format_exception_only(etype, value))
             click.secho(
                 f"{formatted_exception}Run with --verbose to see the full exception",
                 fg="yellow",
             )
-        click.secho(f"Error: {self.message}", fg="red", file=file)
+        else:
+            etype, value, _ = sys.exc_info()
+            formatted_exception = "".join(traceback.format_exception_only(etype, value))
+            click.secho(
+                f"{formatted_exception}",
+                fg="yellow",
+            )
 
 
 def _clean_pycache(path: Path):
@@ -401,11 +405,27 @@ def _config_file_callback(ctx, param, value):  # noqa: unused-argument
 
     if value:
         config = OmegaConf.to_container(OmegaConf.load(value))[section]
+        for key, value in config.items():
+            _validate_config_file(key)
         ctx.default_map.update(config)
 
     return value
 
 
+def _validate_config_file(key):
+    """Validate the keys provided in the config file against the accepted keys."""
+    from kedro.framework.cli.project import run
+
+    run_args = [click_arg.name for click_arg in run.params]
+    run_args.remove("config")
+    if key not in run_args:
+        KedroCliError.VERBOSE_EXISTS = False
+        message = _suggest_cli_command(key, run_args)
+        raise KedroCliError(
+            f"Key `{key}` in provided configuration is not valid. {message}"
+        )
+
+
 def _split_params(ctx, param, value):
     if isinstance(value, dict):
         return value


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
@@ -428,7 +428,26 @@ def fake_run_config(request, fake_root_dir):
                 "pipeline": "pipeline1",
                 "tags": "tag1, tag2",
                 "node_names": "node1, node2",
-            }
+            },
+            "dummy": {"dummy": "dummy"},
+        }
+        OmegaConf.save(
+            config,
+            config_path,
+        )
+        return config_path
+
+    @staticmethod
+    @fixture(params=["run_config.yml", "run_config.json"])
+    def fake_invalid_run_config(request, fake_root_dir):
+        config_path = str(fake_root_dir / request.param)
+        config = {
+            "run": {
+                "pipeline": "pipeline1",
+                "tags": "tag1, tag2",
+                "node-names": "node1, node2",
+            },
+            "dummy": {"dummy": "dummy"},
         }
         OmegaConf.save(
             config,
@@ -638,6 +657,26 @@ def test_run_with_config(
             namespace=None,
         )
 
+    @mark.parametrize("config_flag", ["--config", "-c"])
+    def test_run_with_invalid_config(
+        self,
+        config_flag,
+        fake_project_cli,
+        fake_metadata,
+        fake_session,
+        fake_invalid_run_config,
+    ):
+        result = CliRunner().invoke(
+            fake_project_cli,
+            ["run", config_flag, fake_invalid_run_config],
+            obj=fake_metadata,
+        )
+        assert result.exit_code
+        assert (
+            "Key `node-names` in provided configuration is not valid. \n\nDid you mean one of "
+            "these?\n    node_names\n    to_nodes\n    namespace" in result.stdout
+        )
+
     @mark.parametrize(
         "fake_run_config_with_params,expected",
         [