Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init User Flow Rework #1501

Merged
merged 8 commits into from Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements/base.txt
Expand Up @@ -7,7 +7,7 @@ cookiecutter~=1.6.0
aws-sam-translator==1.15.1
docker~=4.0
dateparser~=0.7
python-dateutil~=2.6
python-dateutil~=2.6, <2.8.1
requests==2.22.0
serverlessrepo==0.1.9
aws_lambda_builders==0.5.0
2 changes: 1 addition & 1 deletion samcli/commands/init/__init__.py
Expand Up @@ -74,7 +74,7 @@
help="Dependency manager of your Lambda runtime",
required=False,
)
@click.option("-o", "--output-dir", type=click.Path(), help="Where to output the initialized app into")
@click.option("-o", "--output-dir", type=click.Path(), help="Where to output the initialized app into", default=".")
@click.option("-n", "--name", help="Name of your project to be generated as a folder")
@click.option(
"--app-template",
Expand Down
40 changes: 0 additions & 40 deletions samcli/commands/init/init_generator.py
Expand Up @@ -9,47 +9,7 @@


def do_generate(location, runtime, dependency_manager, output_dir, name, no_input, extra_context):
no_build_msg = """
Project generated: {output_dir}/{name}

Steps you can take next within the project folder
===================================================
[*] Invoke Function: sam local invoke HelloWorldFunction --event event.json
[*] Start API Gateway locally: sam local start-api
""".format(
output_dir=output_dir, name=name
)

build_msg = """
Project generated: {output_dir}/{name}

Steps you can take next within the project folder
===================================================
[*] Install dependencies
[*] Invoke Function: sam local invoke HelloWorldFunction --event event.json
[*] Start API Gateway locally: sam local start-api
""".format(
output_dir=output_dir, name=name
)

no_build_step_required = (
"python",
"python3.7",
"python3.6",
"python2.7",
"nodejs",
"nodejs4.3",
"nodejs6.10",
"nodejs8.10",
"nodejs10.x",
"ruby2.5",
)
next_step_msg = no_build_msg if runtime in no_build_step_required else build_msg
try:
generate_project(location, runtime, dependency_manager, output_dir, name, no_input, extra_context)
if not location:
click.secho(next_step_msg, bold=True)
click.secho("Read {name}/README.md for further instructions\n".format(name=name), bold=True)
click.secho("[*] Project initialization is now complete", fg="green")
except GenerateProjectFailedError as e:
raise UserException(str(e))
51 changes: 29 additions & 22 deletions samcli/commands/init/init_templates.py
Expand Up @@ -32,28 +32,33 @@ def __init__(self, no_interactive=False, auto_clone=True):

def prompt_for_location(self, runtime, dependency_manager):
options = self.init_options(runtime, dependency_manager)
choices = map(str, range(1, len(options) + 1))
choice_num = 1
for o in options:
if o.get("displayName") is not None:
msg = str(choice_num) + " - " + o.get("displayName")
click.echo(msg)
else:
msg = (
str(choice_num)
+ " - Default Template for runtime "
+ runtime
+ " with dependency manager "
+ dependency_manager
)
click.echo(msg)
choice_num = choice_num + 1
choice = click.prompt("Template Selection", type=click.Choice(choices), show_choices=False)
template_md = options[int(choice) - 1] # zero index
if len(options) == 1:
template_md = options[0]
else:
choices = list(map(str, range(1, len(options) + 1)))
choice_num = 1
click.echo("\nAWS quick start application templates:")
for o in options:
if o.get("displayName") is not None:
msg = "\t" + str(choice_num) + " - " + o.get("displayName")
click.echo(msg)
else:
msg = (
"\t"
+ str(choice_num)
+ " - Default Template for runtime "
+ runtime
+ " with dependency manager "
+ dependency_manager
)
click.echo(msg)
choice_num = choice_num + 1
choice = click.prompt("Template selection", type=click.Choice(choices), show_choices=False)
template_md = options[int(choice) - 1] # zero index
if template_md.get("init_location") is not None:
return template_md["init_location"]
return (template_md["init_location"], "hello-world")
if template_md.get("directory") is not None:
return os.path.join(self.repo_path, template_md["directory"])
return (os.path.join(self.repo_path, template_md["directory"]), template_md["appTemplate"])
raise UserException("Invalid template. This should not be possible, please raise an issue.")

def location_from_app_template(self, runtime, dependency_manager, app_template):
Expand Down Expand Up @@ -150,7 +155,9 @@ def _should_clone_repo(self, expected_path):
path = Path(expected_path)
if path.exists():
if not self._no_interactive:
overwrite = click.confirm("Init templates exist on disk. Do you wish to update?")
overwrite = click.confirm(
"\nQuick start templates may have been updated. Do you want to re-download the latest", default=True
)
if overwrite:
shutil.rmtree(expected_path) # fail hard if there is an issue
return True
Expand All @@ -160,6 +167,6 @@ def _should_clone_repo(self, expected_path):
if self._no_interactive:
return self._auto_clone
do_clone = click.confirm(
"This process will clone app templates from https://github.com/awslabs/aws-sam-cli-app-templates - is this ok?"
"\nAllow SAM CLI to download AWS-provided quick start templates from Github", default=True
)
return do_clone
81 changes: 66 additions & 15 deletions samcli/commands/init/interactive_init_flow.py
Expand Up @@ -3,7 +3,7 @@
"""
import click

from samcli.local.common.runtime_template import RUNTIMES, RUNTIME_TO_DEPENDENCY_MANAGERS
from samcli.local.common.runtime_template import INIT_RUNTIMES, RUNTIME_TO_DEPENDENCY_MANAGERS
from samcli.commands.init.init_generator import do_generate
from samcli.commands.init.init_templates import InitTemplates

Expand All @@ -12,43 +12,94 @@ def do_interactive(location, runtime, dependency_manager, output_dir, name, app_
if app_template:
location_opt_choice = "1"
else:
click.echo("1 - Use a Managed Application Template\n2 - Provide a Custom Location")
location_opt_choice = click.prompt("Location Choice", type=click.Choice(["1", "2"]), show_choices=False)
click.echo("Which template source would you like to use?")
click.echo("\t1 - AWS Quick Start Templates\n\t2 - Custom Template Location")
location_opt_choice = click.prompt("Choice", type=click.Choice(["1", "2"]), show_choices=False)
if location_opt_choice == "2":
_generate_from_location(location, runtime, dependency_manager, output_dir, name, app_template, no_input)
else:
_generate_from_app_template(location, runtime, dependency_manager, output_dir, name, app_template)


def _generate_from_location(location, runtime, dependency_manager, output_dir, name, app_template, no_input):
location = click.prompt("Template location (git, mercurial, http(s), zip, path)", type=str)
if not output_dir:
output_dir = click.prompt("Output Directory", type=click.Path(), default=".")
location = click.prompt("\nTemplate location (git, mercurial, http(s), zip, path)", type=str)
summary_msg = """
-----------------------
Generating application:
-----------------------
Location: {location}
Output Directory: {output_dir}

To do this without interactive prompts, you can run:

sam init --location {location} --output-dir {output_dir}
""".format(
location=location, output_dir=output_dir
)
click.echo(summary_msg)
do_generate(location, runtime, dependency_manager, output_dir, name, no_input, None)


# pylint: disable=too-many-statements
def _generate_from_app_template(location, runtime, dependency_manager, output_dir, name, app_template):
extra_context = None
if not name:
name = click.prompt("Project Name", type=str)
if not runtime:
runtime = click.prompt("Runtime", type=click.Choice(RUNTIMES))
choices = list(map(str, range(1, len(INIT_RUNTIMES) + 1)))
choice_num = 1
click.echo("\nWhich runtime would you like to use?")
for r in INIT_RUNTIMES:
msg = "\t" + str(choice_num) + " - " + r
click.echo(msg)
choice_num = choice_num + 1
choice = click.prompt("Runtime", type=click.Choice(choices), show_choices=False)
runtime = INIT_RUNTIMES[int(choice) - 1] # zero index
if not dependency_manager:
valid_dep_managers = RUNTIME_TO_DEPENDENCY_MANAGERS.get(runtime)
if valid_dep_managers is None:
dependency_manager = None
elif len(valid_dep_managers) == 1:
dependency_manager = valid_dep_managers[0]
else:
dependency_manager = click.prompt(
"Dependency Manager", type=click.Choice(valid_dep_managers), default=valid_dep_managers[0]
)
choices = list(map(str, range(1, len(valid_dep_managers) + 1)))
choice_num = 1
click.echo("\nWhich dependency manager would you like to use?")
for dm in valid_dep_managers:
msg = "\t" + str(choice_num) + " - " + dm
click.echo(msg)
choice_num = choice_num + 1
choice = click.prompt("Dependency manager", type=click.Choice(choices), show_choices=False)
dependency_manager = valid_dep_managers[int(choice) - 1] # zero index
if not name:
name = click.prompt("\nProject name", type=str, default="sam-app")
templates = InitTemplates()
if app_template is not None:
location = templates.location_from_app_template(runtime, dependency_manager, app_template)
extra_context = {"project_name": name, "runtime": runtime}
else:
location = templates.prompt_for_location(runtime, dependency_manager)
location, app_template = templates.prompt_for_location(runtime, dependency_manager)
extra_context = {"project_name": name, "runtime": runtime}
no_input = True
if not output_dir:
output_dir = click.prompt("Output Directory", type=click.Path(), default=".")
summary_msg = """
-----------------------
Generating application:
-----------------------
Name: {name}
Runtime: {runtime}
Dependency Manager: {dependency_manager}
Application Template: {app_template}
Output Directory: {output_dir}

Non-interactive init command with parameters:

sam init --name {name} --runtime {runtime} --dependency-manager {dependency_manager} --app-template {app_template} --output-dir {output_dir}

Next steps can be found in the README file at {output_dir}/{name}/README.md
""".format(
name=name,
runtime=runtime,
dependency_manager=dependency_manager,
app_template=app_template,
output_dir=output_dir,
)
click.echo(summary_msg)
do_generate(location, runtime, dependency_manager, output_dir, name, no_input, extra_context)
15 changes: 14 additions & 1 deletion samcli/local/common/runtime_template.py
Expand Up @@ -99,4 +99,17 @@
itertools.chain(*[c["runtimes"] for c in list(itertools.chain(*(RUNTIME_DEP_TEMPLATE_MAPPING.values())))])
)

INIT_RUNTIMES = RUNTIMES.union(RUNTIME_DEP_TEMPLATE_MAPPING.keys())
INIT_RUNTIMES = [
"nodejs10.x",
"python3.7",
"ruby2.5",
"go1.x",
"java8",
"dotnetcore2.1",
"nodejs8.10",
"nodejs6.10",
"python3.6",
"python2.7",
"dotnetcore2.0",
"dotnetcore1.0",
]
54 changes: 38 additions & 16 deletions tests/unit/commands/init/test_cli.py
Expand Up @@ -95,19 +95,14 @@ def test_init_cli_interactive(self, generate_project_patch, sd_mock):
# WHEN the user follows interactive init prompts

# 1: selecting managed templates
# 3: ruby2.5 response to runtime
# test-project: response to name
# ruby2.5: response to runtime
# bundler: response to dependency manager
# N: Don't clone/update the source repo
# 1: First choice will always be the hello world example
user_input = """
1
3
test-project
ruby2.5
bundler
N
1
.
"""
runner = CliRunner()
result = runner.invoke(init_cmd, input=user_input)
Expand All @@ -125,22 +120,51 @@ def test_init_cli_interactive(self, generate_project_patch, sd_mock):
{"project_name": "test-project", "runtime": "ruby2.5"},
)

@patch("samcli.commands.init.init_templates.InitTemplates._shared_dir_check")
@patch("samcli.commands.init.init_generator.generate_project")
def test_init_cli_interactive_multiple_dep_mgrs(self, generate_project_patch, sd_mock):
# WHEN the user follows interactive init prompts

# 1: selecting managed templates
# 5: java8 response to runtime
# 2: gradle as the dependency manager
# test-project: response to name
# N: Don't clone/update the source repo
user_input = """
1
5
2
test-project
N
"""
runner = CliRunner()
result = runner.invoke(init_cmd, input=user_input)

# THEN we should receive no errors
self.assertFalse(result.exception)
generate_project_patch.assert_called_once_with(
# need to change the location validation check
ANY,
"java8",
"gradle",
".",
"test-project",
True,
{"project_name": "test-project", "runtime": "java8"},
)

@patch("samcli.commands.init.init_templates.InitTemplates._shared_dir_check")
@patch("samcli.commands.init.init_generator.generate_project")
def test_init_cli_int_with_app_template(self, generate_project_patch, sd_mock):
# WHEN the user follows interactive init prompts

# 3: ruby2.5 response to runtime
# test-project: response to name
# ruby2.5: response to runtime
# bundler: response to dependency manager
# N: Don't clone/update the source repo
# .: output dir
user_input = """
3
test-project
ruby2.5
bundler
N
.
"""
runner = CliRunner()
result = runner.invoke(init_cmd, ["--app-template", "hello-world"], input=user_input)
Expand All @@ -165,11 +189,9 @@ def test_init_cli_int_from_location(self, generate_project_patch, sd_mock):

# 2: selecting custom location
# foo: the "location"
# output/: the "output dir"
user_input = """
2
foo
output/
"""

runner = CliRunner()
Expand All @@ -182,7 +204,7 @@ def test_init_cli_int_from_location(self, generate_project_patch, sd_mock):
"foo",
None,
None,
"output/",
".",
None,
False,
None,
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/commands/init/test_templates.py
Expand Up @@ -45,8 +45,9 @@ def test_fallback_options(self, git_exec_mock, prompt_mock, sd_mock):
mock_sub.side_effect = OSError("Fail")
mock_cfg.return_value = "/tmp/test-sam"
it = InitTemplates(True)
location = it.prompt_for_location("ruby2.5", "bundler")
location, app_template = it.prompt_for_location("ruby2.5", "bundler")
self.assertTrue(search("cookiecutter-aws-sam-hello-ruby", location))
self.assertEqual("hello-world", app_template)

@patch("samcli.commands.init.init_templates.InitTemplates._git_executable")
@patch("click.prompt")
Expand All @@ -58,8 +59,9 @@ def test_fallback_process_error(self, git_exec_mock, prompt_mock, sd_mock):
mock_sub.side_effect = subprocess.CalledProcessError("fail", "fail", "not found".encode("utf-8"))
mock_cfg.return_value = "/tmp/test-sam"
it = InitTemplates(True)
location = it.prompt_for_location("ruby2.5", "bundler")
location, app_template = it.prompt_for_location("ruby2.5", "bundler")
self.assertTrue(search("cookiecutter-aws-sam-hello-ruby", location))
self.assertEqual("hello-world", app_template)

def test_git_executable_windows(self):
with patch("platform.system", new_callable=MagicMock) as mock_platform:
Expand Down