Repository: kedro-org/kedro

Issue:
Use `click` choice options to simplify validation of project creation workflow
## Description
Going through the `click` documentation, I came across these features -
* https://click.palletsprojects.com/en/8.1.x/options/#choice-options
* https://click.palletsprojects.com/en/8.1.x/options/#prompting

These could be used to get rid of/simplify the validation we do for inputs to `kedro new` options like `--tools` and `--example`. 

## Context
The project creation workflow is a little complicated, some values come from the CLI command and some from the cookiecutter prompts 


## Possible Implementation
- Could use `click` functionalities to get rid of some of the custom validation we do for inputs using regex etc
- Could explore if click prompts allow us to replace the cookiecutter prompts (related https://github.com/kedro-org/kedro/issues/2928 cc @lrcouto)
- There might be other CLI options that we're accepting as strings and validating (haven't checked) that could benefit from inbuild `click` features




Discussion:

Patch:
diff --git a/kedro/framework/cli/starters.py b/kedro/framework/cli/starters.py
index 0d3b54c8ec..924db1dae0 100644
--- a/kedro/framework/cli/starters.py
+++ b/kedro/framework/cli/starters.py
@@ -167,6 +167,7 @@ def _kedro_version_equal_or_lower_to_starters(version: str) -> bool:
     "pyspark": "6",
     "viz": "7",
 }
+
 NUMBER_TO_TOOLS_NAME = {
     "1": "Linting",
     "2": "Testing",
@@ -311,10 +312,31 @@ def starter() -> None:
 @click.option("--starter", "-s", "starter_alias", help=STARTER_ARG_HELP)
 @click.option("--checkout", help=CHECKOUT_ARG_HELP)
 @click.option("--directory", help=DIRECTORY_ARG_HELP)
-@click.option("--tools", "-t", "selected_tools", help=TOOLS_ARG_HELP)
-@click.option("--name", "-n", "project_name", help=NAME_ARG_HELP)
-@click.option("--example", "-e", "example_pipeline", help=EXAMPLE_ARG_HELP)
-@click.option("--telemetry", "-tc", "telemetry_consent", help=TELEMETRY_ARG_HELP)
+@click.option(
+    "--name",
+    "-n",
+    "project_name",
+    help=NAME_ARG_HELP,
+)
+@click.option(
+    "--tools",
+    "-t",
+    "selected_tools",
+    help=TOOLS_ARG_HELP,
+)
+@click.option(
+    "--example",
+    "-e",
+    "example_pipeline",
+    help=EXAMPLE_ARG_HELP,
+)
+@click.option(
+    "--telemetry",
+    "-tc",
+    "telemetry_consent",
+    help=TELEMETRY_ARG_HELP,
+    type=click.Choice(["yes", "no", "y", "n"], case_sensitive=False),
+)
 def new(  # noqa: PLR0913
     config_path: str,
     starter_alias: str,
@@ -337,6 +359,7 @@ def new(  # noqa: PLR0913
         "example": example_pipeline,
         "telemetry_consent": telemetry_consent,
     }
+
     _validate_flag_inputs(flag_inputs)
     starters_dict = _get_starters_dict()
 
@@ -381,6 +404,7 @@ def new(  # noqa: PLR0913
     shutil.rmtree(tmpdir, onerror=_remove_readonly)  # type: ignore[arg-type]
 
     # Obtain config, either from a file or from interactive user prompts.
+
     extra_context = _get_extra_context(
         prompts_required=prompts_required,
         config_path=config_path,
@@ -399,7 +423,6 @@ def new(  # noqa: PLR0913
     )
 
     if telemetry_consent is not None:
-        _validate_input_with_regex_pattern("yes_no", telemetry_consent)
         telemetry_consent = (
             "true" if _parse_yes_no_to_bool(telemetry_consent) else "false"
         )
@@ -727,7 +750,8 @@ def _fetch_validate_parse_config_from_file(
 
 
 def _fetch_validate_parse_config_from_user_prompts(
-    prompts: dict[str, Any], cookiecutter_context: OrderedDict | None
+    prompts: dict[str, Any],
+    cookiecutter_context: OrderedDict | None,
 ) -> dict[str, str]:
     """Interactively obtains information from user prompts.
 
@@ -739,9 +763,6 @@ def _fetch_validate_parse_config_from_user_prompts(
         Configuration for starting a new project. This is passed as ``extra_context``
             to cookiecutter and will overwrite the cookiecutter.json defaults.
     """
-    from cookiecutter.environment import StrictEnvironment
-    from cookiecutter.prompt import read_user_variable, render_variable
-
     if not cookiecutter_context:
         raise Exception("No cookiecutter context available.")
 
@@ -751,14 +772,16 @@ def _fetch_validate_parse_config_from_user_prompts(
         prompt = _Prompt(**prompt_dict)
 
         # render the variable on the command line
-        cookiecutter_variable = render_variable(
-            env=StrictEnvironment(context=cookiecutter_context),
-            raw=cookiecutter_context.get(variable_name),
-            cookiecutter_dict=config,
-        )
+        default_value = cookiecutter_context.get(variable_name) or ""
 
         # read the user's input for the variable
-        user_input = read_user_variable(str(prompt), cookiecutter_variable)
+        user_input = click.prompt(
+            str(prompt),
+            default=default_value,
+            show_default=True,
+            type=str,
+        ).strip()
+
         if user_input:
             prompt.validate(user_input)
             config[variable_name] = user_input
@@ -997,9 +1020,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
         self.error_message = kwargs.get("error_message", "")
 
     def __str__(self) -> str:
+        # Format the title with optional color
         title = self.title.strip().title()
-        title = click.style(title + "\n" + "=" * len(title), bold=True)
-        prompt_lines = [title, self.text]
+        title = click.style(title, fg="cyan", bold=True)
+        title_line = "=" * len(self.title)
+        title_line = click.style(title_line, fg="cyan", bold=True)
+
+        # Format the main text
+        text = self.text.strip()
+
+        # Combine everything
+        prompt_lines = [title, title_line, text]
         prompt_text = "\n".join(str(line).strip() for line in prompt_lines)
         return f"\n{prompt_text}\n"
 


Test patch:
diff --git a/tests/framework/cli/test_starters.py b/tests/framework/cli/test_starters.py
index a1852c75af..e6a8bd2f60 100644
--- a/tests/framework/cli/test_starters.py
+++ b/tests/framework/cli/test_starters.py
@@ -1676,12 +1676,9 @@ def test_flag_value_is_invalid(self, fake_kedro_cli):
         )
 
         repo_name = "new-kedro-project"
-        assert result.exit_code == 1
+        assert result.exit_code == 2
 
-        assert (
-            "'wrong' is an invalid value for example pipeline. It must contain only y, n, YES, or NO (case insensitive)."
-            in result.output
-        )
+        assert "'wrong' is not one of 'yes', 'no', 'y', 'n'" in result.output
 
         telemetry_file_path = Path(repo_name + "/.telemetry")
         assert not telemetry_file_path.exists()