diff --git a/requirements/base.txt b/requirements/base.txt index 0c4dac42ca..782a8a7465 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -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 diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index 79a0966a5f..db92f30765 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -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", diff --git a/samcli/commands/init/init_generator.py b/samcli/commands/init/init_generator.py index 5c1111f3fc..dc6bbbea8b 100644 --- a/samcli/commands/init/init_generator.py +++ b/samcli/commands/init/init_generator.py @@ -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)) diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index f6a5e9cb79..8f53bb4efd 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -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): @@ -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 @@ -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 diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index dbb55a7b44..2e1784d3fe 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -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 @@ -12,8 +12,9 @@ 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: @@ -21,34 +22,84 @@ def do_interactive(location, runtime, dependency_manager, output_dir, name, app_ 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) diff --git a/samcli/local/common/runtime_template.py b/samcli/local/common/runtime_template.py index eda4ae3802..7788e2cf53 100644 --- a/samcli/local/common/runtime_template.py +++ b/samcli/local/common/runtime_template.py @@ -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", +] diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 98dc5e1b88..0e9ebc364e 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -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) @@ -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) @@ -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() @@ -182,7 +204,7 @@ def test_init_cli_int_from_location(self, generate_project_patch, sd_mock): "foo", None, None, - "output/", + ".", None, False, None, diff --git a/tests/unit/commands/init/test_templates.py b/tests/unit/commands/init/test_templates.py index 6f10ca61e1..0b91a5f00f 100644 --- a/tests/unit/commands/init/test_templates.py +++ b/tests/unit/commands/init/test_templates.py @@ -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") @@ -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: