Repository: Avaiga/taipy
Pull request: Fix/#1202 - Catch cookiecutter.OutputDirExistsException and exit
Resolves #1202 

Problem statement:
BUG-taipy create fails if the project folder already exists
**Description**
`taipy create` command fails if the project folder already exists.

**How to reproduce**

```sh
taipy create 
# accept defaults. Default root folder name is 'taipy_application'

# Run the create again and accept defaults
taipy create
```

Fails with error -

```sh
❯ taipy create --template=scenario-management
Application root folder name [taipy_application]:
Application main Python file [main.py]:
Application title [Taipy Application]:
Does the application use TOML Config? [No]:
Traceback (most recent call last):
  File "/Users/anunayasri/workspace/taipy/./extaipy.py", line 8, in <module>
    sys.exit(_entrypoint())
             ^^^^^^^^^^^^^
  File "/Users/anunayasri/workspace/taipy/taipy/_entrypoint.py", line 62, in _entrypoint
    _ScaffoldCLI.handle_command()
  File "/Users/anunayasri/workspace/taipy/taipy/_cli/_scaffold_cli.py", line 49, in handle_command
    cookiecutter(cls._TEMPLATE_MAP[args.template])
  File "/Users/anunayasri/workspace/taipy/.venv/lib/python3.11/site-packages/cookiecutter/main.py", line 114, in cookiecutter
    result = generate_files(
             ^^^^^^^^^^^^^^^
  File "/Users/anunayasri/workspace/taipy/.venv/lib/python3.11/site-packages/cookiecutter/generate.py", line 291, in generate_files
    project_dir, output_directory_created = render_and_create_dir(
                                            ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/anunayasri/workspace/taipy/.venv/lib/python3.11/site-packages/cookiecutter/generate.py", line 223, in render_and_create_dir
    raise OutputDirExistsException(msg)
cookiecutter.exceptions.OutputDirExistsException: Error: "taipy_application" directory already exists


**Expected behavior**
 It should exit gracefully, after declaring that the folder already exists. Or it can ask for a different folder name if the folder already exists.


**Runtime environment**
Please specify relevant indications.
 - Taipy version (or branch name): `develop`
 - OS: Mac OS

Hints from issue discussion:
Thank you for reporting the issue. Indeed, it should at least exit gracefully with a good error message.

Commit message: fix: catch cookiecutter OutputDirExistsException and exit

Patch to review:
diff --git a/taipy/_cli/_scaffold_cli.py b/taipy/_cli/_scaffold_cli.py
index d52caf998c..4d1d1435bb 100644
--- a/taipy/_cli/_scaffold_cli.py
+++ b/taipy/_cli/_scaffold_cli.py
@@ -12,6 +12,7 @@
 import pathlib
 import sys
 
+from cookiecutter.exceptions import OutputDirExistsException
 from cookiecutter.main import cookiecutter
 
 import taipy
@@ -45,6 +46,10 @@ def handle_command(cls):
         args = cls._parse_arguments()
         if not args:
             return
-
-        cookiecutter(cls._TEMPLATE_MAP[args.template])
+        try:
+            cookiecutter(cls._TEMPLATE_MAP[args.template])
+        except OutputDirExistsException as err:
+            error_msg = f"{str(err)[7:]}. Please remove the existing directory or provide a new folder name."
+            cls._logger.error(error_msg)
+            sys.exit(1)
         sys.exit(0)
diff --git a/tests/templates/test_template_cli.py b/tests/templates/test_template_cli.py
index 4501286605..cbef0e1445 100644
--- a/tests/templates/test_template_cli.py
+++ b/tests/templates/test_template_cli.py
@@ -9,6 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 
+import os
 from unittest.mock import patch
 
 import pytest
@@ -29,3 +30,17 @@ def test_create_cli_with_unsupported_template(capsys):
             _entrypoint()
         _, err = capsys.readouterr()
         assert "invalid choice: 'not-a-template'" in err
+
+
+def test_create_app_on_existing_folder(tmpdir, caplog, monkeypatch):
+    os.chdir(tmpdir)
+    os.mkdir(os.path.join(tmpdir, "taipy_application"))
+
+    # Mock the click.prompt to always return the default value
+    monkeypatch.setattr("click.prompt", lambda *args, **kw: kw["default"] if "default" in kw else "")
+
+    with patch("sys.argv", ["prog", "create"]):
+        with pytest.raises(SystemExit):
+            _entrypoint()
+
+    assert '"taipy_application" directory already exists' in caplog.text


Code review comments:
diff_hunk: @@ -45,6 +46,10 @@ def handle_command(cls):
         args = cls._parse_arguments()
         if not args:
             return
-
-        cookiecutter(cls._TEMPLATE_MAP[args.template])
+        try:
+            cookiecutter(cls._TEMPLATE_MAP[args.template])
+        except OutputDirExistsException as err:
+            error_msg = f"{str(err)[7:]}. Please remove the existing directory or provide a new folder name."
+            cls._logger.error(error_msg)
path: taipy/_cli/_scaffold_cli.py
text: @user1:
Can't we simply get rid of the cookie-cutter exception?

Plus I believe we don't need to use the taipy logger here. There is no application running. We just exit the command line with a message.

@author:
We can not get the `output_dir` since it's handle by cookiecutter and is only returned if the command success.

@user1:
Oh, right, it is not part of the command line arguments. It is in the answer to the application name question!



@author:
That may be to vague though, because we do not mention the folder name in the log. It would look weird, something like:
```
Application root folder name [taipy_application]: a
Application main Python file [main.py]: 
Application title [Taipy Application]: 
Page names in multi-page application? []: 
Does the application use scenario management or version management? [No]: 
Does the application use Rest API? [No]: 
[2024-04-25 14:32:17.716][Taipy][ERROR] Please remove the existing directory or provide a new folder name.
```

There is nothing indicate that the "a" folder exists, but only a message said to remove the existing folder.

@user1:
That's true. And what is the output of this piece of code: `{str(err)[7:]}`?


@author:
In cookiecutter, the message is defined as
```python
f'Error: "{dir_to_create}" directory already exists'
```

So the `[7:]` is to remove the "Error: " part of the message

@author:
I use the `logger.error` so that we can indicate this is an error raised by Taipy. Print would not have the same effect and we should keep the `"Error: "` part of the message

@user1:
OK, I got it now, and I agree. Thank you.

However, since this is for a command line and not for runtime, I would still not use a logger but the print function directly. 
That means we don't need to remove the 'Error' part of the cookiedcutter message.

 ```suggestion
            error_msg = f"{str(err)}. Please remove the existing directory or provide a new folder name."
            print(error_msg)
```

What do you think?

@user1:
SOrry about that. We sent our messages at the same time.


@author:
Yes that makes sense