From 05e3d0f18a2a6ef94de7f3698c06dd9dab6d7919 Mon Sep 17 00:00:00 2001 From: Lucas <12496191+lucashuy@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:45:30 -0700 Subject: [PATCH 1/3] feat: Dynamically determine latest init runtime --- samcli/commands/init/interactive_init_flow.py | 51 ++++++++++++++++++- tests/unit/commands/init/test_cli.py | 44 +++++++++++++--- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 57a1c1f64b..5d1ff1744b 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -4,6 +4,7 @@ import logging import pathlib +import re import tempfile from typing import Optional, Tuple @@ -28,6 +29,7 @@ ) from samcli.lib.config.samconfig import DEFAULT_CONFIG_FILE_NAME from samcli.lib.schemas.schemas_code_manager import do_download_source_code_binding, do_extract_and_merge_schemas_code +from samcli.lib.utils.architecture import SUPPORTED_RUNTIMES from samcli.lib.utils.osutils import remove from samcli.lib.utils.packagetype import IMAGE, ZIP from samcli.local.common.runtime_template import ( @@ -323,6 +325,49 @@ def _generate_from_use_case( ) +def _get_latest_python_runtime() -> str: + """ + Returns the latest support version of Python + SAM CLI supports + + Returns + ------- + str: + The name of the latest Python runtime (ex. "python3.12") + """ + + # set python3.9 as fallback + latest_major = 3 + latest_minor = 9 + + compiled_regex = re.compile(r"python(.*?)\.(.*)") + + for runtime in SUPPORTED_RUNTIMES: + if not runtime.startswith("python"): + continue + + # python3.12 => 3.12 => (3, 12) + version_match = re.match(compiled_regex, runtime) + + if not version_match: + LOG.debug(f"Failed to match version while checking {runtime}") + continue + + matched_groups = version_match.groups() + + try: + version_major = int(matched_groups[0]) + version_minor = int(matched_groups[1]) + except (ValueError, IndexError): + LOG.debug(f"Failed to parse version while checking {runtime}") + continue + + latest_major = version_major if version_major > latest_major else latest_major + latest_minor = version_minor if version_minor > latest_minor else latest_minor + + return f"python{latest_major}.{latest_minor}" + + def _generate_default_hello_world_application( use_case: str, package_type: Optional[str], @@ -356,8 +401,10 @@ def _generate_default_hello_world_application( """ is_package_type_image = bool(package_type == IMAGE) if use_case == "Hello World Example" and not (runtime or base_image or is_package_type_image or dependency_manager): - if click.confirm("\nUse the most popular runtime and package type? (Python and zip)"): - runtime, package_type, dependency_manager, pt_explicit = "python3.9", ZIP, "pip", True + latest_python = _get_latest_python_runtime() + + if click.confirm(f"\nUse the most popular runtime and package type? ({latest_python} and zip)"): + runtime, package_type, dependency_manager, pt_explicit = _get_latest_python_runtime(), ZIP, "pip", True return (runtime, package_type, dependency_manager, pt_explicit) diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 3825c30200..617d94983f 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -3,6 +3,8 @@ import shutil import subprocess import tempfile +from unittest import mock +from parameterized import parameterized import requests from pathlib import Path from typing import Dict, Any @@ -25,7 +27,7 @@ get_template_value, template_does_not_meet_filter_criteria, ) -from samcli.commands.init.interactive_init_flow import get_sorted_runtimes +from samcli.commands.init.interactive_init_flow import _get_latest_python_runtime, get_sorted_runtimes from samcli.lib.init import GenerateProjectFailedError from samcli.lib.utils import osutils from samcli.lib.utils.git_repo import GitRepo @@ -2006,9 +2008,9 @@ def test_init_cli_generate_default_hello_world_app( request_mock.side_effect = requests.Timeout() init_options_from_manifest_mock.return_value = [ { - "directory": "python3.9/cookiecutter-aws-sam-hello-python", + "directory": "python3.12/cookiecutter-aws-sam-hello-python", "displayName": "Hello World Example", - "dependencyManager": "npm", + "dependencyManager": "pip", "appTemplate": "hello-world", "packageType": "Zip", "useCaseName": "Hello World Example", @@ -2026,10 +2028,10 @@ def test_init_cli_generate_default_hello_world_app( get_preprocessed_manifest_mock.return_value = { "Hello World Example": { - "python3.9": { + "python3.12": { "Zip": [ { - "directory": "python3.9/cookiecutter-aws-sam-hello-python3.9", + "directory": "python3.12/cookiecutter-aws-sam-hello-python3.12", "displayName": "Hello World Example", "dependencyManager": "pip", "appTemplate": "hello-world", @@ -2070,16 +2072,17 @@ def test_init_cli_generate_default_hello_world_app( runner = CliRunner() result = runner.invoke(init_cmd, input=user_input) + print(result.stdout) self.assertFalse(result.exception) generate_project_patch.assert_called_once_with( ANY, ZIP, - "python3.9", + "python3.12", "pip", ".", "test-project", True, - {"project_name": "test-project", "runtime": "python3.9", "architectures": {"value": ["x86_64"]}}, + {"project_name": "test-project", "runtime": "python3.12", "architectures": {"value": ["x86_64"]}}, False, False, False, @@ -3193,3 +3196,30 @@ def test_init_cli_generate_app_template_provide_via_application_insights_options True, False, ) + + def test_latest_python_fetcher_returns_latest(self): + latest_python = "python3.100000" + + with mock.patch( + "samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES", + {"python3.2": Any, latest_python: Any, "python3.14": Any}, + ): + result = _get_latest_python_runtime() + + self.assertEqual(result, latest_python) + + @parameterized.expand( + [ + ("dotnet3.1",), + ("foo bar",), + ("",), + ] + ) + def test_latest_python_fetcher_fallback_invalid_runtime(self, invalid_runtime): + with mock.patch( + "samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES", + {invalid_runtime: Any}, + ): + result = _get_latest_python_runtime() + + self.assertEqual(result, "python3.9") From 922f1b98fc404dc2032213e8f931175c10612c93 Mon Sep 17 00:00:00 2001 From: Lucas <12496191+lucashuy@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:08:05 -0700 Subject: [PATCH 2/3] Caught edge case and added extra debug log --- samcli/commands/init/interactive_init_flow.py | 13 ++++++++++--- tests/unit/commands/init/test_cli.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 5d1ff1744b..a55adccee6 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -362,10 +362,17 @@ def _get_latest_python_runtime() -> str: LOG.debug(f"Failed to parse version while checking {runtime}") continue - latest_major = version_major if version_major > latest_major else latest_major - latest_minor = version_minor if version_minor > latest_minor else latest_minor + if version_major > latest_major: + latest_major = version_major + latest_minor = version_minor + elif version_major == latest_major: + latest_minor = version_minor if version_minor > latest_minor else latest_minor - return f"python{latest_major}.{latest_minor}" + selected_version = f"python{latest_major}.{latest_minor}" + + LOG.debug(f"Using {selected_version} as the latest runtime version") + + return selected_version def _generate_default_hello_world_application( diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 617d94983f..af7a4ef1b1 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -3223,3 +3223,18 @@ def test_latest_python_fetcher_fallback_invalid_runtime(self, invalid_runtime): result = _get_latest_python_runtime() self.assertEqual(result, "python3.9") + + @parameterized.expand( + [ + ({"python7.8": Any, "python9.1": Any}, "python9.1"), + ({"python6.1": Any, "python4.7": Any}, "python6.1"), + ] + ) + def test_latest_python_fetcher_major_minor_difference(self, versions, expected): + with mock.patch( + "samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES", + versions, + ): + result = _get_latest_python_runtime() + + self.assertEqual(result, expected) From de5023883103a1db10bb9c5d6b10f5f08c2450bf Mon Sep 17 00:00:00 2001 From: Lucas <12496191+lucashuy@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:16:57 -0700 Subject: [PATCH 3/3] remove default runtime --- samcli/commands/exceptions.py | 7 +++ samcli/commands/init/interactive_init_flow.py | 14 ++++-- tests/unit/commands/init/test_cli.py | 48 ++++++++----------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/samcli/commands/exceptions.py b/samcli/commands/exceptions.py index d8708c9272..5f835e0d33 100644 --- a/samcli/commands/exceptions.py +++ b/samcli/commands/exceptions.py @@ -152,3 +152,10 @@ class LinterRuleMatchedException(UserException): """ The linter matched a rule meaning that the template linting failed """ + + +class PopularRuntimeNotFoundException(Exception): + """ + Exception thrown when we were not able to parse the SUPPORTED_RUNTIMES + constant correctly for the latest runtime + """ diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index a55adccee6..bebb62bb2b 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -12,7 +12,7 @@ from botocore.exceptions import ClientError, WaiterError from samcli.commands._utils.options import generate_next_command_recommendation -from samcli.commands.exceptions import InvalidInitOptionException, SchemasApiException +from samcli.commands.exceptions import InvalidInitOptionException, PopularRuntimeNotFoundException, SchemasApiException from samcli.commands.init.init_flow_helpers import ( _get_image_from_runtime, _get_runtime_from_image, @@ -335,10 +335,8 @@ def _get_latest_python_runtime() -> str: str: The name of the latest Python runtime (ex. "python3.12") """ - - # set python3.9 as fallback - latest_major = 3 - latest_minor = 9 + latest_major = 0 + latest_minor = 0 compiled_regex = re.compile(r"python(.*?)\.(.*)") @@ -368,6 +366,12 @@ def _get_latest_python_runtime() -> str: elif version_major == latest_major: latest_minor = version_minor if version_minor > latest_minor else latest_minor + if not latest_major: + # major version is still 0, assume that something went wrong + # this in theory should not happen as long as Python is + # listed in the SUPPORTED_RUNTIMES constant + raise PopularRuntimeNotFoundException("Was unable to search for the latest supported runtime") + selected_version = f"python{latest_major}.{latest_minor}" LOG.debug(f"Using {selected_version} as the latest runtime version") diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index af7a4ef1b1..2c1dd94301 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -15,7 +15,7 @@ import click from click.testing import CliRunner -from samcli.commands.exceptions import UserException +from samcli.commands.exceptions import PopularRuntimeNotFoundException, UserException from samcli.commands.init import cli as init_cmd from samcli.commands.init.command import do_cli as init_cli from samcli.commands.init.command import PackageType @@ -3197,40 +3197,14 @@ def test_init_cli_generate_app_template_provide_via_application_insights_options False, ) - def test_latest_python_fetcher_returns_latest(self): - latest_python = "python3.100000" - - with mock.patch( - "samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES", - {"python3.2": Any, latest_python: Any, "python3.14": Any}, - ): - result = _get_latest_python_runtime() - - self.assertEqual(result, latest_python) - - @parameterized.expand( - [ - ("dotnet3.1",), - ("foo bar",), - ("",), - ] - ) - def test_latest_python_fetcher_fallback_invalid_runtime(self, invalid_runtime): - with mock.patch( - "samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES", - {invalid_runtime: Any}, - ): - result = _get_latest_python_runtime() - - self.assertEqual(result, "python3.9") - @parameterized.expand( [ + ({"python3.2": Any, "python3.100000": Any, "python3.14": Any}, "python3.100000"), ({"python7.8": Any, "python9.1": Any}, "python9.1"), ({"python6.1": Any, "python4.7": Any}, "python6.1"), ] ) - def test_latest_python_fetcher_major_minor_difference(self, versions, expected): + def test_latest_python_fetcher_correct_latest(self, versions, expected): with mock.patch( "samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES", versions, @@ -3238,3 +3212,19 @@ def test_latest_python_fetcher_major_minor_difference(self, versions, expected): result = _get_latest_python_runtime() self.assertEqual(result, expected) + + def test_latest_python_fetcher_has_valid_supported_runtimes(self): + """ + Mainly checks if the SUPPORTED_RUNTIMES constant actually has + Python runtime inside of it + """ + result = _get_latest_python_runtime() + self.assertTrue(result, "Python was not found in the SUPPORTED_RUNTIMES const") + + def test_latest_python_fetchers_raises_not_found(self): + with mock.patch( + "samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES", + {"invalid": Any}, + ): + with self.assertRaises(PopularRuntimeNotFoundException): + _get_latest_python_runtime()