From a6fe18fded0bddec91959998916fc96ac6af5008 Mon Sep 17 00:00:00 2001 From: Mo Zaatar Date: Fri, 30 Dec 2022 15:44:16 +0800 Subject: [PATCH] feat(bootstrap): prompt for env tokens (#114) * feat(bootstrap): prompt user for empty values in `.env` file --- src/algokit/core/bootstrap.py | 48 +++++++-- ...ap_all.test_bootstrap_all_env.approved.txt | 2 +- ...ll.test_bootstrap_all_sub_dir.approved.txt | 2 +- tests/bootstrap/test_bootstrap_env.py | 98 ++++++++++++++++++- ...nv_different_prompt_scenarios.approved.txt | 31 ++++++ ...otenv_missing_template_exists.approved.txt | 2 +- ...tstrap_env_dotenv_with_values.approved.txt | 16 +++ 7 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_different_prompt_scenarios.approved.txt create mode 100644 tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_with_values.approved.txt diff --git a/src/algokit/core/bootstrap.py b/src/algokit/core/bootstrap.py index ffb339f6..0ba39124 100644 --- a/src/algokit/core/bootstrap.py +++ b/src/algokit/core/bootstrap.py @@ -2,10 +2,11 @@ import platform import sys from pathlib import Path -from shutil import copyfile, which +from shutil import which from typing import Callable, Iterator import click +import questionary from algokit.core import proc @@ -51,14 +52,43 @@ def bootstrap_env(project_dir: Path) -> None: if env_path.exists(): logger.info(".env already exists; skipping bootstrap of .env") - else: - logger.debug(f"{env_path} doesn't exist yet") - if not env_template_path.exists(): - logger.info("No .env or .env.template file; nothing to do here, skipping bootstrap of .env") - else: - logger.debug(f"{env_template_path} exists") - logger.info(f"Copying {env_template_path} to {env_path}") - copyfile(env_template_path, env_path) + return + + logger.debug(f"{env_path} doesn't exist yet") + if not env_template_path.exists(): + logger.info("No .env or .env.template file; nothing to do here, skipping bootstrap of .env") + return + + logger.debug(f"{env_template_path} exists") + logger.info(f"Copying {env_template_path} to {env_path} and prompting for empty values") + # find all empty values in .env file and prompt the user for a value + with env_template_path.open(encoding="utf-8") as env_template_file, env_path.open( + mode="w", encoding="utf-8" + ) as env_file: + comment_lines: list[str] = [] + for line in env_template_file: + # strip newline character(s) from end of line for simpler handling + stripped_line = line.strip() + # if it is a comment line, keep it in var and continue + if stripped_line.startswith("#"): + comment_lines.append(line) + env_file.write(line) + # keep blank lines in output but don't accumulate them in comments + elif not stripped_line: + env_file.write(line) + else: + # lines not blank and not empty + var_name, *var_value = stripped_line.split("=", maxsplit=1) + # if it is an empty value, the user should be prompted for value with the comment line above + if var_value and not var_value[0]: + logger.info("".join(comment_lines)) + var_name = var_name.strip() + new_value = questionary.text(f"Please provide a value for {var_name}:").unsafe_ask() + env_file.write(f"{var_name}={new_value}\n") + else: + # this is a line with value, reset comment lines. + env_file.write(line) + comment_lines = [] def bootstrap_poetry(project_dir: Path, install_prompt: Callable[[str], bool]) -> None: diff --git a/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_env.approved.txt b/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_env.approved.txt index e958996a..dac780a8 100644 --- a/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_env.approved.txt +++ b/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_env.approved.txt @@ -2,5 +2,5 @@ DEBUG: Checking {current_working_directory} for bootstrapping needs DEBUG: Running `algokit bootstrap env` DEBUG: {current_working_directory}/.env doesn't exist yet DEBUG: {current_working_directory}/.env.template exists -Copying {current_working_directory}/.env.template to {current_working_directory}/.env +Copying {current_working_directory}/.env.template to {current_working_directory}/.env and prompting for empty values Finished bootstrapping {current_working_directory} diff --git a/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_sub_dir.approved.txt b/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_sub_dir.approved.txt index 8d03d9d5..17136d4e 100644 --- a/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_sub_dir.approved.txt +++ b/tests/bootstrap/test_bootstrap_all.test_bootstrap_all_sub_dir.approved.txt @@ -4,7 +4,7 @@ DEBUG: Checking {current_working_directory}/live_dir for bootstrapping needs DEBUG: Running `algokit bootstrap env` DEBUG: {current_working_directory}/live_dir/.env doesn't exist yet DEBUG: {current_working_directory}/live_dir/.env.template exists -Copying {current_working_directory}/live_dir/.env.template to {current_working_directory}/live_dir/.env +Copying {current_working_directory}/live_dir/.env.template to {current_working_directory}/live_dir/.env and prompting for empty values DEBUG: Running `algokit bootstrap poetry` DEBUG: Running 'poetry --version' in '{current_working_directory}' DEBUG: poetry: STDOUT diff --git a/tests/bootstrap/test_bootstrap_env.py b/tests/bootstrap/test_bootstrap_env.py index 0133a122..b4a4e46a 100644 --- a/tests/bootstrap/test_bootstrap_env.py +++ b/tests/bootstrap/test_bootstrap_env.py @@ -1,10 +1,30 @@ +import click +import pytest from _pytest.tmpdir import TempPathFactory -from utils.approvals import verify +from approvaltests.scrubbers.scrubbers import Scrubber +from prompt_toolkit.input import PipeInput +from utils.approvals import TokenScrubber, combine_scrubbers, verify from utils.click_invoker import invoke from tests import get_combined_verify_output +def make_output_scrubber(**extra_tokens: str) -> Scrubber: + default_tokens = { + "KqueueSelector": "Using selector: KqueueSelector", + "EpollSelector": "Using selector: EpollSelector", + "IocpProactor": "Using proactor: IocpProactor", + } + tokens = default_tokens | extra_tokens + return combine_scrubbers( + click.unstyle, + TokenScrubber(tokens=tokens), + lambda t: t.replace("KqueueSelector", "selector") + .replace("EpollSelector", "selector") + .replace("IocpProactor", "selector"), + ) + + def test_bootstrap_env_no_files(tmp_path_factory: TempPathFactory): cwd = tmp_path_factory.mktemp("cwd") @@ -41,3 +61,79 @@ def test_bootstrap_env_dotenv_missing_template_exists(tmp_path_factory: TempPath assert result.exit_code == 0 verify(get_combined_verify_output(result.output, ".env", (cwd / ".env").read_text("utf-8"))) + + +def test_bootstrap_env_dotenv_with_values(tmp_path_factory: TempPathFactory, mock_questionary_input: PipeInput): + cwd = tmp_path_factory.mktemp("cwd") + (cwd / ".env.template").write_text( + """ +TOKEN_1=123 +# comment for token 2 - you should enter a valid value +# another comment +TOKEN_2_WITH_MULTI_LINES_COMMENT=test +TOKEN_3=test value with spaces + +TOKEN_4_WITH_NO_EQUALS_SIGN +# another comment +TOKEN_5_SPECIAL_CHAR=* +""" + ) + + result = invoke( + "bootstrap env", + cwd=cwd, + ) + + assert result.exit_code == 0 + verify(get_combined_verify_output(result.output, ".env", (cwd / ".env").read_text("utf-8"))) + + +def test_bootstrap_env_dotenv_different_prompt_scenarios( + tmp_path_factory: TempPathFactory, + mock_questionary_input: PipeInput, + request: pytest.FixtureRequest, + mock_os_dependency: None, +): + cwd = tmp_path_factory.mktemp("cwd") + (cwd / ".env.template").write_text( + """ +TOKEN_1=123 + +# comment for token 2 - you should enter a valid value +# another comment +TOKEN_2_WITH_MULTI_LINES_COMMENT= +TOKEN_3=test value + +TOKEN_4_WITH_SPACES = +TOKEN_5_WITHOUT_COMMENT= +TOKEN_WITH_NO_EQUALS_SIGN +# another comment +TOKEN_6_EMPTY_WITH_COMMENT= +TOKEN_7_VALUE_WILL_BE_EMPTY= +TOKEN_8 = value with spaces +TOKEN_8_SPECIAL_CHAR=* +""" + ) + + # provide values for tokens + mock_questionary_input.send_text("test value for TOKEN_2_WITH_MULTI_LINES_COMMENT") + mock_questionary_input.send_text("\n") # enter + mock_questionary_input.send_text("test value for TOKEN_4_WITH_SPACES") + mock_questionary_input.send_text("\n") # enter + mock_questionary_input.send_text("test value for TOKEN_5_WITHOUT_COMMENT") + mock_questionary_input.send_text("\n") # enter + mock_questionary_input.send_text("test value for TOKEN_6_EMPTY_WITH_COMMENT") + mock_questionary_input.send_text("\n") # enter + mock_questionary_input.send_text("") # Empty value for TOKEN_7_VALUE_WILL_BE_EMPTY + mock_questionary_input.send_text("\n") # enter + + result = invoke( + "bootstrap env", + cwd=cwd, + ) + + assert result.exit_code == 0 + verify( + get_combined_verify_output(result.output, ".env", (cwd / ".env").read_text("utf-8")), + scrubber=make_output_scrubber(), + ) diff --git a/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_different_prompt_scenarios.approved.txt b/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_different_prompt_scenarios.approved.txt new file mode 100644 index 00000000..d467ff33 --- /dev/null +++ b/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_different_prompt_scenarios.approved.txt @@ -0,0 +1,31 @@ +DEBUG: {current_working_directory}/.env doesn't exist yet +DEBUG: {current_working_directory}/.env.template exists +Copying {current_working_directory}/.env.template to {current_working_directory}/.env and prompting for empty values +# comment for token 2 - you should enter a valid value +# another comment + +DEBUG: {selector} + + +# another comment + + +---- +.env: +---- + +TOKEN_1=123 + +# comment for token 2 - you should enter a valid value +# another comment +TOKEN_2_WITH_MULTI_LINES_COMMENT=test value for TOKEN_2_WITH_MULTI_LINES_COMMENT +TOKEN_3=test value + +TOKEN_4_WITH_SPACES=test value for TOKEN_4_WITH_SPACES +TOKEN_5_WITHOUT_COMMENT=test value for TOKEN_5_WITHOUT_COMMENT +TOKEN_WITH_NO_EQUALS_SIGN +# another comment +TOKEN_6_EMPTY_WITH_COMMENT=test value for TOKEN_6_EMPTY_WITH_COMMENT +TOKEN_7_VALUE_WILL_BE_EMPTY= +TOKEN_8 = value with spaces +TOKEN_8_SPECIAL_CHAR=* diff --git a/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_missing_template_exists.approved.txt b/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_missing_template_exists.approved.txt index 127e6853..7fe59291 100644 --- a/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_missing_template_exists.approved.txt +++ b/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_missing_template_exists.approved.txt @@ -1,6 +1,6 @@ DEBUG: {current_working_directory}/.env doesn't exist yet DEBUG: {current_working_directory}/.env.template exists -Copying {current_working_directory}/.env.template to {current_working_directory}/.env +Copying {current_working_directory}/.env.template to {current_working_directory}/.env and prompting for empty values ---- .env: ---- diff --git a/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_with_values.approved.txt b/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_with_values.approved.txt new file mode 100644 index 00000000..09a99ea6 --- /dev/null +++ b/tests/bootstrap/test_bootstrap_env.test_bootstrap_env_dotenv_with_values.approved.txt @@ -0,0 +1,16 @@ +DEBUG: {current_working_directory}/.env doesn't exist yet +DEBUG: {current_working_directory}/.env.template exists +Copying {current_working_directory}/.env.template to {current_working_directory}/.env and prompting for empty values +---- +.env: +---- + +TOKEN_1=123 +# comment for token 2 - you should enter a valid value +# another comment +TOKEN_2_WITH_MULTI_LINES_COMMENT=test +TOKEN_3=test value with spaces + +TOKEN_4_WITH_NO_EQUALS_SIGN +# another comment +TOKEN_5_SPECIAL_CHAR=*