diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ee3d60d..67e7d1e01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Added support for Python 3.13. ([#1661](https://github.com/heroku/heroku-buildpack-python/pull/1661)) +- Removed the `idle3` and `pydoc3` scripts since they do not work with relocated Python and so have been broken for some time. Invoke them via their modules instead (e.g. `python -m pydoc`). ([#1661](https://github.com/heroku/heroku-buildpack-python/pull/1661)) ## [v259] - 2024-10-09 diff --git a/README.md b/README.md index 3c78ac632..f590addf9 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Specify a Python Runtime Supported runtime options include: +- `python-3.13.0` on all [supported stacks](https://devcenter.heroku.com/articles/stack#stack-support-details) - `python-3.12.7` on all [supported stacks](https://devcenter.heroku.com/articles/stack#stack-support-details) - `python-3.11.10` on all [supported stacks](https://devcenter.heroku.com/articles/stack#stack-support-details) - `python-3.10.15` on all [supported stacks](https://devcenter.heroku.com/articles/stack#stack-support-details) diff --git a/bin/compile b/bin/compile index 12c74f3b6..bc6621c15 100755 --- a/bin/compile +++ b/bin/compile @@ -201,11 +201,11 @@ package_manager_install_start_time=$(nowms) bundled_pip_module_path="$(utils::bundled_pip_module_path "${BUILD_DIR}")" case "${package_manager}" in pip) - pip::install_pip_setuptools_wheel "${bundled_pip_module_path}" + pip::install_pip_setuptools_wheel "${bundled_pip_module_path}" "${python_major_version}" ;; pipenv) # TODO: Stop installing pip when using Pipenv. - pip::install_pip_setuptools_wheel "${bundled_pip_module_path}" + pip::install_pip_setuptools_wheel "${bundled_pip_module_path}" "${python_major_version}" pipenv::install_pipenv ;; *) @@ -217,10 +217,13 @@ meta_time "package_manager_install_duration" "${package_manager_install_start_ti # SQLite3 support. # Installs the sqlite3 dev headers and sqlite3 binary but not the # libsqlite3-0 library since that exists in the base image. -install_sqlite_start_time=$(nowms) -source "${BUILDPACK_DIR}/bin/steps/sqlite3" -buildpack_sqlite3_install -meta_time "sqlite_install_duration" "${install_sqlite_start_time}" +# We skip this step on Python 3.13, as a first step towards removing this feature. +if [[ "${python_major_version}" == +(3.8|3.9|3.10|3.11|3.12) ]]; then + install_sqlite_start_time=$(nowms) + source "${BUILDPACK_DIR}/bin/steps/sqlite3" + buildpack_sqlite3_install + meta_time "sqlite_install_duration" "${install_sqlite_start_time}" +fi # Install app dependencies. dependencies_install_start_time=$(nowms) diff --git a/builds/Dockerfile b/builds/Dockerfile index f3fcfffb7..cc16e9b25 100644 --- a/builds/Dockerfile +++ b/builds/Dockerfile @@ -15,4 +15,4 @@ RUN apt-get update --error-on=any \ && rm -rf /var/lib/apt/lists/* WORKDIR /tmp -COPY build_python_runtime.sh . +COPY build_python_runtime.sh python-3.13-ubuntu-22.04-libexpat-workaround.patch . diff --git a/builds/build_python_runtime.sh b/builds/build_python_runtime.sh index ea5c0eb29..3da58a09c 100755 --- a/builds/build_python_runtime.sh +++ b/builds/build_python_runtime.sh @@ -27,6 +27,7 @@ case "${STACK:?}" in "3.10" "3.11" "3.12" + "3.13" ) ;; heroku-20) @@ -36,6 +37,7 @@ case "${STACK:?}" in "3.10" "3.11" "3.12" + "3.13" ) ;; *) @@ -49,6 +51,10 @@ fi # The release keys can be found on https://www.python.org/downloads/ -> "OpenPGP Public Keys". case "${PYTHON_MAJOR_VERSION}" in + 3.13) + # https://github.com/Yhg1s.gpg + GPG_KEY_FINGERPRINT='7169605F62C751356D054A26A821E680E5FA6305' + ;; 3.12) # https://github.com/Yhg1s.gpg GPG_KEY_FINGERPRINT='7169605F62C751356D054A26A821E680E5FA6305' @@ -84,6 +90,14 @@ gpg --batch --verify python.tgz.asc python.tgz tar --extract --file python.tgz --strip-components=1 --directory "${SRC_DIR}" cd "${SRC_DIR}" +# Work around PGO profile test failures with Python 3.13 on Ubuntu 22.04, due to the tests +# checking the raw libexpat version which doesn't account for Ubuntu backports: +# https://github.com/heroku/heroku-buildpack-python/pull/1661#issuecomment-2405259352 +# https://github.com/python/cpython/issues/125067 +if [[ "${PYTHON_MAJOR_VERSION}" == "3.13" && "${STACK}" == "heroku-22" ]]; then + patch -p1 12) || major >= 4)); then + if (((major == 3 && minor > 13) || major >= 4)); then if [[ "${python_version_origin}" == "cached" ]]; then display_error <<-EOF Error: The cached Python version is not recognised. @@ -281,6 +282,7 @@ function python_version::resolve_python_version() { 3.10) echo "${LATEST_PYTHON_3_10}" ;; 3.11) echo "${LATEST_PYTHON_3_11}" ;; 3.12) echo "${LATEST_PYTHON_3_12}" ;; + 3.13) echo "${LATEST_PYTHON_3_13}" ;; *) utils::abort_internal_error "Unhandled Python major version: ${requested_python_version}" ;; esac } diff --git a/spec/fixtures/pipenv_python_3.13/Pipfile b/spec/fixtures/pipenv_python_3.13/Pipfile new file mode 100644 index 000000000..73f50fc86 --- /dev/null +++ b/spec/fixtures/pipenv_python_3.13/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +urllib3 = "*" + +[dev-packages] + +[requires] +python_version = "3.13" diff --git a/spec/fixtures/pipenv_python_3.13/Pipfile.lock b/spec/fixtures/pipenv_python_3.13/Pipfile.lock new file mode 100644 index 000000000..dcd0b3ca7 --- /dev/null +++ b/spec/fixtures/pipenv_python_3.13/Pipfile.lock @@ -0,0 +1,30 @@ +{ + "_meta": { + "hash": { + "sha256": "60dc67ee4223a391c5e0ae2b4d7ea54a7b245773d76b6ff82156dda97a3e4fb2" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.2.3" + } + }, + "develop": {} +} diff --git a/spec/fixtures/python_3.13/requirements.txt b/spec/fixtures/python_3.13/requirements.txt new file mode 100644 index 000000000..a42590beb --- /dev/null +++ b/spec/fixtures/python_3.13/requirements.txt @@ -0,0 +1 @@ +urllib3 diff --git a/spec/fixtures/python_3.13/runtime.txt b/spec/fixtures/python_3.13/runtime.txt new file mode 100644 index 000000000..dae7fec92 --- /dev/null +++ b/spec/fixtures/python_3.13/runtime.txt @@ -0,0 +1 @@ +python-3.13.0 diff --git a/spec/hatchet/pipenv_spec.rb b/spec/hatchet/pipenv_spec.rb index 942bdb808..91d4fbb1a 100644 --- a/spec/hatchet/pipenv_spec.rb +++ b/spec/hatchet/pipenv_spec.rb @@ -208,6 +208,24 @@ include_examples 'builds using Pipenv with the requested Python version', '3.12', LATEST_PYTHON_3_12 end + context 'with a Pipfile.lock containing python_version 3.13' do + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.13') } + + it 'builds with latest Python 3.13' do + app.deploy do |app| + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Python app detected + remote: -----> Using Python 3.13 specified in Pipfile.lock + remote: -----> Installing Python #{LATEST_PYTHON_3_13} + remote: -----> Installing pip #{PIP_VERSION} + remote: -----> Installing Pipenv #{PIPENV_VERSION} + remote: -----> Installing dependencies with Pipenv + remote: Installing dependencies from Pipfile.lock \\(.+\\)... + REGEX + end + end + end + # As well as testing `python_full_version`, this also tests: # 1. That `python_full_version` takes precedence over `python_version`. # 2. That Pipenv works on the oldest Python version supported by all stacks. diff --git a/spec/hatchet/python_version_spec.rb b/spec/hatchet/python_version_spec.rb index e0d01049d..e9751a920 100644 --- a/spec/hatchet/python_version_spec.rb +++ b/spec/hatchet/python_version_spec.rb @@ -5,15 +5,26 @@ RSpec.shared_examples 'builds with the requested Python version' do |requested_version| it "builds with Python #{requested_version}" do app.deploy do |app| - expect(clean_output(app.output)).to include(<<~OUTPUT) - remote: -----> Python app detected - remote: -----> Using Python #{requested_version} specified in runtime.txt - remote: -----> Installing Python #{requested_version} - remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - remote: -----> Installing SQLite3 - remote: -----> Installing requirements with pip - remote: Collecting urllib3 (from -r requirements.txt (line 1)) - OUTPUT + if requested_version.start_with?('3.13.') + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: -----> Using Python #{requested_version} specified in runtime.txt + remote: -----> Installing Python #{requested_version} + remote: -----> Installing pip #{PIP_VERSION} + remote: -----> Installing requirements with pip + remote: Collecting urllib3 (from -r requirements.txt (line 1)) + OUTPUT + else + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: -----> Using Python #{requested_version} specified in runtime.txt + remote: -----> Installing Python #{requested_version} + remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + remote: -----> Installing SQLite3 + remote: -----> Installing requirements with pip + remote: Collecting urllib3 (from -r requirements.txt (line 1)) + OUTPUT + end expect(app.run('python -V')).to include("Python #{requested_version}") end end @@ -200,6 +211,12 @@ include_examples 'builds with the requested Python version', LATEST_PYTHON_3_12 end + context 'when runtime.txt contains python-3.13.0' do + let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.13') } + + include_examples 'builds with the requested Python version', LATEST_PYTHON_3_13 + end + context 'when runtime.txt contains an invalid Python version string' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_invalid', allow_failure: true) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3c0632a64..4fddb3764 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ LATEST_PYTHON_3_10 = '3.10.15' LATEST_PYTHON_3_11 = '3.11.10' LATEST_PYTHON_3_12 = '3.12.7' +LATEST_PYTHON_3_13 = '3.13.0' DEFAULT_PYTHON_FULL_VERSION = LATEST_PYTHON_3_12 DEFAULT_PYTHON_MAJOR_VERSION = DEFAULT_PYTHON_FULL_VERSION.gsub(/\.\d+$/, '')