Repository: kedro-org/kedro
Pull request: Add name flag to kedro new
> **NOTE:** Kedro datasets are moving from `kedro.extras.datasets` to a separate `kedro-datasets` package in
> [`kedro-plugins` repository](https://github.com/kedro-org/kedro-plugins). Any changes to the dataset implementations
> should be done by opening a pull request in that repository.
## Description
<!-- Why was this PR created? -->

Adds a `--name` flag to `kedro new`, allowing the user to skip the prompt that asks for a project name.

## Development notes
<!-- What have you changed, and how has this been tested? -->


## Developer Certificate of Origin
We need all contributions to comply with the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). All commits must be signed off by including a `Signed-off-by` line in the commit message. [See our wiki for guidance](https://github.com/kedro-org/kedro/wiki/Guidelines-for-contributing-developers/).

If your PR is blocked due to unsigned commits, then you must follow the instructions under "Rebase the branch" on the GitHub Checks page for your PR. This will retroactively add the sign-off to all unsigned commits and allow the DCO check to pass.

## Checklist

- [x] Read the [contributing](https://github.com/kedro-org/kedro/blob/main/CONTRIBUTING.md) guidelines
- [x] Signed off each commit with a [Developer Certificate of Origin (DCO)](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-commit-signoff-policy-for-your-repository)
- [x] Opened this PR as a 'Draft Pull Request' if it is work-in-progress
- [ ] Updated the documentation to reflect the code changes
- [x] Added a description of this change in the [`RELEASE.md`](https://github.com/kedro-org/kedro/blob/main/RELEASE.md) file
- [x] Added tests to cover my changes
- [ ] Checked if this change will affect Kedro-Viz, and if so, communicated that with the Viz team


Problem statement:
Addition `--name` `-n` argument for kedro new in non-interactive mode
## Description
<!-- Is your feature request related to a problem? A clear and concise description of what the problem is: "I'm always frustrated when ..." -->

When you have created a Kedro project and execute a kedro command on the CLI for the first time it asks a <project_name>. This is annoying when you keep creating new project, or need to create project with non-interactive mode.

Currently the workaround looks like:
`yes project | kedro new -s pandas-iris`

The task is to add an option to kedro new to immediately

## Context
<!-- Why is this change important to you? How would you use it? How can it benefit other users? -->
When running `kedro new`, it will prompt you to enter the project name. It takes some time for `cookiecutter` to download the repository and user are stuck until it finished. It would be a nice-to-have if I can give a `name` argument so I can create the project in 1 command instead of 2.

## What this is not
- We should not make this a mandatory argument. We should keep the current behavior when `-n` is not provided, then prompt user to input the name of the project.

> (kedro_core) kedro_gallery master % kedro new -s pandas-iris


> Project Name
> ============
> Please enter a human readable name for your new project.
> Spaces, hyphens, and underscores are allowed.
>  [Iris]: 

## Possible Implementation
<!-- (Optional) Suggest an idea for implementing the addition or change. -->
`kedro new -s pandas-iris -n my-pandas` - introduce a new argument

This should be very easy to implement.

## Possible Alternatives
<!-- (Optional) Describe any alternative solutions or features you've considered. -->

Hints from issue discussion:
I've been looking for this in a loooong time, i definitely upvote this!
Tangentially related #1692 
Can I also ask for telemetry consent at this point as well? For CI/CD workflows this is critical.
> Can I also ask for telemetry consent at this point as well? For CI/CD workflows this is critical.
Relate to 
- #1640 

This has been design in technical design, but I guess we haven't turn that into concrete issue. I have convert them into issues and put it in backlog
There are a lot of assignees, so I'm a bit confused, but can I take this? I've already implemented this for a private Kedro-ish fork, so I'll just copy it.
> There are a lot of assignees, so I'm a bit confused, but can I take this? I've already implemented this for a private Kedro-ish fork, so I'll just copy it.

AFAIK @lrcouto and @DimedS started working on this, so you might want to check with them 🙂 
I've started working on this yesterday. It's gonna need a bit of cleanup on the `new` CLI function on top of adding the new flag IMO.

Commit message: Test if names are set correctly

Signed-off-by: lrcouto <laurarccouto@gmail.com>

Patch to review:
diff --git a/kedro/framework/cli/starters.py b/kedro/framework/cli/starters.py
index bdb4f49294..e116d8ef0e 100644
--- a/kedro/framework/cli/starters.py
+++ b/kedro/framework/cli/starters.py
@@ -125,6 +125,8 @@ class KedroStarterSpec:  # noqa: too-few-public-methods
     "6": "Pyspark",
 }
 
+NAME_ARG_HELP = "The name of the Kedro project."
+
 # noqa: unused-argument
 def _remove_readonly(func: Callable, path: Path, excinfo: tuple):  # pragma: no cover
     """Remove readonly files on Windows
@@ -258,7 +260,10 @@ def create_cli():  # pragma: no cover
 @click.option("--checkout", help=CHECKOUT_ARG_HELP)
 @click.option("--directory", help=DIRECTORY_ARG_HELP)
 @click.option("--addons", "-a", "selected_addons", help=ADDON_ARG_HELP)
-def new(config_path, starter_alias, selected_addons, checkout, directory, **kwargs):
+@click.option("--name", "-n", "name", help=NAME_ARG_HELP)
+def new(  # noqa: too-many-arguments
+    config_path, starter_alias, selected_addons, name, checkout, directory, **kwargs
+):
     """Create a new kedro project."""
     if checkout and not starter_alias:
         raise KedroCliError("Cannot use the --checkout flag without a --starter value.")
@@ -293,7 +298,9 @@ def new(config_path, starter_alias, selected_addons, checkout, directory, **kwar
     prompts_required = _get_prompts_required(cookiecutter_dir)
 
     # Select which prompts will be displayed to the user based on which flags were selected.
-    prompts_required = _select_prompts_to_display(prompts_required, selected_addons)
+    prompts_required = _select_prompts_to_display(
+        prompts_required, selected_addons, name
+    )
 
     # We only need to make cookiecutter_context if interactive prompts are needed.
     if not config_path:
@@ -321,6 +328,7 @@ def new(config_path, starter_alias, selected_addons, checkout, directory, **kwar
         config = _fetch_config_from_user_prompts(prompts_required, cookiecutter_context)
 
     config = _get_addons_from_cli_input(selected_addons, config)
+    config = _get_name_from_cli_input(name, config)
 
     cookiecutter_args = _make_cookiecutter_args(config, checkout, directory)
 
@@ -394,7 +402,27 @@ def _get_addons_from_cli_input(
     return config
 
 
-def _select_prompts_to_display(prompts_required: dict, selected_addons: str) -> dict:
+def _get_name_from_cli_input(name: str, config: dict[str, str]) -> dict[str, str]:
+    """Inserts project_name from the CLI input in the project
+    configuration, if it exists.
+
+    Args:
+        name: a string containing the value for the --name flag,
+            or None in case the flag wasn't used.
+
+    Returns:
+        Configuration for starting a new project, with the selected name
+        from the `--name` flag.
+    """
+
+    if name is not None:
+        config["project_name"] = name
+    return config
+
+
+def _select_prompts_to_display(
+    prompts_required: dict, selected_addons: str, name: str
+) -> dict:
     """Selects which prompts an user will receive when creating a new
     Kedro project, based on what information was already made available
     through CLI input.
@@ -429,6 +457,16 @@ def _select_prompts_to_display(prompts_required: dict, selected_addons: str) ->
             sys.exit(1)
         del prompts_required["add_ons"]
 
+    if name is not None:
+        if bool(re.match(r"^[\w -]{2,}$", name)) is False:
+            click.secho(
+                "Kedro project names must contain only alphanumeric symbols, spaces, underscores and hyphens and be at least 2 characters long",
+                fg="red",
+                err=True,
+            )
+            sys.exit(1)
+        del prompts_required["project_name"]
+
     return prompts_required
 
 
diff --git a/tests/framework/cli/test_starters.py b/tests/framework/cli/test_starters.py
index a6f99ea3f4..8eb945b595 100644
--- a/tests/framework/cli/test_starters.py
+++ b/tests/framework/cli/test_starters.py
@@ -62,6 +62,12 @@ def _make_cli_prompt_input_without_addons(
     return "\n".join([project_name, repo_name, python_package])
 
 
+def _make_cli_prompt_input_without_name(
+    add_ons="none", repo_name="", python_package=""
+):
+    return "\n".join([add_ons, repo_name, python_package])
+
+
 def _convert_addon_names_to_numbers(selected_addons: str):
     string_to_number = {
         "lint": "1",
@@ -225,6 +231,19 @@ def _assert_template_ok(
     assert (full_path / "src" / python_package / "__init__.py").is_file()
 
 
+def _assert_name_ok(
+    result,
+    project_name="New Kedro Project",
+    repo_name="new-kedro-project",
+    output_dir=".",
+):
+    assert result.exit_code == 0, result.output
+    assert "Change directory to the project generated in" in result.output
+    assert (
+        "The project name '" + project_name + "' has been applied to: " in result.output
+    )
+
+
 def test_starter_list(fake_kedro_cli):
     """Check that `kedro starter list` prints out all starter aliases."""
     result = CliRunner().invoke(fake_kedro_cli, ["starter", "list"])
@@ -1021,3 +1040,45 @@ def test_invalid_add_on_combination(self, fake_kedro_cli, add_ons):
             "Add-on options 'all' and 'none' cannot be used with other options"
             in result.output
         )
+
+
+@pytest.mark.usefixtures("chdir_to_tmp")
+class TestNameFromCLI:
+    @pytest.mark.parametrize(
+        "name",
+        [
+            "readable_name",
+            "Readable Name",
+            "Readable-name",
+            "readable_name_12233",
+            "123ReadableName",
+        ],
+    )
+    def test_valid_names(self, fake_kedro_cli, name):
+        result = CliRunner().invoke(
+            fake_kedro_cli,
+            ["new", "--name", name],
+            input=_make_cli_prompt_input_without_name(),
+        )
+
+        repo_name = name.lower().replace(" ", "_").replace("-", "_")
+        assert result.exit_code == 0
+        _assert_name_ok(result, project_name=name, repo_name=repo_name)
+        _clean_up_project(Path("./" + repo_name))
+
+    @pytest.mark.parametrize(
+        "name",
+        ["bad_name$%!", "Bad.Name", ""],
+    )
+    def test_invalid_names(self, fake_kedro_cli, name):
+        result = CliRunner().invoke(
+            fake_kedro_cli,
+            ["new", "--name", name],
+            input=_make_cli_prompt_input_without_name(),
+        )
+
+        assert result.exit_code != 0
+        assert (
+            "Kedro project names must contain only alphanumeric symbols, spaces, underscores and hyphens and be at least 2 characters long"
+            in result.output
+        )
diff --git a/tests/tools/test_cli.py b/tests/tools/test_cli.py
index 7489126c06..5dbb4b740a 100644
--- a/tests/tools/test_cli.py
+++ b/tests/tools/test_cli.py
@@ -105,6 +105,8 @@ def test_get_cli_structure_depth(self, mocker, fake_metadata):
                 "-a",
                 "--starter",
                 "-s",
+                "--name",
+                "-n",
                 "--checkout",
                 "--directory",
                 "--help",


Code review comments:
diff_hunk: @@ -321,6 +328,7 @@ def new(config_path, starter_alias, selected_addons, checkout, directory, **kwar
         config = _fetch_config_from_user_prompts(prompts_required, cookiecutter_context)
 
     config = _get_addons_from_cli_input(selected_addons, config)
+    config = _get_name_from_cli_input(name, config)
path: kedro/framework/cli/starters.py
text: @user1:
It's generally not good practice to modify your input, in this case `config`, in this way. Here it's also getting hard to follow what "config" is exactly. I'd probably change this to something like: 

```suggestion
    config_with_add_ons = _get_addons_from_cli_input(selected_addons, config)
    config_with_add_ons_and_name = _get_name_from_cli_input(name, config_with_add_ons)
```

diff_hunk: @@ -258,7 +260,10 @@ def create_cli():  # pragma: no cover
 @click.option("--checkout", help=CHECKOUT_ARG_HELP)
 @click.option("--directory", help=DIRECTORY_ARG_HELP)
 @click.option("--addons", "-a", "selected_addons", help=ADDON_ARG_HELP)
-def new(config_path, starter_alias, selected_addons, checkout, directory, **kwargs):
+@click.option("--name", "-n", "name", help=NAME_ARG_HELP)
+def new(  # noqa: too-many-arguments
+    config_path, starter_alias, selected_addons, name, checkout, directory, **kwargs
path: kedro/framework/cli/starters.py
text: @user1:
I didn't notice that `selected_addons` wasn't added as the last argument before `**kwargs` earlier on, but usually new arguments should be added at the end, because adding them somewhere randomly is a breaking change. Of course, all these changes are breaking anyway, but it's good to keep this in mind.

diff_hunk: @@ -125,6 +125,8 @@ class KedroStarterSpec:  # noqa: too-few-public-methods
     "6": "Pyspark",
 }
 
+NAME_ARG_HELP = "The name of the Kedro project."
path: kedro/framework/cli/starters.py
text: @user1:
```suggestion
NAME_ARG_HELP = "The name of your new Kedro project."
```