From ed1097088e85cef8e3abf76cebf29d54a29152e2 Mon Sep 17 00:00:00 2001 From: Ed Morley <501702+edmorley@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:50:59 +0000 Subject: [PATCH] Remove Python 3.8 support (#313) Python 3.8 reached upstream EOL on 7th October 2024: https://devguide.python.org/versions/#supported-versions The Python version support policy is that supported versions follows the upstream EOL lifecycle: https://devcenter.heroku.com/articles/python-support#python-version-support-policy And Python 3.8 support has been deprecated since December 2023: https://devcenter.heroku.com/changelog-items/2768 (Plus Python 3.8 binaries have never been available for Heroku-22 or Heroku-24, meaning it was only ever supported on Heroku-20 and older.) Dropping support for Python 3.8 also unblocks upgrading to Poetry v2 (which has already dropped 3.8 support). Apps using Python 3.8 that aren't able to upgrade immediately will need to pin to an older buildpack version temporarily (which will work until Heroku-20 EOLs). GUS-W-17472312. --- CHANGELOG.md | 5 + src/errors.rs | 39 +++-- src/python_version.rs | 30 ++-- tests/fixtures/python_3.7/.python-version | 1 - .../.python-version | 0 .../requirements.txt | 0 .../.python-version | 0 .../requirements.txt | 0 .../.python-version | 1 + .../requirements.txt | 0 tests/python_version_test.rs | 138 +++++++++--------- 11 files changed, 111 insertions(+), 103 deletions(-) delete mode 100644 tests/fixtures/python_3.7/.python-version rename tests/fixtures/{python_3.8 => python_version_eol}/.python-version (100%) rename tests/fixtures/{python_3.7 => python_version_eol}/requirements.txt (100%) rename tests/fixtures/{python_version_file_unknown_version => python_version_non_existent_major}/.python-version (100%) rename tests/fixtures/{python_3.8 => python_version_non_existent_major}/requirements.txt (100%) create mode 100644 tests/fixtures/python_version_non_existent_minor/.python-version rename tests/fixtures/{python_version_file_unknown_version => python_version_non_existent_minor}/requirements.txt (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec13065..f811df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- Removed support for Python 3.8. ([#313](https://github.com/heroku/buildpacks-python/pull/313)) + ### Changed - Buildpack detection now recognises more Python-related file and directory names. ([#312](https://github.com/heroku/buildpacks-python/pull/312)) +- Improved the error messages shown for EOL or unrecognised major Python versions. ([#313](https://github.com/heroku/buildpacks-python/pull/313)) ## [0.21.0] - 2024-12-18 diff --git a/src/errors.rs b/src/errors.rs index 7855e75..bcc57ae 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,7 +8,8 @@ use crate::layers::python::PythonLayerError; use crate::package_manager::DeterminePackageManagerError; use crate::python_version::{ RequestedPythonVersion, RequestedPythonVersionError, ResolvePythonVersionError, - DEFAULT_PYTHON_FULL_VERSION, DEFAULT_PYTHON_VERSION, + DEFAULT_PYTHON_FULL_VERSION, DEFAULT_PYTHON_VERSION, NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION, + OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION, }; use crate::python_version_file::ParsePythonVersionFileError; use crate::runtime_txt::ParseRuntimeTxtError; @@ -231,16 +232,17 @@ fn on_resolve_python_version_error(error: ResolvePythonVersionError) { .. } = requested_python_version; log_error( - "Requested Python version has reached end-of-life", + "The requested Python version has reached end-of-life", formatdoc! {" - The requested Python version {major}.{minor} has reached its upstream end-of-life, - and is therefore no longer receiving security updates: + Python {major}.{minor} has reached its upstream end-of-life, and is + therefore no longer receiving security updates: https://devguide.python.org/versions/#supported-versions - As such, it is no longer supported by this buildpack. + As such, it's no longer supported by this buildpack: + https://devcenter.heroku.com/articles/python-support#supported-python-versions - Please upgrade to a newer Python version by updating the version - configured via the {origin} file. + Please upgrade to at least Python 3.{OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION} by changing the + version in your {origin} file. If possible, we recommend upgrading all the way to Python {DEFAULT_PYTHON_VERSION}, since it contains many performance and usability improvements. @@ -255,17 +257,21 @@ fn on_resolve_python_version_error(error: ResolvePythonVersionError) { .. } = requested_python_version; log_error( - "Requested Python version is not recognised", + "The requested Python version isn't recognised", formatdoc! {" - The requested Python version {major}.{minor} is not recognised. + The requested Python version {major}.{minor} isn't recognised. - Check that this Python version has been officially released: + Check that this Python version has been officially released, + and that the Python buildpack has added support for it: https://devguide.python.org/versions/#supported-versions + https://devcenter.heroku.com/articles/python-support#supported-python-versions - If it has, make sure that you are using the latest version of this buildpack. + If it has, make sure that you are using the latest version + of this buildpack, and haven't pinned to an older release + via a custom buildpack configuration in project.toml. - If it has not, please switch to a supported version (such as Python {DEFAULT_PYTHON_VERSION}) - by updating the version configured via the {origin} file. + Otherwise, switch to a supported version (such as Python 3.{NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION}) + by changing the version in your {origin} file. "}, ); } @@ -294,11 +300,12 @@ fn on_python_layer_error(error: PythonLayerError) { }, // This error will change once the Python version is validated against a manifest. // TODO: (W-12613425) Write the supported Python versions inline, instead of linking out to Dev Center. - // TODO: Decide how to explain to users how stacks, base images and builder images versions relate to each other. + // TODO: Update this error message to suggest switching to the major version syntax in .python-version, + // which will prevent the error from ever occurring (now that all stacks support the same versions). PythonLayerError::PythonArchiveNotFound { python_version } => log_error( - "Requested Python version is not available", + "The requested Python version wasn't found", formatdoc! {" - The requested Python version ({python_version}) is not available for this builder image. + The requested Python version ({python_version}) wasn't found. Please switch to a supported Python version, or else don't specify a version and the buildpack will use a default version (currently Python {DEFAULT_PYTHON_VERSION}). diff --git a/src/python_version.rs b/src/python_version.rs index 9c6f94d..f11c25d 100644 --- a/src/python_version.rs +++ b/src/python_version.rs @@ -15,7 +15,11 @@ pub(crate) const DEFAULT_PYTHON_VERSION: RequestedPythonVersion = RequestedPytho }; pub(crate) const DEFAULT_PYTHON_FULL_VERSION: PythonVersion = LATEST_PYTHON_3_13; -pub(crate) const LATEST_PYTHON_3_8: PythonVersion = PythonVersion::new(3, 8, 20); +pub(crate) const OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION: u16 = 9; +pub(crate) const NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION: u16 = 13; +pub(crate) const NEXT_UNRELEASED_PYTHON_3_MINOR_VERSION: u16 = + NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION + 1; + pub(crate) const LATEST_PYTHON_3_9: PythonVersion = PythonVersion::new(3, 9, 21); pub(crate) const LATEST_PYTHON_3_10: PythonVersion = PythonVersion::new(3, 10, 16); pub(crate) const LATEST_PYTHON_3_11: PythonVersion = PythonVersion::new(3, 11, 11); @@ -156,18 +160,17 @@ pub(crate) fn resolve_python_version( } = requested_python_version; match (major, minor, patch) { - (..3, _, _) | (3, ..8, _) => Err(ResolvePythonVersionError::EolVersion( - requested_python_version.clone(), - )), - (3, 8, None) => Ok(LATEST_PYTHON_3_8), + (..3, _, _) | (3, ..OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION, _) => Err( + ResolvePythonVersionError::EolVersion(requested_python_version.clone()), + ), + (3, NEXT_UNRELEASED_PYTHON_3_MINOR_VERSION.., _) | (4.., _, _) => Err( + ResolvePythonVersionError::UnknownVersion(requested_python_version.clone()), + ), (3, 9, None) => Ok(LATEST_PYTHON_3_9), (3, 10, None) => Ok(LATEST_PYTHON_3_10), (3, 11, None) => Ok(LATEST_PYTHON_3_11), (3, 12, None) => Ok(LATEST_PYTHON_3_12), (3, 13, None) => Ok(LATEST_PYTHON_3_13), - (3, 14.., _) | (4.., _, _) => Err(ResolvePythonVersionError::UnknownVersion( - requested_python_version.clone(), - )), (major, minor, Some(patch)) => Ok(PythonVersion::new(major, minor, patch)), } } @@ -183,9 +186,6 @@ pub(crate) enum ResolvePythonVersionError { mod tests { use super::*; - const OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION: u16 = 8; - const NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION: u16 = 13; - #[test] fn python_version_url() { assert_eq!( @@ -239,10 +239,10 @@ mod tests { #[test] fn read_requested_python_version_python_version_file() { assert_eq!( - read_requested_python_version(Path::new("tests/fixtures/python_3.7")).unwrap(), + read_requested_python_version(Path::new("tests/fixtures/python_3.9")).unwrap(), RequestedPythonVersion { major: 3, - minor: 7, + minor: 9, patch: None, origin: PythonVersionOrigin::PythonVersionFile, } @@ -357,7 +357,7 @@ mod tests { fn resolve_python_version_unsupported() { let requested_python_version = RequestedPythonVersion { major: 3, - minor: NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION + 1, + minor: NEXT_UNRELEASED_PYTHON_3_MINOR_VERSION, patch: None, origin: PythonVersionOrigin::PythonVersionFile, }; @@ -370,7 +370,7 @@ mod tests { let requested_python_version = RequestedPythonVersion { major: 3, - minor: NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION + 1, + minor: NEXT_UNRELEASED_PYTHON_3_MINOR_VERSION, patch: Some(0), origin: PythonVersionOrigin::PythonVersionFile, }; diff --git a/tests/fixtures/python_3.7/.python-version b/tests/fixtures/python_3.7/.python-version deleted file mode 100644 index 475ba51..0000000 --- a/tests/fixtures/python_3.7/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.7 diff --git a/tests/fixtures/python_3.8/.python-version b/tests/fixtures/python_version_eol/.python-version similarity index 100% rename from tests/fixtures/python_3.8/.python-version rename to tests/fixtures/python_version_eol/.python-version diff --git a/tests/fixtures/python_3.7/requirements.txt b/tests/fixtures/python_version_eol/requirements.txt similarity index 100% rename from tests/fixtures/python_3.7/requirements.txt rename to tests/fixtures/python_version_eol/requirements.txt diff --git a/tests/fixtures/python_version_file_unknown_version/.python-version b/tests/fixtures/python_version_non_existent_major/.python-version similarity index 100% rename from tests/fixtures/python_version_file_unknown_version/.python-version rename to tests/fixtures/python_version_non_existent_major/.python-version diff --git a/tests/fixtures/python_3.8/requirements.txt b/tests/fixtures/python_version_non_existent_major/requirements.txt similarity index 100% rename from tests/fixtures/python_3.8/requirements.txt rename to tests/fixtures/python_version_non_existent_major/requirements.txt diff --git a/tests/fixtures/python_version_non_existent_minor/.python-version b/tests/fixtures/python_version_non_existent_minor/.python-version new file mode 100644 index 0000000..bb40ea0 --- /dev/null +++ b/tests/fixtures/python_version_non_existent_minor/.python-version @@ -0,0 +1 @@ +3.12.999 diff --git a/tests/fixtures/python_version_file_unknown_version/requirements.txt b/tests/fixtures/python_version_non_existent_minor/requirements.txt similarity index 100% rename from tests/fixtures/python_version_file_unknown_version/requirements.txt rename to tests/fixtures/python_version_non_existent_minor/requirements.txt diff --git a/tests/python_version_test.rs b/tests/python_version_test.rs index eb7da6b..e366237 100644 --- a/tests/python_version_test.rs +++ b/tests/python_version_test.rs @@ -1,9 +1,9 @@ use crate::python_version::{ PythonVersion, DEFAULT_PYTHON_FULL_VERSION, DEFAULT_PYTHON_VERSION, LATEST_PYTHON_3_10, - LATEST_PYTHON_3_11, LATEST_PYTHON_3_12, LATEST_PYTHON_3_13, LATEST_PYTHON_3_8, - LATEST_PYTHON_3_9, + LATEST_PYTHON_3_11, LATEST_PYTHON_3_12, LATEST_PYTHON_3_13, LATEST_PYTHON_3_9, + NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION, }; -use crate::tests::{builder, default_build_config}; +use crate::tests::default_build_config; use indoc::{formatdoc, indoc}; use libcnb_test::{assert_contains, assert_empty, PackResult, TestRunner}; @@ -29,44 +29,6 @@ fn python_version_unspecified() { }); } -#[test] -#[ignore = "integration test"] -fn python_3_7() { - let mut config = default_build_config("tests/fixtures/python_3.7"); - config.expected_pack_result(PackResult::Failure); - - TestRunner::default().build(config, |context| { - assert_contains!( - context.pack_stderr, - &formatdoc! {" - [Error: Requested Python version has reached end-of-life] - The requested Python version 3.7 has reached its upstream end-of-life, - and is therefore no longer receiving security updates: - https://devguide.python.org/versions/#supported-versions - - As such, it is no longer supported by this buildpack. - - Please upgrade to a newer Python version by updating the version - configured via the .python-version file. - - If possible, we recommend upgrading all the way to Python {DEFAULT_PYTHON_VERSION}, - since it contains many performance and usability improvements. - "} - ); - }); -} - -#[test] -#[ignore = "integration test"] -fn python_3_8() { - // Python 3.8 is only available on Heroku-20 and older. - let fixture = "tests/fixtures/python_3.8"; - match builder().as_str() { - "heroku/builder:20" => builds_with_python_version(fixture, &LATEST_PYTHON_3_8), - _ => rejects_non_existent_python_version(fixture, &LATEST_PYTHON_3_8), - }; -} - #[test] #[ignore = "integration test"] fn python_3_9() { @@ -154,27 +116,6 @@ fn builds_with_python_version(fixture_path: &str, python_version: &PythonVersion }); } -fn rejects_non_existent_python_version(fixture_path: &str, python_version: &PythonVersion) { - let mut config = default_build_config(fixture_path); - config.expected_pack_result(PackResult::Failure); - - TestRunner::default().build(config, |context| { - assert_contains!( - context.pack_stderr, - &formatdoc! {" - [Error: Requested Python version is not available] - The requested Python version ({python_version}) is not available for this builder image. - - Please switch to a supported Python version, or else don't specify a version - and the buildpack will use a default version (currently Python {DEFAULT_PYTHON_VERSION}). - - For a list of the supported Python versions, see: - https://devcenter.heroku.com/articles/python-support#supported-runtimes - "} - ); - }); -} - #[test] #[ignore = "integration test"] fn python_version_file_io_error() { @@ -275,24 +216,79 @@ fn python_version_file_no_version() { #[test] #[ignore = "integration test"] -fn python_version_file_unknown_version() { - let mut config = default_build_config("tests/fixtures/python_version_file_unknown_version"); +fn python_version_eol() { + let mut config = default_build_config("tests/fixtures/python_version_eol"); config.expected_pack_result(PackResult::Failure); TestRunner::default().build(config, |context| { assert_contains!( context.pack_stderr, &formatdoc! {" - [Error: Requested Python version is not recognised] - The requested Python version 3.99 is not recognised. - - Check that this Python version has been officially released: + [Error: The requested Python version has reached end-of-life] + Python 3.8 has reached its upstream end-of-life, and is + therefore no longer receiving security updates: + https://devguide.python.org/versions/#supported-versions + + As such, it's no longer supported by this buildpack: + https://devcenter.heroku.com/articles/python-support#supported-python-versions + + Please upgrade to at least Python 3.9 by changing the + version in your .python-version file. + + If possible, we recommend upgrading all the way to Python {DEFAULT_PYTHON_VERSION}, + since it contains many performance and usability improvements. + "} + ); + }); +} + +#[test] +#[ignore = "integration test"] +fn python_version_non_existent_major() { + let mut config = default_build_config("tests/fixtures/python_version_non_existent_major"); + config.expected_pack_result(PackResult::Failure); + + TestRunner::default().build(config, |context| { + assert_contains!( + context.pack_stderr, + &formatdoc! {" + [Error: The requested Python version isn't recognised] + The requested Python version 3.99 isn't recognised. + + Check that this Python version has been officially released, + and that the Python buildpack has added support for it: https://devguide.python.org/versions/#supported-versions + https://devcenter.heroku.com/articles/python-support#supported-python-versions + + If it has, make sure that you are using the latest version + of this buildpack, and haven't pinned to an older release + via a custom buildpack configuration in project.toml. + + Otherwise, switch to a supported version (such as Python 3.{NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION}) + by changing the version in your .python-version file. + "} + ); + }); +} + +#[test] +#[ignore = "integration test"] +fn python_version_non_existent_minor() { + let mut config = default_build_config("tests/fixtures/python_version_non_existent_minor"); + config.expected_pack_result(PackResult::Failure); + + TestRunner::default().build(config, |context| { + assert_contains!( + context.pack_stderr, + &formatdoc! {" + [Error: The requested Python version wasn't found] + The requested Python version (3.12.999) wasn't found. - If it has, make sure that you are using the latest version of this buildpack. + Please switch to a supported Python version, or else don't specify a version + and the buildpack will use a default version (currently Python {DEFAULT_PYTHON_VERSION}). - If it has not, please switch to a supported version (such as Python {DEFAULT_PYTHON_VERSION}) - by updating the version configured via the .python-version file. + For a list of the supported Python versions, see: + https://devcenter.heroku.com/articles/python-support#supported-runtimes "} ); });