From 83ef992af538c96cb97de5139f19ca4e36a7a1ad Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Tue, 13 Jul 2021 16:53:51 -0700 Subject: [PATCH 1/4] Autofill question if default value is presented --- samcli/lib/cookiecutter/question.py | 48 +++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/samcli/lib/cookiecutter/question.py b/samcli/lib/cookiecutter/question.py index 3b3f483f676..e72ce0a4c4a 100644 --- a/samcli/lib/cookiecutter/question.py +++ b/samcli/lib/cookiecutter/question.py @@ -1,4 +1,5 @@ """ This module represents the questions to ask to the user to fulfill the cookiecutter context. """ +from abc import ABC, abstractmethod from enum import Enum from typing import Any, Dict, List, Optional, Type, Union @@ -14,7 +15,18 @@ class QuestionKind(Enum): default = "default" -class Question: +class Promptable(ABC): + """ + Abstract class Question, Info, Choice, Confirm implement. + These classes need to implement their own prompt() method to prompt differently. + """ + + @abstractmethod + def prompt(self, text: str, default_answer: Optional[Any]) -> Any: + pass + + +class Question(Promptable): """ A question to be prompt to the user in an interactive flow where the response is used to fulfill the cookiecutter context. @@ -53,12 +65,14 @@ def __init__( text: str, default: Optional[Union[str, Dict]] = None, is_required: Optional[bool] = None, + allow_autofill: Optional[bool] = None, next_question_map: Optional[Dict[str, str]] = None, default_next_question_key: Optional[str] = None, ): self._key = key self._text = text self._required = is_required + self._allow_autofill = allow_autofill self._default_answer = default # if it is an optional question, set an empty default answer to prevent click from keep asking for an answer if not self._required and self._default_answer is None: @@ -108,7 +122,15 @@ def ask(self, context: Optional[Dict] = None) -> Any: # set an empty default answer to prevent click from keep asking for an answer if not self._required and resolved_default_answer is None: resolved_default_answer = "" - return click.prompt(text=self._resolve_text(context), default=resolved_default_answer) + + # skip the question and directly use the default value if autofill is allowed. + if resolved_default_answer is not None and self._allow_autofill: + return resolved_default_answer + + return self.prompt(self._resolve_text(context), resolved_default_answer) + + def prompt(self, text: str, default_answer: Optional[Any]) -> Any: + return click.prompt(text=text, default=default_answer) def get_next_question_key(self, answer: Any) -> Optional[str]: # _next_question_map is a Dict[str(answer), str(next question key)] @@ -200,13 +222,13 @@ def _resolve_default_answer(self, context: Optional[Dict] = None) -> Optional[An class Info(Question): - def ask(self, context: Optional[Dict] = None) -> None: - return click.echo(message=self._resolve_text(context)) + def prompt(self, text: str, default_answer: Optional[Any]) -> Any: + return click.echo(message=text) class Confirm(Question): - def ask(self, context: Optional[Dict] = None) -> bool: - return click.confirm(text=self._resolve_text(context)) + def prompt(self, text: str, default_answer: Optional[Any]) -> Any: + return click.confirm(text=text) class Choice(Question): @@ -217,27 +239,27 @@ def __init__( options: List[str], default: Optional[str] = None, is_required: Optional[bool] = None, + allow_autofill: Optional[bool] = None, next_question_map: Optional[Dict[str, str]] = None, default_next_question_key: Optional[str] = None, ): if not options: raise ValueError("No defined options") self._options = options - super().__init__(key, text, default, is_required, next_question_map, default_next_question_key) + super().__init__(key, text, default, is_required, allow_autofill, next_question_map, default_next_question_key) - def ask(self, context: Optional[Dict] = None) -> str: - resolved_default_answer = self._resolve_default_answer(context) - click.echo(self._resolve_text(context)) + def prompt(self, text: str, default_answer: Optional[Any]) -> Any: + click.echo(text) for index, option in enumerate(self._options): click.echo(f"\t{index + 1} - {option}") options_indexes = self._get_options_indexes(base=1) choices = list(map(str, options_indexes)) choice = click.prompt( text="Choice", - default=resolved_default_answer, + default=default_answer, show_choices=False, type=click.Choice(choices), - show_default=resolved_default_answer is not None, + show_default=default_answer is not None, ) return self._options[int(choice) - 1] @@ -260,6 +282,7 @@ def create_question_from_json(question_json: Dict) -> Question: options = question_json.get("options") default = question_json.get("default") is_required = question_json.get("isRequired") + allow_autofill = question_json.get("allowAutofill") next_question_map = question_json.get("nextQuestion") default_next_question = question_json.get("defaultNextQuestion") kind_str = question_json.get("kind") @@ -271,6 +294,7 @@ def create_question_from_json(question_json: Dict) -> Question: "text": text, "default": default, "is_required": is_required, + "allow_autofill": allow_autofill, "next_question_map": next_question_map, "default_next_question_key": default_next_question, } From b46966e6593ec37db4d277482024de8ad2617748 Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Tue, 13 Jul 2021 17:29:54 -0700 Subject: [PATCH 2/4] Add unit test --- tests/unit/lib/cookiecutter/test_question.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/lib/cookiecutter/test_question.py b/tests/unit/lib/cookiecutter/test_question.py index e61dee88f27..2db7055357c 100644 --- a/tests/unit/lib/cookiecutter/test_question.py +++ b/tests/unit/lib/cookiecutter/test_question.py @@ -27,6 +27,7 @@ def setUp(self): key=self._ANY_KEY, default=self._ANY_ANSWER, is_required=True, + allow_autofill=False, next_question_map=self._ANY_NEXT_QUESTION_MAP, default_next_question_key=self._ANY_DEFAULT_NEXT_QUESTION_KEY, ) @@ -151,6 +152,16 @@ def test_ask_resolves_from_cookiecutter_context_with_default_object_missing_keys with self.assertRaises(KeyError): question.ask(context=context) + def test_question_allow_autofill_with_default_value(self): + q = Question(text=self._ANY_TEXT, key=self._ANY_KEY, is_required=True, allow_autofill=True, default="123") + self.assertEquals("123", q.ask()) + + @patch("samcli.lib.cookiecutter.question.click") + def test_question_allow_autofill_without_default_value(self, click_mock): + answer_mock = click_mock.prompt.return_value = Mock() + q = Question(text=self._ANY_TEXT, key=self._ANY_KEY, is_required=True, allow_autofill=True) + self.assertEquals(answer_mock, q.ask()) + class TestChoice(TestCase): def setUp(self): From bb78f7d797758c86c9d51479378588949c76049a Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Tue, 13 Jul 2021 17:55:53 -0700 Subject: [PATCH 3/4] Fix integ test --- tests/integration/pipeline/test_init_command.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/pipeline/test_init_command.py b/tests/integration/pipeline/test_init_command.py index 621bf324946..d0e8e47b9d5 100644 --- a/tests/integration/pipeline/test_init_command.py +++ b/tests/integration/pipeline/test_init_command.py @@ -1,5 +1,6 @@ from pathlib import Path +from samcli.commands.pipeline.bootstrap.cli import PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME from tests.integration.pipeline.base import InitIntegBase from tests.testing_utils import run_command_with_inputs @@ -31,6 +32,10 @@ class TestInit(InitIntegBase): Here we use Jenkins template for testing """ + def setUp(self) -> None: + # make sure there is no pipelineconfig.toml, otherwise the autofill could affect the question flow + Path(PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME).unlink(missing_ok=True) + def test_quick_start(self): generated_jenkinsfile_path = Path("Jenkinsfile") self.generated_files.append(generated_jenkinsfile_path) From 788dcbbc24a7fb895d05f42f315d7f2fca3e7176 Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Wed, 14 Jul 2021 09:22:20 -0700 Subject: [PATCH 4/4] Fix integ test --- samcli/lib/cookiecutter/question.py | 9 +++++---- tests/integration/pipeline/test_init_command.py | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/samcli/lib/cookiecutter/question.py b/samcli/lib/cookiecutter/question.py index e72ce0a4c4a..4fad0ea0208 100644 --- a/samcli/lib/cookiecutter/question.py +++ b/samcli/lib/cookiecutter/question.py @@ -118,15 +118,16 @@ def ask(self, context: Optional[Dict] = None) -> Any: The user provided answer. """ resolved_default_answer = self._resolve_default_answer(context) - # if it is an optional question with no default answer, - # set an empty default answer to prevent click from keep asking for an answer - if not self._required and resolved_default_answer is None: - resolved_default_answer = "" # skip the question and directly use the default value if autofill is allowed. if resolved_default_answer is not None and self._allow_autofill: return resolved_default_answer + # if it is an optional question with no default answer, + # set an empty default answer to prevent click from keep asking for an answer + if not self._required and resolved_default_answer is None: + resolved_default_answer = "" + return self.prompt(self._resolve_text(context), resolved_default_answer) def prompt(self, text: str, default_answer: Optional[Any]) -> Any: diff --git a/tests/integration/pipeline/test_init_command.py b/tests/integration/pipeline/test_init_command.py index d0e8e47b9d5..bbb269cb00f 100644 --- a/tests/integration/pipeline/test_init_command.py +++ b/tests/integration/pipeline/test_init_command.py @@ -34,7 +34,9 @@ class TestInit(InitIntegBase): def setUp(self) -> None: # make sure there is no pipelineconfig.toml, otherwise the autofill could affect the question flow - Path(PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME).unlink(missing_ok=True) + pipelineconfig_file = Path(PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME) + if pipelineconfig_file.exists(): + pipelineconfig_file.unlink() def test_quick_start(self): generated_jenkinsfile_path = Path("Jenkinsfile")