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()