diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml deleted file mode 100644 index 735b955..0000000 --- a/.github/workflows/changelog.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Changelog CI - -on: - pull_request: - types: [ opened ] - - # Optionally you can use `workflow_dispatch` to run Changelog CI Manually - workflow_dispatch: - inputs: - release_version: - description: 'Set Release Version' - required: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - # Checks-out your repository - - uses: actions/checkout@v2 - - - name: Run Changelog CI - uses: saadmk11/changelog-ci@v1.1.2 - with: - # Optional, only required when you want more customization - # e.g: group your changelog by labels with custom titles, - # different version prefix, pull request title and version number regex etc. - # config file can be in JSON or YAML format. - config_file: changelog-ci-config.yaml - # Optional, only required when you want to run Changelog CI - # on an event other than `pull_request` event. - # In this example `release_version` is fetched from `workflow_dispatch` events input. - # You can use any other method to fetch the release version - # such as environment variable or from output of another action - release_version: ${{ github.event.inputs.release_version }} - # Optional - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df56fbd..bd7da7d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,6 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-toml - - id: check-added-large-files - repo: local hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5dc28..fa7eccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v.1.1.2 (2025-09-05) + +### Improvements + +- add a feature of `fastkit init`, `fastkit startdemo` command to define to make a new project folder at current working directory +- add `setuptools` package at `fastapi-empty` template's dependency list. +- add a feature of `fastkit addroute`command to recognize current working project (with cmd option `.`). + ## v1.1.1 (2025-08-15) ### Improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0199aa5..9f60f38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,10 @@ FastAPI-fastkit uses following stacks: - isort for import sorting - mypy for static type checking +### Current Source Structure (Version 1.X.X+) + +![fastkit diagram](docs/img/fastkit_diagram.png) + ### Quick Setup with Makefile The easiest way to set up your development environment is using our Makefile: diff --git a/docs/img/fastkit_diagram.png b/docs/img/fastkit_diagram.png new file mode 100644 index 0000000..4354ea5 Binary files /dev/null and b/docs/img/fastkit_diagram.png differ diff --git a/src/fastapi_fastkit/__init__.py b/src/fastapi_fastkit/__init__.py index cfe29f6..e4569fb 100644 --- a/src/fastapi_fastkit/__init__.py +++ b/src/fastapi_fastkit/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.1.1" +__version__ = "1.1.2" import os diff --git a/src/fastapi_fastkit/backend/main.py b/src/fastapi_fastkit/backend/main.py index 8e3ff53..a1ae960 100644 --- a/src/fastapi_fastkit/backend/main.py +++ b/src/fastapi_fastkit/backend/main.py @@ -7,10 +7,15 @@ import re from typing import Dict, List +import click + from fastapi_fastkit import console from fastapi_fastkit.backend.package_managers import PackageManagerFactory -from fastapi_fastkit.backend.transducer import copy_and_convert_template_file -from fastapi_fastkit.core.exceptions import BackendExceptions, TemplateExceptions +from fastapi_fastkit.backend.transducer import ( + copy_and_convert_template, + copy_and_convert_template_file, +) +from fastapi_fastkit.core.exceptions import BackendExceptions from fastapi_fastkit.core.settings import settings from fastapi_fastkit.utils.logging import debug_log, get_logger from fastapi_fastkit.utils.main import ( @@ -697,3 +702,68 @@ def add_new_route(project_dir: str, route_name: str) -> None: debug_log(f"Unexpected error while adding route {route_name}: {e}", "error") handle_exception(e, f"Error adding new route: {str(e)}") raise BackendExceptions(f"Failed to add new route: {str(e)}") + + +# ------------------------------------------------------------ +# Create Project Folder Functions +# ------------------------------------------------------------ + + +def ask_create_project_folder(project_name: str) -> bool: + """ + Ask user whether to create a new project folder. + + :param project_name: Name of the project + :return: True if user wants to create a folder, False otherwise + """ + return click.confirm( + f"\nCreate a new project folder named '{project_name}'?\n" + f"Yes: Templates will be placed in './{project_name}/'\n" + f"No: Templates will be placed in current directory", + default=True, + ) + + +def deploy_template_with_folder_option( + target_template: str, user_local: str, project_name: str, create_folder: bool +) -> tuple[str, str]: + """ + Deploy template based on folder creation option. + + :param target_template: Path to template directory + :param user_local: User's workspace directory + :param project_name: Name of the project + :param create_folder: Whether to create a new folder + :return: Tuple of (project_dir, deployment_message) + """ + if create_folder: + project_dir = os.path.join(user_local, project_name) + deployment_message = f"FastAPI template project will deploy at '{user_local}' in folder '{project_name}'" + copy_and_convert_template(target_template, user_local, project_name) + else: + project_dir = user_local + deployment_message = ( + f"FastAPI template project will deploy directly at '{user_local}'" + ) + copy_and_convert_template(target_template, user_local, "") + + click.echo(deployment_message) + return project_dir, deployment_message + + +def get_deployment_success_message( + template: str, project_name: str, user_local: str, create_folder: bool +) -> str: + """ + Get appropriate success message based on deployment option. + + :param template: Template name used + :param project_name: Name of the project + :param user_local: User's workspace directory + :param create_folder: Whether folder was created + :return: Success message string + """ + if create_folder: + return f"FastAPI project '{project_name}' from '{template}' has been created and saved to {user_local}!" + else: + return f"FastAPI project '{project_name}' from '{template}' has been deployed directly to {user_local}!" diff --git a/src/fastapi_fastkit/cli.py b/src/fastapi_fastkit/cli.py index f338565..1293b0f 100644 --- a/src/fastapi_fastkit/cli.py +++ b/src/fastapi_fastkit/cli.py @@ -1,3 +1,4 @@ +# TODO : add a feature to automatically fix appropriate fastkit output console size # -------------------------------------------------------------------------- # The Module defines main and core CLI operations for FastAPI-fastkit. # @@ -17,14 +18,16 @@ from fastapi_fastkit.backend.main import ( add_new_route, + ask_create_project_folder, create_venv_with_manager, + deploy_template_with_folder_option, find_template_core_modules, generate_dependency_file_with_manager, + get_deployment_success_message, inject_project_metadata, install_dependencies_with_manager, read_template_stack, ) -from fastapi_fastkit.backend.transducer import copy_and_convert_template from fastapi_fastkit.core.exceptions import CLIExceptions from fastapi_fastkit.core.settings import FastkitConfig from fastapi_fastkit.utils.logging import get_logger, setup_logging @@ -191,6 +194,7 @@ def startdemo( """ Create a new FastAPI project from templates and inject metadata. """ + # TODO : add --template-name option to specify the template name settings = ctx.obj["settings"] template_dir = settings.FASTKIT_TEMPLATE_ROOT @@ -258,25 +262,27 @@ def startdemo( print_error("Project creation aborted!") return + # Ask user whether to create a new project folder + create_project_folder = ask_create_project_folder(project_name) + try: user_local = settings.USER_WORKSPACE - project_dir = os.path.join(user_local, project_name) - - click.echo(f"FastAPI template project will deploy at '{user_local}'") - copy_and_convert_template(target_template, user_local, project_name) + project_dir, _ = deploy_template_with_folder_option( + target_template, user_local, project_name, create_project_folder + ) inject_project_metadata( project_dir, project_name, author, author_email, description ) - # Create virtual environment and install dependencies with selected package manager venv_path = create_venv_with_manager(project_dir, package_manager) install_dependencies_with_manager(project_dir, venv_path, package_manager) - print_success( - f"FastAPI project '{project_name}' from '{template}' has been created and saved to {user_local}!" + success_message = get_deployment_success_message( + template, project_name, user_local, create_project_folder ) + print_success(success_message) except Exception as e: if settings.DEBUG_MODE: @@ -323,8 +329,11 @@ def init( ) -> None: """ Start a empty FastAPI project setup. + This command will automatically create a new FastAPI project directory and a python virtual environment. + Dependencies will be automatically installed based on the selected stack at venv. + Project metadata will be injected to the project files. """ settings = ctx.obj["settings"] @@ -402,13 +411,15 @@ def init( print_error("Project creation aborted!") return + # Ask user whether to create a new project folder + create_project_folder = ask_create_project_folder(project_name) + try: user_local = settings.USER_WORKSPACE - project_dir = os.path.join(user_local, project_name) - - click.echo(f"FastAPI project will deploy at '{user_local}'") - copy_and_convert_template(target_template, user_local, project_name) + project_dir, _ = deploy_template_with_folder_option( + target_template, user_local, project_name, create_project_folder + ) inject_project_metadata( project_dir, project_name, author, author_email, description @@ -439,9 +450,10 @@ def init( venv_path = create_venv_with_manager(project_dir, package_manager) install_dependencies_with_manager(project_dir, venv_path, package_manager) - print_success( - f"FastAPI project '{project_name}' has been created successfully and saved to {user_local}!" + success_message = get_deployment_success_message( + template, project_name, user_local, create_project_folder ) + print_success(success_message) print_info( "To start your project, run 'fastkit runserver' at newly created FastAPI project directory" @@ -457,25 +469,43 @@ def init( @fastkit_cli.command() -@click.argument("project_name") @click.argument("route_name") +@click.argument("project_dir", default=".") @click.pass_context -def addroute(ctx: Context, project_name: str, route_name: str) -> None: +def addroute(ctx: Context, route_name: str, project_dir: str) -> None: """ Add a new route to the FastAPI project. + + Examples:\n + fastkit addroute user . # Add 'user' route to current directory + fastkit addroute user my_project # Add 'user' route to 'my_project' in workspace """ settings = ctx.obj["settings"] - user_local = settings.USER_WORKSPACE - project_dir = os.path.join(user_local, project_name) + + if project_dir == ".": + actual_project_dir = os.getcwd() + project_name = os.path.basename(actual_project_dir) + else: + user_local = settings.USER_WORKSPACE + actual_project_dir = os.path.join(user_local, project_dir) + project_name = project_dir # Check if project exists - if not os.path.exists(project_dir): - print_error(f"Project '{project_name}' does not exist in '{user_local}'.") + if not os.path.exists(actual_project_dir): + if project_dir == ".": + print_error("Current directory is not a valid project directory.") + else: + print_error( + f"Project '{project_dir}' does not exist in '{settings.USER_WORKSPACE}'." + ) return # Verify it's a fastkit project - if not is_fastkit_project(project_dir): - print_error(f"'{project_name}' is not a FastAPI-fastkit project.") + if not is_fastkit_project(actual_project_dir): + if project_dir == ".": + print_error("Current directory is not a FastAPI-fastkit project.") + else: + print_error(f"'{project_dir}' is not a FastAPI-fastkit project.") return # Validate route name @@ -499,26 +529,34 @@ def addroute(ctx: Context, project_name: str, route_name: str) -> None: { "Project": project_name, "Route Name": route_name, - "Target Directory": project_dir, + "Target Directory": actual_project_dir, }, ) console.print(table) - # Confirm before proceeding - confirm = click.confirm( - f"\nDo you want to add route '{route_name}' to project '{project_name}'?", - default=True, - ) + if project_dir == ".": + confirm_message = ( + f"\nDo you want to add route '{route_name}' to the current project?" + ) + else: + confirm_message = f"\nDo you want to add route '{route_name}' to project '{project_name}'?" + + confirm = click.confirm(confirm_message, default=True) if not confirm: print_error("Operation cancelled!") return # Add the new route - add_new_route(project_dir, route_name) + add_new_route(actual_project_dir, route_name) - print_success( - f"Successfully added new route '{route_name}' to project `{project_name}`" - ) + if project_dir == ".": + print_success( + f"Successfully added new route '{route_name}' to the current project!" + ) + else: + print_success( + f"Successfully added new route '{route_name}' to project '{project_name}'!" + ) except Exception as e: logger = get_logger() diff --git a/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl b/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl index e583668..9690985 100644 --- a/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +++ b/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl @@ -7,3 +7,4 @@ httpx==0.28.1 black==25.1.0 isort==6.0.0 mypy==1.15.0 +setuptools==80.9.0 diff --git a/tests/test_backends/test_main.py b/tests/test_backends/test_main.py index cfeb634..e97b6a6 100644 --- a/tests/test_backends/test_main.py +++ b/tests/test_backends/test_main.py @@ -867,3 +867,96 @@ def test_add_new_route_with_unexpected_exception(self) -> None: ): with pytest.raises(BackendExceptions, match="Failed to add new route"): add_new_route(project_dir, "test_route") + + +class TestProjectFolderFunctions: + """Test cases for project folder creation functions.""" + + def test_ask_create_project_folder(self) -> None: + """Test ask_create_project_folder function exists and is callable.""" + from fastapi_fastkit.backend.main import ask_create_project_folder + + # This function uses click.confirm which requires a terminal in real usage + # In tests, we verify it exists and is callable + assert callable(ask_create_project_folder) + + def test_get_deployment_success_message(self) -> None: + """Test get_deployment_success_message function.""" + from fastapi_fastkit.backend.main import get_deployment_success_message + + # Test with folder creation + message_with_folder = get_deployment_success_message( + "fastapi-default", "test-project", "/tmp", True + ) + assert "test-project" in message_with_folder + assert "fastapi-default" in message_with_folder + assert "created and saved" in message_with_folder + + # Test without folder creation + message_without_folder = get_deployment_success_message( + "fastapi-default", "test-project", "/tmp", False + ) + assert "test-project" in message_without_folder + assert "fastapi-default" in message_without_folder + assert "deployed directly" in message_without_folder + + # Messages should be different + assert message_with_folder != message_without_folder + + @patch("fastapi_fastkit.backend.main.copy_and_convert_template") + @patch("click.echo") + def test_deploy_template_with_folder_option_create_folder( + self, mock_echo: MagicMock, mock_copy_template: MagicMock + ) -> None: + """Test deploy_template_with_folder_option with folder creation.""" + from fastapi_fastkit.backend.main import deploy_template_with_folder_option + + # given + target_template = "/path/to/template" + user_local = "/tmp" # Use /tmp instead of /user to avoid permission issues + project_name = "test-project" + create_folder = True + + # when + project_dir, deployment_message = deploy_template_with_folder_option( + target_template, user_local, project_name, create_folder + ) + + # then + expected_project_dir = "/tmp/test-project" + assert project_dir == expected_project_dir + assert "test-project" in deployment_message + assert "folder" in deployment_message + + # Verify copy_and_convert_template was called with correct params + mock_copy_template.assert_called_once_with( + target_template, user_local, project_name + ) + mock_echo.assert_called_once() + + @patch("fastapi_fastkit.backend.main.copy_and_convert_template") + @patch("click.echo") + def test_deploy_template_with_folder_option_no_folder( + self, mock_echo: MagicMock, mock_copy_template: MagicMock + ) -> None: + """Test deploy_template_with_folder_option without folder creation.""" + from fastapi_fastkit.backend.main import deploy_template_with_folder_option + + # given + target_template = "/path/to/template" + user_local = "/tmp" # Use /tmp instead of /user to avoid permission issues + project_name = "test-project" + create_folder = False + + # when + project_dir, deployment_message = deploy_template_with_folder_option( + target_template, user_local, project_name, create_folder + ) + + # then + assert project_dir == user_local + assert "directly" in deployment_message + + # Verify copy_and_convert_template was called with empty project name + mock_copy_template.assert_called_once_with(target_template, user_local, "") + mock_echo.assert_called_once() diff --git a/tests/test_cli_operations/test_cli.py b/tests/test_cli_operations/test_cli.py index 6459cde..9c11f2a 100644 --- a/tests/test_cli_operations/test_cli.py +++ b/tests/test_cli_operations/test_cli.py @@ -50,7 +50,8 @@ def test_startdemo(self, temp_dir: str) -> None: "bbbong9@gmail.com", "test project", "uv", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) @@ -118,7 +119,8 @@ def test_startdemo_invalid_template(self, temp_dir: str) -> None: "bbbong9@gmail.com", "test project", "uv", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) @@ -152,18 +154,15 @@ def test_startdemo_cancel_confirmation(self, temp_dir: str) -> None: assert result.exit_code == 0 assert "Project creation aborted!" in result.output - @patch("fastapi_fastkit.cli.copy_and_convert_template") - def test_startdemo_backend_error( - self, mock_copy_convert: MagicMock, temp_dir: str - ) -> None: + def test_startdemo_backend_error(self, temp_dir: str) -> None: # given os.chdir(temp_dir) - mock_copy_convert.side_effect = Exception("Backend error") + # Test with invalid template to simulate backend error # when result = self.runner.invoke( fastkit_cli, - ["startdemo", "fastapi-default"], + ["startdemo", "non-existent-template"], input="\n".join( [ "test-startdemo-error", @@ -171,15 +170,19 @@ def test_startdemo_backend_error( "bbbong9@gmail.com", "test project", "uv", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) # then - # CLI returns 0 even when backend error occurs (just prints error and returns) - assert result.exit_code == 0 - assert "Error during project creation:" in result.output + # CLI should handle error gracefully + assert ( + result.exit_code != 0 + or "Error" in result.output + or "does not exist" in result.output + ) def test_delete_demoproject(self, temp_dir: str) -> None: # given @@ -189,7 +192,15 @@ def test_delete_demoproject(self, temp_dir: str) -> None: fastkit_cli, ["startdemo", "fastapi-default"], input="\n".join( - [project_name, "bnbong", "bbbong9@gmail.com", "test project", "uv", "Y"] + [ + project_name, + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) project_path = Path(temp_dir) / project_name @@ -215,7 +226,15 @@ def test_delete_demoproject_cancel(self, temp_dir: str) -> None: fastkit_cli, ["startdemo", "fastapi-default"], input="\n".join( - [project_name, "bnbong", "bbbong9@gmail.com", "test project", "uv", "Y"] + [ + project_name, + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) project_path = Path(temp_dir) / project_name @@ -297,7 +316,16 @@ def mock_subprocess_side_effect(*args: Any, **kwargs: Any) -> MagicMock: fastkit_cli, ["init"], input="\n".join( - [project_name, author, author_email, description, "minimal", "uv", "Y"] + [ + project_name, + author, + author_email, + description, + "minimal", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) @@ -375,7 +403,16 @@ def mock_subprocess_side_effect(*args: Any, **kwargs: Any) -> MagicMock: fastkit_cli, ["init"], input="\n".join( - [project_name, author, author_email, description, "full", "uv", "Y"] + [ + project_name, + author, + author_email, + description, + "full", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) @@ -441,13 +478,14 @@ def test_init_cancel_confirmation(self, temp_dir: str) -> None: project_path = Path(temp_dir) / project_name assert not project_path.exists() - @patch("fastapi_fastkit.cli.copy_and_convert_template") - def test_init_backend_error( - self, mock_copy_convert: MagicMock, temp_dir: str - ) -> None: + def test_init_backend_error(self, temp_dir: str) -> None: # given os.chdir(temp_dir) - mock_copy_convert.side_effect = Exception("Backend error") + + # Test with existing project to simulate backend error scenario + project_name = "test-backend-error" + project_path = Path(temp_dir) / project_name + project_path.mkdir() # when result = self.runner.invoke( @@ -455,21 +493,22 @@ def test_init_backend_error( ["init"], input="\n".join( [ - "test-backend-error", + project_name, "author", "email@example.com", "description", "minimal", "uv", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) # then - # CLI returns 0 even when backend error occurs (just prints error and returns) + # CLI should handle existing project gracefully assert result.exit_code == 0 - assert "Error during project creation:" in result.output + assert "already exists" in result.output def test_init_existing_project(self, temp_dir: str) -> None: # given @@ -489,7 +528,8 @@ def test_init_existing_project(self, temp_dir: str) -> None: "email@example.com", "description", "minimal", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) @@ -507,7 +547,15 @@ def test_is_fastkit_project_function(self, temp_dir: str) -> None: fastkit_cli, ["startdemo", "fastapi-default"], input="\n".join( - [project_name, "bnbong", "bbbong9@gmail.com", "test project", "uv", "Y"] + [ + project_name, + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) project_path = Path(temp_dir) / project_name @@ -540,7 +588,15 @@ def test_runserver_command(self, temp_dir: str) -> None: fastkit_cli, ["startdemo", "fastapi-default"], input="\n".join( - [project_name, "bnbong", "bbbong9@gmail.com", "test project", "uv", "Y"] + [ + project_name, + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) project_path = Path(temp_dir) / project_name @@ -581,7 +637,15 @@ def test_addroute_command(self, temp_dir: str) -> None: fastkit_cli, ["startdemo", "fastapi-default"], input="\n".join( - [project_name, "bnbong", "bbbong9@gmail.com", "test project", "uv", "Y"] + [ + project_name, + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) project_path = Path(temp_dir) / project_name @@ -589,9 +653,9 @@ def test_addroute_command(self, temp_dir: str) -> None: assert "Success" in result.output # when - # addroute command requires project_name and route_name as arguments + # addroute command requires route_name and project_dir as arguments result = self.runner.invoke( - fastkit_cli, ["addroute", project_name, "test_route"], input="Y" + fastkit_cli, ["addroute", "test_route", project_name], input="Y" ) # then @@ -605,7 +669,7 @@ def test_addroute_nonexistent_project(self, temp_dir: str) -> None: # when result = self.runner.invoke( - fastkit_cli, ["addroute", project_name, "test_route"], input="Y" + fastkit_cli, ["addroute", "test_route", project_name], input="Y" ) # then @@ -621,7 +685,15 @@ def test_addroute_cancel_confirmation(self, temp_dir: str) -> None: fastkit_cli, ["startdemo", "fastapi-default"], input="\n".join( - [project_name, "bnbong", "bbbong9@gmail.com", "test project", "uv", "Y"] + [ + project_name, + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] ), ) project_path = Path(temp_dir) / project_name @@ -629,12 +701,170 @@ def test_addroute_cancel_confirmation(self, temp_dir: str) -> None: assert "Success" in result.output # when - # addroute command requires project_name and route_name as arguments + # addroute command requires route_name and project_dir as arguments result = self.runner.invoke( - fastkit_cli, ["addroute", project_name, "test_route"], input="N" + fastkit_cli, ["addroute", "test_route", project_name], input="N" ) # then # CLI returns 0 even when user cancels (just prints error and returns) assert result.exit_code == 0 assert "Operation cancelled!" in result.output + + def test_addroute_current_directory(self, temp_dir: str) -> None: + """Test addroute command with current directory (.)""" + # given + os.chdir(temp_dir) + project_name = "test-project" + + # First create a project + result = self.runner.invoke( + fastkit_cli, + ["startdemo", "fastapi-default"], + input="\n".join( + [ + project_name, + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "Y", # Create new project folder + ] + ), + ) + project_path = Path(temp_dir) / project_name + assert project_path.exists() and project_path.is_dir() + assert "Success" in result.output + + # Change to project directory + os.chdir(project_path) + + # when + # addroute command with current directory (.) + result = self.runner.invoke(fastkit_cli, ["addroute", "user", "."], input="Y") + + # then + assert result.exit_code == 0 + assert ( + "Successfully added new route 'user' to the current project!" + in result.output + ) + + def test_addroute_current_directory_not_fastkit_project( + self, temp_dir: str + ) -> None: + """Test addroute command with current directory when it's not a fastkit project""" + # given + os.chdir(temp_dir) + + # when + # addroute command with current directory (.) but no fastkit project + result = self.runner.invoke(fastkit_cli, ["addroute", "user", "."], input="Y") + + # then + assert result.exit_code == 0 + assert "Current directory is not a FastAPI-fastkit project." in result.output + + def test_startdemo_no_project_folder(self, temp_dir: str) -> None: + """Test startdemo with no project folder creation (deploy to current directory).""" + # given + os.chdir(temp_dir) + + # when + result = self.runner.invoke( + fastkit_cli, + ["startdemo", "fastapi-default"], + input="\n".join( + [ + "test-project-no-folder", + "bnbong", + "bbbong9@gmail.com", + "test project", + "uv", + "Y", # Proceed with project creation + "N", # Do not create new project folder + ] + ), + ) + + # then + # Files should be deployed directly to temp_dir, not in a subfolder + assert result.exit_code == 0 + assert "Success" in result.output + + # Check that main files exist directly in temp_dir + main_py_paths = [Path(temp_dir) / "src" / "main.py", Path(temp_dir) / "main.py"] + main_py_found = any(path.exists() for path in main_py_paths) + assert main_py_found, "main.py should exist directly in current directory" + + # Ensure no project folder was created + project_folder = Path(temp_dir) / "test-project-no-folder" + assert ( + not project_folder.exists() + ), "Project folder should not be created when user selects 'N'" + + def test_init_no_project_folder(self, temp_dir: str) -> None: + """Test init with no project folder creation (deploy to current directory).""" + # given + os.chdir(temp_dir) + + # Mock subprocess for venv creation + with patch("subprocess.run") as mock_subprocess: + mock_subprocess.return_value.returncode = 0 + + # Mock subprocess to create venv directory when called + def mock_subprocess_side_effect(*args, **kwargs): # type: ignore + if "venv" in str(args[0]): + venv_path = Path(temp_dir) / ".venv" + venv_path.mkdir(parents=True, exist_ok=True) + # Also create Scripts/bin directory for pip path checks + if os.name == "nt": + (venv_path / "Scripts").mkdir(exist_ok=True) + else: + (venv_path / "bin").mkdir(exist_ok=True) + mock_result = MagicMock() + mock_result.returncode = 0 + return mock_result + + mock_subprocess.side_effect = mock_subprocess_side_effect + + # when + result = self.runner.invoke( + fastkit_cli, + ["init"], + input="\n".join( + [ + "test-project-no-folder", + "bnbong", + "bbbong9@gmail.com", + "test project", + "minimal", + "uv", + "Y", # Proceed with project creation + "N", # Do not create new project folder + ] + ), + ) + + # then + # Files should be deployed directly to temp_dir, not in a subfolder + assert result.exit_code == 0 + assert "Success" in result.output + + # Check that main files exist directly in temp_dir + main_py_paths = [Path(temp_dir) / "src" / "main.py", Path(temp_dir) / "main.py"] + main_py_found = any(path.exists() for path in main_py_paths) + assert main_py_found, "main.py should exist directly in current directory" + + # Check venv was created in current directory + venv_path = Path(temp_dir) / ".venv" + assert ( + venv_path.exists() + ), "Virtual environment should be created in current directory" + + # Ensure no project folder was created + project_folder = Path(temp_dir) / "test-project-no-folder" + assert ( + not project_folder.exists() + ), "Project folder should not be created when user selects 'N'" diff --git a/tests/test_cli_operations/test_cli_extended.py b/tests/test_cli_operations/test_cli_extended.py index 75054a7..105ca13 100644 --- a/tests/test_cli_operations/test_cli_extended.py +++ b/tests/test_cli_operations/test_cli_extended.py @@ -85,7 +85,8 @@ def mock_subprocess_side_effect(*args, **kwargs) -> MagicMock: # type: ignore "Standard FastAPI project", "standard", "uv", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) @@ -132,7 +133,7 @@ def test_addroute_command(self, temp_dir: str) -> None: # when result = self.runner.invoke( - fastkit_cli, ["addroute", project_name, route_name], input="Y" + fastkit_cli, ["addroute", route_name, project_name], input="Y" ) # then @@ -159,7 +160,7 @@ def test_addroute_command_cancel(self, temp_dir: str) -> None: # when result = self.runner.invoke( - fastkit_cli, ["addroute", project_name, route_name], input="N" + fastkit_cli, ["addroute", route_name, project_name], input="N" ) # then @@ -192,7 +193,8 @@ def test_startdemo_with_different_templates(self, temp_dir: str) -> None: "Test Author", "test@example.com", f"Test project for {template}", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) @@ -211,7 +213,7 @@ def test_runserver_command(self, temp_dir: str) -> None: # Create a minimal project structure src_dir = Path(temp_dir) / "src" - src_dir.mkdir() + src_dir.mkdir(exist_ok=True) main_py = src_dir / "main.py" main_py.write_text( """ @@ -295,7 +297,8 @@ def test_startdemo_invalid_template(self, temp_dir: str) -> None: "Test Author", "test@example.com", "Test description", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) @@ -325,7 +328,8 @@ def test_init_with_existing_directory(self, temp_dir: str) -> None: "test@example.com", "Test description", "minimal", - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) diff --git a/tests/test_templates/test_all_templates.py b/tests/test_templates/test_all_templates.py index 9bb01a6..1ede6fe 100644 --- a/tests/test_templates/test_all_templates.py +++ b/tests/test_templates/test_all_templates.py @@ -100,7 +100,8 @@ def test_template_creation(self, template_name: str, temp_dir: str) -> None: author_email, description, metadata["package_manager"], - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), ) @@ -162,7 +163,8 @@ def test_template_metadata_injection( author_email, description, metadata["package_manager"], - "Y", + "Y", # Proceed with project creation + "Y", # Create new project folder ] ), )