Skip to content

Commit

Permalink
Auto skip questions if stage detected (#3045)
Browse files Browse the repository at this point in the history
* Autofill question if default value is presented
  • Loading branch information
aahung authored Jul 14, 2021
1 parent 174f68c commit de116e3
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 12 deletions.
49 changes: 37 additions & 12 deletions samcli/lib/cookiecutter/question.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -104,11 +118,20 @@ def ask(self, context: Optional[Dict] = None) -> Any:
The user provided answer.
"""
resolved_default_answer = self._resolve_default_answer(context)

# 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 click.prompt(text=self._resolve_text(context), default=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)]
Expand Down Expand Up @@ -200,13 +223,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):
Expand All @@ -217,27 +240,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]

Expand All @@ -260,6 +283,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")
Expand All @@ -271,6 +295,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,
}
Expand Down
7 changes: 7 additions & 0 deletions tests/integration/pipeline/test_init_command.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -31,6 +32,12 @@ 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
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")
self.generated_files.append(generated_jenkinsfile_path)
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/lib/cookiecutter/test_question.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit de116e3

Please sign in to comment.