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

feat: Dynamically determine latest init runtime #7593

Merged
merged 3 commits into from
Oct 25, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions samcli/commands/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
64 changes: 61 additions & 3 deletions samcli/commands/init/interactive_init_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import logging
import pathlib
import re
import tempfile
from typing import Optional, Tuple

import click
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,
Expand All @@ -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 (
Expand Down Expand Up @@ -323,6 +325,60 @@ 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")
"""
latest_major = 0
latest_minor = 0

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

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

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

return selected_version


def _generate_default_hello_world_application(
use_case: str,
package_type: Optional[str],
Expand Down Expand Up @@ -356,8 +412,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)


Expand Down
51 changes: 43 additions & 8 deletions tests/unit/commands/init/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,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
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -3193,3 +3196,35 @@ def test_init_cli_generate_app_template_provide_via_application_insights_options
True,
False,
)

@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_correct_latest(self, versions, expected):
with mock.patch(
"samcli.commands.init.interactive_init_flow.SUPPORTED_RUNTIMES",
versions,
):
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()
Loading