diff --git a/.changes/next-release/1822269289894500-feature-Python-88643.json b/.changes/next-release/1822269289894500-feature-Python-88643.json new file mode 100644 index 000000000..4120a0d7d --- /dev/null +++ b/.changes/next-release/1822269289894500-feature-Python-88643.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "Python", + "description": "Add support for Python 3.14 ([#2163](https://github.com/aws/chalice/pull/2163))" +} diff --git a/.changes/next-release/1822363688067875-enhancement-pip-542.json b/.changes/next-release/1822363688067875-enhancement-pip-542.json new file mode 100644 index 000000000..8b6c58387 --- /dev/null +++ b/.changes/next-release/1822363688067875-enhancement-pip-542.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "Pip", + "description": "Update pip to the latest version (<26.2) ([#2163](https://github.com/aws/chalice/pull/2163))" +} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 288851ed7..d0469b5f0 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -17,10 +17,10 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.10', 3.11, 3.12, 3.13] + python-version: ['3.10', 3.11, 3.12, 3.13, 3.14] steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 name: Set up Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} @@ -33,13 +33,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', 3.11, 3.12, 3.13] + python-version: ['3.10', 3.11, 3.12, 3.13, 3.14] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v6 with: node-version: '24' - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 name: Set up Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} @@ -66,7 +66,7 @@ jobs: # python-version: [3.6, 3.7, 3.8, 3.9] # steps: # - uses: actions/checkout@v2 -# - uses: actions/setup-python@v2 +# - uses: actions/setup-python@v6 # name: Set up Python ${{ matrix.python-version }} # with: # python-version: ${{ matrix.python-version }} diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 7847a3acc..d2b48e46f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -67,7 +67,7 @@ First, create a virtual environment for chalice:: $ source venv/bin/activate Keep in mind that chalice is designed to work with AWS Lambda. -Make sure to create your virtual environment using Python 3.10 to 3.13, +Make sure to create your virtual environment using Python 3.10 to 3.14, as these are versions currently supported by both AWS Lambda and chalice. Next, you'll need to install chalice. The easiest way to configure this diff --git a/README.rst b/README.rst index d22fa0d96..baee72422 100644 --- a/README.rst +++ b/README.rst @@ -111,7 +111,7 @@ Quickstart In this tutorial, you'll use the ``chalice`` command line utility to create and deploy a basic REST API. This quickstart uses Python 3.10, but AWS Chalice supports all versions of python supported by AWS Lambda, -which includes Python 3.10 through python 3.13. +which includes Python 3.10 through python 3.14. To install Chalice, we'll first create and activate a virtual environment in python3.10:: diff --git a/chalice/analyzer.py b/chalice/analyzer.py index 7e2e3f6cf..3578769e8 100644 --- a/chalice/analyzer.py +++ b/chalice/analyzer.py @@ -193,6 +193,13 @@ def __init__(self, value): self.value = value +def get_string_literal_value(node): + # type: (ast.AST) -> Optional[str] + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value + return None + + class ParsedCode(object): def __init__(self, parsed_ast, symbol_table): # type: (ast.AST, ChainedSymbolTable) -> None @@ -411,8 +418,9 @@ def visit_Assign(self, node): rhs_inferred_type = self._get_inferred_type_for_node(node.value) if rhs_inferred_type is None: # Special casing assignment to a string literal. - if isinstance(node.value, ast.Str): - rhs_inferred_type = StringLiteral(node.value.s) + string_value = get_string_literal_value(node.value) + if string_value is not None: + rhs_inferred_type = StringLiteral(string_value) self._set_inferred_type_for_node(node.value, rhs_inferred_type) for t in node.targets: if isinstance(t, ast.Name): @@ -451,9 +459,10 @@ def visit_Call(self, node): # e_0(e_1) : B3CT[e_1] if len(node.args) >= 1: service_arg = node.args[0] - if isinstance(service_arg, ast.Str): + service_name = get_string_literal_value(service_arg) + if service_name is not None: self._set_inferred_type_for_node( - node, Boto3ClientType(service_arg.s)) + node, Boto3ClientType(service_name)) elif isinstance(self._get_inferred_type_for_node(service_arg), StringLiteral): sub_type = self._get_inferred_type_for_node(service_arg) diff --git a/chalice/config.py b/chalice/config.py index 22ba2b4b6..6f7d571ea 100644 --- a/chalice/config.py +++ b/chalice/config.py @@ -150,11 +150,11 @@ def lambda_python_version(self) -> str: major, minor = sys.version_info[0], sys.version_info[1] if (major, minor) < (3, 10): return 'python3.10' - elif (major, minor) <= (3, 12): + elif (major, minor) <= (3, 13): # Otherwise we use your current version of python if Lambda # supports it. return f'python{major}.{minor}' - return 'python3.13' + return 'python3.14' @property def log_retention_in_days(self) -> int: diff --git a/chalice/deploy/packager.py b/chalice/deploy/packager.py index b51bedb39..e3504092f 100644 --- a/chalice/deploy/packager.py +++ b/chalice/deploy/packager.py @@ -80,6 +80,7 @@ class BaseLambdaDeploymentPackager(object): 'python3.11': 'cp311', 'python3.12': 'cp312', 'python3.13': 'cp313', + 'python3.14': 'cp314', } def __init__( @@ -499,6 +500,7 @@ class DependencyBuilder(object): 'cp311': (2, 26), 'cp312': (2, 34), 'cp313': (2, 34), + 'cp314': (2, 34), } # Fallback version if we're on an unknown python version # not in _RUNTIME_GLIBC. @@ -635,9 +637,27 @@ def _download_binary_wheels( # Try to get binary wheels for each package that isn't compatible. logger.debug("Downloading manylinux wheels: %s", packages) self._pip.download_manylinux_wheels( - abi, [pkg.identifier for pkg in packages], directory + abi, + [pkg.identifier for pkg in packages], + directory, + self._get_pip_platforms(abi), ) + def _get_pip_platforms(self, abi: str) -> List[str]: + # Pip treats --platform as a literal tag and does not auto-include + # lower manylinux_X_Y versions, so we enumerate every glibc minor up + # to the runtime's. The trailing manylinux2014_x86_64 alias engages + # pip's legacy compatibility hierarchy (manylinux1, manylinux2010). + runtime_major, runtime_minor = self._RUNTIME_GLIBC.get( + abi, self._DEFAULT_GLIBC + ) + platforms = [ + 'manylinux_%s_%s_x86_64' % (runtime_major, minor) + for minor in range(17, runtime_minor + 1) + ] + platforms.append('manylinux2014_x86_64') + return platforms + def _download_sdists(self, packages: Set[Package], directory: str) -> None: logger.debug("Downloading missing sdists: %s", packages) self._pip.download_sdists( @@ -1161,7 +1181,11 @@ def download_all_dependencies( self.build_wheel(wheel_package_path, directory) def download_manylinux_wheels( - self, abi: str, packages: List[str], directory: str + self, + abi: str, + packages: List[str], + directory: str, + platforms: Optional[List[str]] = None, ) -> None: """Download wheel files for manylinux for all the given packages.""" # If any one of these dependencies fails pip will bail out. Since we @@ -1169,23 +1193,21 @@ def download_manylinux_wheels( # each package to pip individually. The return code of pip doesn't # matter here since we will inspect the working directory to see which # wheels were downloaded. We are only interested in wheel files - # compatible with lambda, which means manylinux1_x86_64 platform and + # compatible with lambda, which means a manylinux x86_64 platform and # cpython implementation. The compatible abi depends on the python # version and is checked later. + if platforms is None: + platforms = ['manylinux2014_x86_64'] for package in packages: - arguments = [ - '--only-binary=:all:', - '--no-deps', - '--platform', - 'manylinux2014_x86_64', - '--implementation', - 'cp', - '--abi', - abi, - '--dest', - directory, + arguments = ['--only-binary=:all:', '--no-deps'] + for platform in platforms: + arguments.extend(['--platform', platform]) + arguments.extend([ + '--implementation', 'cp', + '--abi', abi, + '--dest', directory, package, - ] + ]) self._execute('download', arguments) def download_sdists(self, packages: List[str], directory: str) -> None: diff --git a/chalice/pipeline.py b/chalice/pipeline.py index 72438213f..405bb90d2 100644 --- a/chalice/pipeline.py +++ b/chalice/pipeline.py @@ -132,7 +132,9 @@ class CreatePipelineTemplateV2(BasePipelineTemplate): "Description": "Enter the name of your application" }, "CodeBuildImage": { - "Default": "aws/codebuild/amazonlinux2-x86_64-standard:3.0", + "Default": ( + "aws/codebuild/amazonlinux2023-x86_64-standard:5.0" + ), "Type": "String", "Description": "Name of codebuild image to use." } diff --git a/docs/source/topics/cd.rst b/docs/source/topics/cd.rst index 67b58bea0..af1206252 100644 --- a/docs/source/topics/cd.rst +++ b/docs/source/topics/cd.rst @@ -42,7 +42,7 @@ version, ``v2``, is recommended. The version can be specified using the * The ``v2`` buildspec uses `runtime-versions `__ to configure which version of Python to use instead of a Python version specific CodeBuild image. For ``v2`` templates the - ``aws/codebuild/amazonlinux2-x86_64-standard`` image. + ``aws/codebuild/amazonlinux2023-x86_64-standard`` image. **The v2 pipeline template requires Python 3.10 or higher.** If you're using Python versions less than 3.10 you must use the ``v1`` pipeline template. diff --git a/docs/source/topics/pyversion.rst b/docs/source/topics/pyversion.rst index 5cc1f5b1f..d24217429 100644 --- a/docs/source/topics/pyversion.rst +++ b/docs/source/topics/pyversion.rst @@ -1,7 +1,7 @@ Python Version Support ====================== -Chalice supports Python 3.10 through Python 3.13. You can see the list of +Chalice supports Python 3.10 through Python 3.14. You can see the list of supported python versions for Lambda in their `docs `__. @@ -35,8 +35,8 @@ explicitly configure which version of python you want to use. For example:: In the example above, we're using python 3.10.20 so chalice automatically -selects the ``python3.10`` runtime for lambda. If we were using python 3.13, -chalice would automatically select ``python3.13`` as the runtime. +selects the ``python3.10`` runtime for lambda. If we were using python 3.14, +chalice would automatically select ``python3.14`` as the runtime. Chalice will emit a warning if the minor version does not match a python version supported by Lambda. Chalice will select the closest Lambda version @@ -61,7 +61,7 @@ Python 3.10 :: ... https://endpoint/api -To upgrade the application to use Python 3.13, create a python3 virtual +To upgrade the application to use Python 3.14, create a python3 virtual environment and redeploy. :: @@ -70,6 +70,6 @@ environment and redeploy. $ python3 -m venv /tmp/venv3 $ source /tmp/venv3/bin/activate $ python --version - Python 3.13.3 + Python 3.14.4 $ chalice deploy ... diff --git a/setup.py b/setup.py index db1d95af7..bf8a04740 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def recursive_include(relative_dir): 'click>=7,<9.0', 'botocore>=1.14.0,<2.0.0', 'six>=1.10.0,<2.0.0', - 'pip>=9,<25.1', + 'pip>=9,<26.2', 'jmespath>=0.9.3,<2.0.0', 'pyyaml>=5.3.1,<7.0.0', 'inquirer>=3.0.0,<4.0.0', @@ -73,5 +73,6 @@ def recursive_include(relative_dir): 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', ], ) diff --git a/tests/functional/cli/test_cli.py b/tests/functional/cli/test_cli.py index ca098b6e2..51703b760 100644 --- a/tests/functional/cli/test_cli.py +++ b/tests/functional/cli/test_cli.py @@ -410,8 +410,8 @@ def test_error_when_no_deployed_record(runner, mock_cli_factory): @pytest.mark.skipif( - (3, 10) <= sys.version_info[:2] <= (3, 13), - reason=("Cannot generate pipeline for python3.10 - python3.13"), + (3, 10) <= sys.version_info[:2] <= (3, 14), + reason=("Cannot generate pipeline for python3.10 - python3.14"), ) def test_can_generate_pipeline_for_all(runner): with runner.isolated_filesystem(): diff --git a/tests/functional/test_package.py b/tests/functional/test_package.py index afdc28241..d6e12c12a 100644 --- a/tests/functional/test_package.py +++ b/tests/functional/test_package.py @@ -271,6 +271,7 @@ def test_can_get_sdist_if_missing_initially(self, tmpdir, pip_runner): pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', + 'manylinux_2_17_x86_64', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', 'cp36m', '--dest', mock.ANY, 'foo==1.2' @@ -743,6 +744,7 @@ def test_can_replace_incompat_whl(self, tmpdir, osutils, pip_runner): pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', + 'manylinux_2_17_x86_64', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', 'cp36m', '--dest', mock.ANY, 'bar==1.2' @@ -785,6 +787,7 @@ def test_whitelist_sqlalchemy(self, tmpdir, osutils, pip_runner, pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', + 'manylinux_2_17_x86_64', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', abi, '--dest', mock.ANY, '%s==1.1.18' % package @@ -942,6 +945,7 @@ def test_build_into_existing_dir_with_preinstalled_packages( pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', + 'manylinux_2_17_x86_64', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', abi, '--dest', mock.ANY, 'foo==1.2' diff --git a/tests/integration/test_package.py b/tests/integration/test_package.py index d4fc49ae6..8f4c59266 100644 --- a/tests/integration/test_package.py +++ b/tests/integration/test_package.py @@ -17,25 +17,19 @@ PY_VERSION = sys.version_info[:2] -LEGACY_VERSION_CUTOFF = (3, 9) -# Pin numpy for packages that depend on it so these smoke tests do not -# drift to a newer wheel that pip can install locally but Chalice cannot -# package for the target Lambda runtime. -NUMPY_VERSION = '2.2.6' -# We're being cautious here, but we want to fix the package versions we -# try to install on older versions of python. -# If the python version being tested is less than or equal to -# LEGACY_VERSION_CUTOFF, -# then we'll install the `legacy_version` in the packages below. This is to -# ensure we don't regress on being able to package older package versions on -# older versions on python. Any python version above LEGACY_VERSION_CUTOFF -# will install the `version` identifier. That way newer versions of python -# won't need to update this list as long as a package can still be installed -# on versions greater than LEGACY_VERSION_CUTOFF. +PY314_OR_LATER = PY_VERSION >= (3, 14) +# Keep these smoke-test pins compatible with every Python version in the +# current CI matrix. Some projects dropped Python 3.10 support before adding +# cp314 wheels, so those packages need Python-version-specific pins. +NUMPY_VERSION = '2.3.4' if PY314_OR_LATER else '2.2.6' +PANDAS_VERSION = '2.3.3' +SQLALCHEMY_VERSION = '2.0.49' +SCIPY_VERSION = '1.17.1' if PY314_OR_LATER else '1.15.3' +CFFI_VERSION = '2.0.0' +PYGIT2_VERSION = '1.19.2' if PY314_OR_LATER else '1.17.0' PACKAGES_TO_TEST = { 'pandas': { - 'version': '2.2.3', - 'legacy_version': '1.5.3', + 'version': PANDAS_VERSION, 'dependencies': ['numpy==%s' % NUMPY_VERSION], 'contents': [ 'pandas/*__init__.py', @@ -43,8 +37,7 @@ ], }, 'SQLAlchemy': { - 'version': '2.0.40', - 'legacy_version': '1.4.47', + 'version': SQLALCHEMY_VERSION, 'contents': [ 'sqlalchemy/__init__.py', 'sqlalchemy/*cpython-*-x86_64-linux-gnu.so' @@ -52,7 +45,6 @@ }, 'numpy': { 'version': NUMPY_VERSION, - 'legacy_version': '1.23.3', 'contents': [ 'numpy/__init__.py', 'numpy/*cpython-*-x86_64-linux-gnu.so' @@ -60,7 +52,6 @@ }, 'cryptography': { 'version': '44.0.3', - 'legacy_version': '39.0.0', 'contents': [ 'cryptography/__init__.py', 'cryptography/*.so' @@ -68,22 +59,18 @@ }, 'Jinja2': { 'version': '3.1.6', - 'legacy_version': '2.11.2', 'contents': ['jinja2/__init__.py'], }, 'Mako': { 'version': '1.3.10', - 'legacy_version': '1.1.3', 'contents': ['mako/__init__.py'], }, 'MarkupSafe': { 'version': '3.0.2', - 'legacy_version': '1.1.1', 'contents': ['markupsafe/__init__.py'], }, 'scipy': { - 'version': '1.15.3', - 'legacy_version': '1.10.1', + 'version': SCIPY_VERSION, 'dependencies': ['numpy==%s' % NUMPY_VERSION], 'contents': [ 'scipy/__init__.py', @@ -91,18 +78,15 @@ ], }, 'cffi': { - 'version': '1.17.1', - 'legacy_version': '1.15.1', + 'version': CFFI_VERSION, 'contents': ['_cffi_backend.cpython-*-x86_64-linux-gnu.so'], }, 'pygit2': { - 'version': '1.17.0', - 'legacy_version': '1.10.1', + 'version': PYGIT2_VERSION, 'contents': ['pygit2/_pygit2.cpython-*-x86_64-linux-gnu.so'], }, 'pyrsistent': { 'version': '0.20.0', - 'legacy_version': '0.17.3', 'contents': ['pyrsistent/__init__.py'], }, } @@ -137,12 +121,8 @@ def _get_random_package_name(): def _get_package_install_test_cases(): testcases = [] - if PY_VERSION <= LEGACY_VERSION_CUTOFF: - version_key = 'legacy_version' - else: - version_key = 'version' for package, config in PACKAGES_TO_TEST.items(): - package_version = f'{package}=={config[version_key]}' + package_version = f'{package}=={config["version"]}' requirements = [package_version] + config.get('dependencies', []) testcases.append( pytest.param(requirements, config['contents'], id=package_version) @@ -228,11 +208,10 @@ def test_can_package_sqlalchemy(self, runner, app_skeleton, ) def test_can_package_pandas(self, runner, app_skeleton, no_local_config): - version = '2.2.3' if sys.version_info[1] >= 10 else '2.0.3' assert_can_package_dependency( runner, app_skeleton, - ['pandas==' + version, 'numpy==%s' % NUMPY_VERSION], + ['pandas==' + PANDAS_VERSION, 'numpy==%s' % NUMPY_VERSION], contents=[ 'pandas/_libs/__init__.py', ], diff --git a/tests/unit/deploy/test_packager.py b/tests/unit/deploy/test_packager.py index 3b64d6f6c..b1765a356 100644 --- a/tests/unit/deploy/test_packager.py +++ b/tests/unit/deploy/test_packager.py @@ -5,6 +5,8 @@ from chalice.compat import pip_no_compile_c_env_vars from chalice.compat import pip_no_compile_c_shim from chalice.deploy.packager import Package +from chalice.deploy.packager import BaseLambdaDeploymentPackager +from chalice.deploy.packager import DependencyBuilder from chalice.deploy.packager import PipRunner from chalice.deploy.packager import SubprocessPip from chalice.deploy.packager import InvalidSourceDistributionNameError @@ -57,6 +59,63 @@ def osutils(): return OSUtils() +def test_python314_runtime_maps_to_cp314(): + runtime_to_abi = BaseLambdaDeploymentPackager._RUNTIME_TO_ABI + assert runtime_to_abi['python3.14'] == 'cp314' + + +def test_cp314_wheels_use_al2023_glibc(osutils): + builder = DependencyBuilder(osutils) + assert builder._is_compatible_wheel_filename( + 'cp314', 'foo-1.0-cp314-cp314-manylinux_2_34_x86_64.whl' + ) + assert builder._is_compatible_wheel_filename( + 'cp314', 'foo-1.0-cp39-abi3-manylinux_2_34_x86_64.whl' + ) + assert not builder._is_compatible_wheel_filename( + 'cp314', 'foo-1.0-cp314-cp314-manylinux_2_35_x86_64.whl' + ) + + +def test_pip_platforms_enumerate_glibc_range(osutils): + builder = DependencyBuilder(osutils) + assert builder._get_pip_platforms('cp314') == [ + 'manylinux_2_17_x86_64', + 'manylinux_2_18_x86_64', + 'manylinux_2_19_x86_64', + 'manylinux_2_20_x86_64', + 'manylinux_2_21_x86_64', + 'manylinux_2_22_x86_64', + 'manylinux_2_23_x86_64', + 'manylinux_2_24_x86_64', + 'manylinux_2_25_x86_64', + 'manylinux_2_26_x86_64', + 'manylinux_2_27_x86_64', + 'manylinux_2_28_x86_64', + 'manylinux_2_29_x86_64', + 'manylinux_2_30_x86_64', + 'manylinux_2_31_x86_64', + 'manylinux_2_32_x86_64', + 'manylinux_2_33_x86_64', + 'manylinux_2_34_x86_64', + 'manylinux2014_x86_64', + ] + # Older runtimes on AL2 stop enumeration at glibc 2.26. + assert builder._get_pip_platforms('cp310') == [ + 'manylinux_2_17_x86_64', + 'manylinux_2_18_x86_64', + 'manylinux_2_19_x86_64', + 'manylinux_2_20_x86_64', + 'manylinux_2_21_x86_64', + 'manylinux_2_22_x86_64', + 'manylinux_2_23_x86_64', + 'manylinux_2_24_x86_64', + 'manylinux_2_25_x86_64', + 'manylinux_2_26_x86_64', + 'manylinux2014_x86_64', + ] + + class FakePopen(object): def __init__(self, rc, out, err): self.returncode = 0 @@ -226,6 +285,23 @@ def test_download_wheels(self, pip_factory): assert pip.calls[i].env_vars is None assert pip.calls[i].shim is None + def test_download_wheels_uses_given_platforms(self, pip_factory): + pip, runner = pip_factory() + runner.download_manylinux_wheels( + 'cp314', + ['foo'], + 'directory', + ['manylinux2014_x86_64', 'manylinux_2_28_x86_64'], + ) + + assert pip.calls[0].args == [ + 'download', '--only-binary=:all:', '--no-deps', + '--platform', 'manylinux2014_x86_64', + '--platform', 'manylinux_2_28_x86_64', + '--implementation', 'cp', '--abi', 'cp314', + '--dest', 'directory', 'foo', + ] + def test_download_wheels_no_wheels(self, pip_factory): pip, runner = pip_factory() runner.download_manylinux_wheels('cp36m', [], 'directory') diff --git a/tests/unit/test_pipeline.py b/tests/unit/test_pipeline.py index a1f33cec3..3b403682a 100644 --- a/tests/unit/test_pipeline.py +++ b/tests/unit/test_pipeline.py @@ -112,7 +112,7 @@ def generate_template(self, app_name='appname', def test_new_default_codebuild_image(self): template = self.generate_template(app_name='app') assert template['Parameters']['CodeBuildImage']['Default'] == ( - "aws/codebuild/amazonlinux2-x86_64-standard:3.0" + "aws/codebuild/amazonlinux2023-x86_64-standard:5.0" ) def test_validate_python_versions(self):