diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a319a1b86..d754e02ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,4 +73,4 @@ jobs: - name: Run buildpack using default app fixture run: make run - name: Run buildpack using an app fixture that's expected to fail - run: make run FIXTURE=spec/fixtures/python_version_invalid/ + run: make run FIXTURE=spec/fixtures/python_version_file_invalid_version/ diff --git a/CHANGELOG.md b/CHANGELOG.md index aada1653b..7563bc94c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Added support for configuring the Python version using a `.python-version` file. Both the `3.N` and `3.N.N` version forms are supported (the former is recommended). The existing `runtime.txt` file will take precedence if both files are found, however, we recommend switching to `.python-version` since it is more commonly supported in the Python ecosystem. ([#1664](https://github.com/heroku/heroku-buildpack-python/pull/1664)) +- Added support for specifying only the Python major version in `runtime.txt` instead of requiring the full Python version (for example `python-3.N` instead of `python-3.N.N`). ([#1664](https://github.com/heroku/heroku-buildpack-python/pull/1664)) ## [v260] - 2024-10-10 diff --git a/README.md b/README.md index f590addf9..235dffcd4 100644 --- a/README.md +++ b/README.md @@ -8,61 +8,52 @@ This is the official [Heroku buildpack](https://devcenter.heroku.com/articles/bu Recommended web frameworks include **Django** and **Flask**, among others. The recommended webserver is **Gunicorn**. There are no restrictions around what software can be used (as long as it's pip-installable). Web processes must bind to `$PORT`, and only the HTTP protocol is permitted for incoming connections. -See it in Action ----------------- -``` -$ ls -my-application requirements.txt runtime.txt - -$ git push heroku main -Counting objects: 4, done. -Delta compression using up to 8 threads. -Compressing objects: 100% (2/2), done. -Writing objects: 100% (4/4), 276 bytes | 276.00 KiB/s, done. -Total 4 (delta 0), reused 0 (delta 0) -remote: Compressing source files... done. -remote: Building source: -remote: -remote: -----> Python app detected -remote: -----> Installing python -remote: -----> Installing pip -remote: -----> Installing SQLite3 -remote: -----> Installing requirements with pip -remote: Collecting flask (from -r /tmp/build_c2c067ef79ff14c9bf1aed6796f9ed1f/requirements.txt (line 1)) -remote: Downloading ... -remote: Installing collected packages: Werkzeug, click, MarkupSafe, Jinja2, itsdangerous, flask -remote: Successfully installed Jinja2-2.10 MarkupSafe-1.1.0 Werkzeug-0.14.1 click-7.0 flask-1.0.2 itsdangerous-1.1.0 -remote: -remote: -----> Discovering process types -remote: Procfile declares types -> (none) -remote: -``` - -A `requirements.txt` must be present at the root of your application's repository to deploy. - -To specify your python version, you also need a `runtime.txt` file - unless you are using the default Python runtime version. - -Current default Python Runtime: Python 3.12.7 - -Alternatively, you can provide a `setup.py` file, or a `Pipfile`. -Using `pipenv` will generate `runtime.txt` at build time if one of the field `python_version` or `python_full_version` is specified in the `requires` section of your `Pipfile`. - -Specify a Buildpack Version ---------------------------- - -You can specify the latest production release of this buildpack for upcoming builds of an existing application: - - $ heroku buildpacks:set heroku/python - - -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) -- `python-3.9.20` on all [supported stacks](https://devcenter.heroku.com/articles/stack#stack-support-details) -- `python-3.8.20` on Heroku-20 only +## Getting Started + +See the [Getting Started on Heroku with Python](https://devcenter.heroku.com/articles/getting-started-with-python) tutorial. + +## Application Requirements + +A `requirements.txt` or `Pipfile` file must be present in the root (top-level) directory of your app's source code. + +## Configuration + +### Python Version + +We recommend that you specify a Python version for your app rather than relying on the buildpack's default Python version. + +For example, to request the latest patch release of Python 3.13, create a `.python-version` file in +the root directory of your app containing: +`3.13` + +The buildpack will look for a Python version in the following places (in descending order of precedence): + +1. `runtime.txt` file (deprecated) +2. `.python-version` file (recommended) +3. The `python_full_version` field in the `Pipfile.lock` file +4. The `python_version` field in the `Pipfile.lock` file + +If none of those are found, the buildpack will use a default Python version for the first +build of an app, and then subsequent builds of that app will be pinned to that version +unless the build cache is cleared or you request a different version. + +The current default Python version is: 3.12 + +The supported Python versions are: + +- Python 3.13 +- Python 3.12 +- Python 3.11 +- Python 3.10 + +These Python versions are deprecated on Heroku: + +- Python 3.9 +- Python 3.8 (only available on the [Heroku-20](https://devcenter.heroku.com/articles/heroku-20-stack) stack) + +Python versions older than those listed above are no longer supported, since they have reached +end-of-life [upstream](https://devguide.python.org/versions/#supported-versions). + +## Documentation + +For more information about using Python on Heroku, see [Dev Center](https://devcenter.heroku.com/categories/python-support). diff --git a/bin/compile b/bin/compile index bc6621c15..f751965fc 100755 --- a/bin/compile +++ b/bin/compile @@ -154,6 +154,9 @@ fi python_version::read_requested_python_version "${BUILD_DIR}" "${package_manager}" "${cached_python_version}" requested_python_version python_version_origin meta_set "python_version_reason" "${python_version_origin}" +# TODO: More strongly recommend specifying a Python version (eg switch the messaging to +# be a warning instead, after version resolution, and mention .python-version inline) +# TODO: Add runtime.txt deprecation warning. case "${python_version_origin}" in default) puts-step "No Python version was specified. Using the buildpack default: Python ${requested_python_version}" diff --git a/bin/steps/python b/bin/steps/python index 725846d15..e4b605df2 100755 --- a/bin/steps/python +++ b/bin/steps/python @@ -18,7 +18,7 @@ PYTHON_URL="${S3_BASE_URL}/python-${python_full_version}-ubuntu-${UBUNTU_VERSION # TODO: Update this message to be more specific once Python 3.8 support is dropped. if ! curl --output /dev/null --silent --head --fail --retry 3 --retry-connrefused --connect-timeout 10 "${PYTHON_URL}"; then display_error <<-EOF - Error: Python ${python_full_version} is not available for this stack (${STACK}). + Error: Python ${python_full_version} isn't available for this stack (${STACK}). For a list of the supported Python versions, see: https://devcenter.heroku.com/articles/python-support#supported-runtimes @@ -35,6 +35,8 @@ function warn_if_patch_update_available() { # Extract the patch version component of the version strings (ie: the '5' in '3.10.5'). local requested_patch_number="${requested_full_version##*.}" local latest_patch_number="${latest_patch_version##*.}" + # TODO: Update this message to suggest using the .python-version major version syntax to stay up to date, + # once runtime.txt is deprecated and sticky-versioning only pins to the major version. if ((requested_patch_number < latest_patch_number)); then puts-warn puts-warn "A Python security update is available! Upgrade as soon as possible to: Python ${latest_patch_version}" diff --git a/lib/python_version.sh b/lib/python_version.sh index 820daf3a9..aa4df3539 100644 --- a/lib/python_version.sh +++ b/lib/python_version.sh @@ -18,8 +18,6 @@ DEFAULT_PYTHON_MAJOR_VERSION="${DEFAULT_PYTHON_FULL_VERSION%.*}" INT_REGEX="(0|[1-9][0-9]*)" # Versions of form N.N or N.N.N. PYTHON_VERSION_REGEX="${INT_REGEX}\.${INT_REGEX}(\.${INT_REGEX})?" -# Versions of form N.N.N only. -PYTHON_FULL_VERSION_REGEX="${INT_REGEX}\.${INT_REGEX}\.${INT_REGEX}" # Determine what Python version has been requested for the project. # @@ -29,8 +27,9 @@ PYTHON_FULL_VERSION_REGEX="${INT_REGEX}\.${INT_REGEX}\.${INT_REGEX}" # # If an app specifies the Python version via multiple means, then the order of precedence is: # 1. runtime.txt -# 2. Pipfile.lock (`python_full_version` field) -# 3. Pipfile.lock (`python_version` field) +# 2. .python-version +# 3. Pipfile.lock (`python_full_version` field) +# 4. Pipfile.lock (`python_version` field) # # If a version wasn't specified by the app, then new apps/those with an empty cache will use # a buildpack default version for the first build, and then subsequent cached builds will use @@ -58,6 +57,14 @@ function python_version::read_requested_python_version() { return 0 fi + local python_version_file_path="${build_dir}/.python-version" + if [[ -f "${python_version_file_path}" ]]; then + contents="$(cat "${python_version_file_path}")" + version="$(python_version::parse_python_version_file "${contents}")" + origin=".python-version" + return 0 + fi + if [[ "${package_manager}" == "pipenv" ]]; then version="$(python_version::read_pipenv_python_version "${build_dir}")" # The Python version fields in a Pipfile.lock are optional. @@ -79,40 +86,122 @@ function python_version::read_requested_python_version() { fi } -# Parse the contents of a runtime.txt file and return the Python version substring (e.g. `3.12.0`). +# Parse the contents of a runtime.txt file and return the Python version substring (e.g. `3.12` or `3.12.0`). function python_version::parse_runtime_txt() { local contents="${1}" - # The file must contain a string of form `python-N.N.N` (leading/trailing whitespace is permitted). - if [[ "${contents}" =~ ^[[:space:]]*python-(${PYTHON_FULL_VERSION_REGEX})[[:space:]]*$ ]]; then + # The file must contain a string of form `python-N.N` or `python-N.N.N`. + # Leading/trailing whitespace is permitted. + if [[ "${contents}" =~ ^[[:space:]]*python-(${PYTHON_VERSION_REGEX})[[:space:]]*$ ]]; then local version="${BASH_REMATCH[1]}" echo "${version}" else display_error <<-EOF Error: Invalid Python version in runtime.txt. - The Python version specified in 'runtime.txt' is not in + The Python version specified in 'runtime.txt' isn't in the correct format. The following file contents were found: ${contents} - However, the version string must begin with a 'python-' prefix, - followed by the version specified as '..'. - Comments are not supported. + However, the version must be specified as either: + 1. 'python-.' (recommended, for automatic patch updates) + 2. 'python-..' (to pin to an exact patch version) - For example, to request Python ${DEFAULT_PYTHON_FULL_VERSION}, use: - python-${DEFAULT_PYTHON_FULL_VERSION} + Remember to include the 'python-' prefix. Comments aren't supported. - Please update 'runtime.txt' to use a valid version string, or - else remove the file to instead use the default version - (currently Python ${DEFAULT_PYTHON_FULL_VERSION}). + For example, to request the latest version of Python ${DEFAULT_PYTHON_MAJOR_VERSION}, + update the 'runtime.txt' file so it contains: + python-${DEFAULT_PYTHON_MAJOR_VERSION} EOF - meta_set "failure_reason" "python-version-invalid" + meta_set "failure_reason" "runtime-txt::invalid-version" return 1 fi } +# Parse the contents of a .python-version file and return the Python version substring (e.g. `3.12` or `3.12.0`). +function python_version::parse_python_version_file() { + local contents="${1}" + local version_lines=() + + while IFS= read -r line; do + # Ignore lines that only contain whitespace and/or comments. + if [[ ! "${line}" =~ ^[[:space:]]*(#.*)?$ ]]; then + version_lines+=("${line}") + fi + done <<<"${contents}" + + case "${#version_lines[@]}" in + 1) + local line="${version_lines[0]}" + if [[ "${line}" =~ ^[[:space:]]*(${PYTHON_VERSION_REGEX})[[:space:]]*$ ]]; then + local version="${BASH_REMATCH[1]}" + echo "${version}" + return 0 + else + display_error <<-EOF + Error: Invalid Python version in .python-version. + + The Python version specified in '.python-version' isn't in + the correct format. + + The following version was found: + ${line} + + However, the version must be specified as either: + 1. '.' (recommended, for automatic patch updates) + 2. '..' (to pin to an exact patch version) + + Don't include quotes or a 'python-' prefix. To include + comments, add them on their own line, prefixed with '#'. + + For example, to request the latest version of Python ${DEFAULT_PYTHON_MAJOR_VERSION}, + update the '.python-version' file so it contains: + ${DEFAULT_PYTHON_MAJOR_VERSION} + EOF + meta_set "failure_reason" "python-version-file::invalid-version" + return 1 + fi + ;; + 0) + display_error <<-EOF + Error: Invalid Python version in .python-version. + + No Python version was found in the '.python-version' file. + + Update the file so that it contains a valid Python version + such as '${DEFAULT_PYTHON_MAJOR_VERSION}'. + + If the file already contains a version, check the line doesn't + begin with a '#', otherwise it will be treated as a comment. + EOF + meta_set "failure_reason" "python-version-file::no-version" + return 1 + ;; + *) + display_error <<-EOF + Error: Invalid Python version in .python-version. + + Multiple Python versions were found in the '.python-version' + file: + + $( + IFS=$'\n' + echo "${version_lines[*]}" + ) + + Update the file so it contains only one Python version. + + If the additional versions are actually comments, prefix + those lines with '#'. + EOF + meta_set "failure_reason" "python-version-file::multiple-versions" + return 1 + ;; + esac +} + # Read the Python version from a Pipfile.lock, which can exist in one of two optional fields, # `python_full_version` (as N.N.N) and `python_version` (as N.N). If both fields are # defined, we will use the value set in `python_full_version`. See: @@ -130,16 +219,16 @@ function python_version::read_pipenv_python_version() { if ! version=$(jq --raw-output '._meta.requires.python_full_version // ._meta.requires.python_version' "${pipfile_lock_path}" 2>&1); then display_error <<-EOF - Error: Cannot parse Pipfile.lock. + Error: Can't parse Pipfile.lock. - A Pipfile.lock file was found, however, it could not be parsed: + A Pipfile.lock file was found, however, it couldn't be parsed: ${version} This is likely due to it not being valid JSON. Run 'pipenv lock' to regenerate/fix the lockfile. EOF - meta_set "failure_reason" "pipfile-lock-invalid" + meta_set "failure_reason" "pipfile-lock::invalid-json" return 1 fi @@ -158,14 +247,14 @@ function python_version::read_pipenv_python_version() { Error: Invalid Python version in Pipfile / Pipfile.lock. The Python version specified in Pipfile / Pipfile.lock by the - 'python_version' or 'python_full_version' field is not valid. + 'python_version' or 'python_full_version' field isn't valid. The following version was found: ${version} However, the version must be specified as either: - 1. '.' (recommended, for automatic security updates) - 2. '..' (to pin to an exact Python version) + 1. '.' (recommended, for automatic patch updates) + 2. '..' (to pin to an exact patch version) Please update your 'Pipfile' to use a valid Python version and then run 'pipenv lock' to regenerate the lockfile. @@ -173,13 +262,13 @@ function python_version::read_pipenv_python_version() { For more information, see: https://pipenv.pypa.io/en/latest/specifiers.html#specifying-versions-of-python EOF - meta_set "failure_reason" "python-version-invalid" + meta_set "failure_reason" "pipfile-lock::invalid-version" return 1 fi } # Resolve a requested Python version (which can be of form N.N or N.N.N) to a specific -# Python version of form N.N.N. Rejects Python major versions that are not supported. +# Python version of form N.N.N. Rejects Python major versions that aren't supported. function python_version::resolve_python_version() { local requested_python_version="${1}" local python_version_origin="${2}" @@ -197,7 +286,7 @@ function python_version::resolve_python_version() { display_error <<-EOF Error: The cached Python version has reached end-of-life. - Your app does not specify a Python version, and so normally + Your app doesn't specify a Python version, and so normally would use the version cached from the last build (${requested_python_version}). However, Python ${major}.${minor} has reached its upstream end-of-life, @@ -207,8 +296,8 @@ function python_version::resolve_python_version() { As such, it is no longer supported by this buildpack. Please upgrade to a newer Python version by creating a - 'runtime.txt' file that contains a Python version like: - python-${DEFAULT_PYTHON_FULL_VERSION} + '.python-version' file that contains a Python version like: + ${DEFAULT_PYTHON_MAJOR_VERSION} For a list of the supported Python versions, see: https://devcenter.heroku.com/articles/python-support#supported-runtimes @@ -230,19 +319,19 @@ function python_version::resolve_python_version() { https://devcenter.heroku.com/articles/python-support#supported-runtimes EOF fi - meta_set "failure_reason" "python-version-eol" + meta_set "failure_reason" "python-version::eol" return 1 fi 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. + Error: The cached Python version isn't recognised. - Your app does not specify a Python version, and so normally + Your app doesn't specify a Python version, and so normally would use the version cached from the last build (${requested_python_version}). - However, Python ${major}.${minor} is not recognised by this version + However, Python ${major}.${minor} isn't recognised by this version of the buildpack. This can occur if you have downgraded the version of the @@ -252,9 +341,9 @@ function python_version::resolve_python_version() { EOF else display_error <<-EOF - Error: The requested Python version is not recognised. + Error: The requested Python version isn't recognised. - 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, and that the Python buildpack has added support for it: @@ -269,7 +358,7 @@ function python_version::resolve_python_version() { by updating the version configured via the '${python_version_origin}' file. EOF fi - meta_set "failure_reason" "python-version-unknown" + meta_set "failure_reason" "python-version::unknown-major" return 1 fi diff --git a/spec/fixtures/ci_pipenv/.python-version b/spec/fixtures/ci_pipenv/.python-version new file mode 100644 index 000000000..e4fba2183 --- /dev/null +++ b/spec/fixtures/ci_pipenv/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/spec/fixtures/ci_requirements/.python-version b/spec/fixtures/ci_requirements/.python-version new file mode 100644 index 000000000..e4fba2183 --- /dev/null +++ b/spec/fixtures/ci_requirements/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/spec/fixtures/pipenv_and_runtime_txt/runtime.txt b/spec/fixtures/pipenv_and_runtime_txt/runtime.txt deleted file mode 100644 index 32905d6e0..000000000 --- a/spec/fixtures/pipenv_and_runtime_txt/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.12.7 diff --git a/spec/fixtures/pipenv_python_3.12/Pipfile b/spec/fixtures/pipenv_basic/Pipfile similarity index 85% rename from spec/fixtures/pipenv_python_3.12/Pipfile rename to spec/fixtures/pipenv_basic/Pipfile index 9bf60112c..883fd6c01 100644 --- a/spec/fixtures/pipenv_python_3.12/Pipfile +++ b/spec/fixtures/pipenv_basic/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] diff --git a/spec/fixtures/pipenv_python_3.12/Pipfile.lock b/spec/fixtures/pipenv_basic/Pipfile.lock similarity index 59% rename from spec/fixtures/pipenv_python_3.12/Pipfile.lock rename to spec/fixtures/pipenv_basic/Pipfile.lock index cc5ce05bc..30824a47d 100644 --- a/spec/fixtures/pipenv_python_3.12/Pipfile.lock +++ b/spec/fixtures/pipenv_basic/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "13de6076b4dc8bbad334b7e4ae8e8ae63f7f8b4aad794c7f0173871b6927c8d0" + "sha256": "9661ed313a79ccb68c7dc4e639068f86ddd91e307ec2ed60498858d002e9b547" }, "pipfile-spec": 6, "requires": { @@ -16,14 +16,14 @@ ] }, "default": { - "urllib3": { + "typing-extensions": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==4.12.2" } }, "develop": {} diff --git a/spec/fixtures/pipenv_python_version_unspecified/bin/compile b/spec/fixtures/pipenv_basic/bin/compile similarity index 100% rename from spec/fixtures/pipenv_python_version_unspecified/bin/compile rename to spec/fixtures/pipenv_basic/bin/compile diff --git a/spec/fixtures/pipenv_python_version_unspecified/bin/detect b/spec/fixtures/pipenv_basic/bin/detect similarity index 100% rename from spec/fixtures/pipenv_python_version_unspecified/bin/detect rename to spec/fixtures/pipenv_basic/bin/detect diff --git a/spec/fixtures/pipenv_python_version_unspecified/setup.py b/spec/fixtures/pipenv_basic/setup.py similarity index 100% rename from spec/fixtures/pipenv_python_version_unspecified/setup.py rename to spec/fixtures/pipenv_basic/setup.py diff --git a/spec/fixtures/pipenv_no_lockfile/Pipfile b/spec/fixtures/pipenv_no_lockfile/Pipfile index 67803f45b..883fd6c01 100644 --- a/spec/fixtures/pipenv_no_lockfile/Pipfile +++ b/spec/fixtures/pipenv_no_lockfile/Pipfile @@ -4,6 +4,9 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] + +[requires] +python_version = "3.12" diff --git a/spec/fixtures/pipenv_python_3.10/Pipfile b/spec/fixtures/pipenv_python_3.10/Pipfile deleted file mode 100644 index 37291dc4f..000000000 --- a/spec/fixtures/pipenv_python_3.10/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -urllib3 = "*" - -[dev-packages] - -[requires] -python_version = "3.10" diff --git a/spec/fixtures/pipenv_python_3.11/Pipfile b/spec/fixtures/pipenv_python_3.11/Pipfile deleted file mode 100644 index 1d28b2380..000000000 --- a/spec/fixtures/pipenv_python_3.11/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -urllib3 = "*" - -[dev-packages] - -[requires] -python_version = "3.11" diff --git a/spec/fixtures/pipenv_python_3.11/Pipfile.lock b/spec/fixtures/pipenv_python_3.11/Pipfile.lock deleted file mode 100644 index 5b0ccff7d..000000000 --- a/spec/fixtures/pipenv_python_3.11/Pipfile.lock +++ /dev/null @@ -1,30 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "c23e4774efa7153ba8addfbe118e9cc0c526a063275824cf25cb2198e0ff1b33" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.11" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "urllib3": { - "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.2.1" - } - }, - "develop": {} -} diff --git a/spec/fixtures/pipenv_python_3.13/Pipfile b/spec/fixtures/pipenv_python_3.13/Pipfile deleted file mode 100644 index 73f50fc86..000000000 --- a/spec/fixtures/pipenv_python_3.13/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[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 deleted file mode 100644 index dcd0b3ca7..000000000 --- a/spec/fixtures/pipenv_python_3.13/Pipfile.lock +++ /dev/null @@ -1,30 +0,0 @@ -{ - "_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/pipenv_python_3.7/Pipfile.lock b/spec/fixtures/pipenv_python_3.7/Pipfile.lock deleted file mode 100644 index 90c8735f4..000000000 --- a/spec/fixtures/pipenv_python_3.7/Pipfile.lock +++ /dev/null @@ -1,30 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "b4a533c276cc9b89b506de9007fbe2210b33f8322ea81f62989117e5ec3adc54" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "urllib3": { - "hashes": [ - "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594", - "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.0.5" - } - }, - "develop": {} -} diff --git a/spec/fixtures/pipenv_python_3.8/Pipfile b/spec/fixtures/pipenv_python_3.8/Pipfile deleted file mode 100644 index 5aea20cfa..000000000 --- a/spec/fixtures/pipenv_python_3.8/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -urllib3 = "*" - -[dev-packages] - -[requires] -python_version = "3.8" diff --git a/spec/fixtures/pipenv_python_3.8/Pipfile.lock b/spec/fixtures/pipenv_python_3.8/Pipfile.lock deleted file mode 100644 index f1618e6a9..000000000 --- a/spec/fixtures/pipenv_python_3.8/Pipfile.lock +++ /dev/null @@ -1,30 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "e9eb0bba6cb0a97533d25cd3d68db92d4afd6b1dc88cd7a51607d8c34475eae0" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "urllib3": { - "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.2.1" - } - }, - "develop": {} -} diff --git a/spec/fixtures/pipenv_python_3.9/Pipfile b/spec/fixtures/pipenv_python_3.9/Pipfile deleted file mode 100644 index 7e9cf996d..000000000 --- a/spec/fixtures/pipenv_python_3.9/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -urllib3 = "*" - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/spec/fixtures/pipenv_python_3.9/Pipfile.lock b/spec/fixtures/pipenv_python_3.9/Pipfile.lock deleted file mode 100644 index 527944fb6..000000000 --- a/spec/fixtures/pipenv_python_3.9/Pipfile.lock +++ /dev/null @@ -1,30 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "7776f372e3411c742b3e8a4209e67f0832f9e249d4c86ce28ece146afaef68d1" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "urllib3": { - "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.2.1" - } - }, - "develop": {} -} diff --git a/spec/fixtures/pipenv_python_full_version/Pipfile b/spec/fixtures/pipenv_python_full_version/Pipfile index aa0852315..f3e9b3e9e 100644 --- a/spec/fixtures/pipenv_python_full_version/Pipfile +++ b/spec/fixtures/pipenv_python_full_version/Pipfile @@ -4,12 +4,13 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] [requires] # Uses the oldest Python version supported by all stacks, to validate Pipenv works with it. python_full_version = "3.9.0" -# Tests that `python_full_version` takes precedence. + +# `python_full_version` should take precedence over this. python_version = "3.12" diff --git a/spec/fixtures/pipenv_python_full_version/Pipfile.lock b/spec/fixtures/pipenv_python_full_version/Pipfile.lock index 888c9982e..3269fa9ce 100644 --- a/spec/fixtures/pipenv_python_full_version/Pipfile.lock +++ b/spec/fixtures/pipenv_python_full_version/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "068e8668ba7ae121efca9caf66d4796267713afe33c711e01a0852fe5e9a1848" + "sha256": "20aa9cfaef42c18684f934bc2c4e062167465b9a062fa284441acf1fe712dc60" }, "pipfile-spec": 6, "requires": { @@ -17,14 +17,14 @@ ] }, "default": { - "urllib3": { + "typing-extensions": { "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.2.3" + "version": "==4.12.2" } }, "develop": {} diff --git a/spec/fixtures/pipenv_python_full_version_invalid/Pipfile b/spec/fixtures/pipenv_python_full_version_invalid/Pipfile index 1782422d3..4016fd876 100644 --- a/spec/fixtures/pipenv_python_full_version_invalid/Pipfile +++ b/spec/fixtures/pipenv_python_full_version_invalid/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] diff --git a/spec/fixtures/pipenv_python_full_version_invalid/Pipfile.lock b/spec/fixtures/pipenv_python_full_version_invalid/Pipfile.lock index 107e6f348..a9d6269b5 100644 --- a/spec/fixtures/pipenv_python_full_version_invalid/Pipfile.lock +++ b/spec/fixtures/pipenv_python_full_version_invalid/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c629159445d228792003fe74526738c9155072a74a29f2d4920fc8aab878db0a" + "sha256": "254efba85c5858a7dfe232cb38f9ead91bf6e50bbf82f51a8a5a01904ca8712e" }, "pipfile-spec": 6, "requires": { @@ -16,14 +16,14 @@ ] }, "default": { - "urllib3": { + "typing-extensions": { "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.2.3" + "version": "==4.12.2" } }, "develop": {} diff --git a/spec/fixtures/pipenv_python_version_and_python_version_file/.python-version b/spec/fixtures/pipenv_python_version_and_python_version_file/.python-version new file mode 100644 index 000000000..6d977fdcf --- /dev/null +++ b/spec/fixtures/pipenv_python_version_and_python_version_file/.python-version @@ -0,0 +1,2 @@ +# This should take precedence over the Python version in Pipfile.lock. +3.13 diff --git a/spec/fixtures/pipenv_and_runtime_txt/Pipfile b/spec/fixtures/pipenv_python_version_and_python_version_file/Pipfile similarity index 60% rename from spec/fixtures/pipenv_and_runtime_txt/Pipfile rename to spec/fixtures/pipenv_python_version_and_python_version_file/Pipfile index 1d28b2380..5afe25b60 100644 --- a/spec/fixtures/pipenv_and_runtime_txt/Pipfile +++ b/spec/fixtures/pipenv_python_version_and_python_version_file/Pipfile @@ -4,9 +4,10 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] [requires] +# The version in .python-version should take precedence over this. python_version = "3.11" diff --git a/spec/fixtures/pipenv_and_runtime_txt/Pipfile.lock b/spec/fixtures/pipenv_python_version_and_python_version_file/Pipfile.lock similarity index 59% rename from spec/fixtures/pipenv_and_runtime_txt/Pipfile.lock rename to spec/fixtures/pipenv_python_version_and_python_version_file/Pipfile.lock index 5b0ccff7d..c505f1d3c 100644 --- a/spec/fixtures/pipenv_and_runtime_txt/Pipfile.lock +++ b/spec/fixtures/pipenv_python_version_and_python_version_file/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c23e4774efa7153ba8addfbe118e9cc0c526a063275824cf25cb2198e0ff1b33" + "sha256": "5a5846099fd5ceb0291e704a94f49cf0b8a226109fdae915a61253b56eaf3ed6" }, "pipfile-spec": 6, "requires": { @@ -16,14 +16,14 @@ ] }, "default": { - "urllib3": { + "typing-extensions": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==4.12.2" } }, "develop": {} diff --git a/spec/fixtures/pipenv_python_3.7/Pipfile b/spec/fixtures/pipenv_python_version_eol/Pipfile similarity index 85% rename from spec/fixtures/pipenv_python_3.7/Pipfile rename to spec/fixtures/pipenv_python_version_eol/Pipfile index c973bafe3..afe33e1b7 100644 --- a/spec/fixtures/pipenv_python_3.7/Pipfile +++ b/spec/fixtures/pipenv_python_version_eol/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] diff --git a/spec/fixtures/pipenv_python_3.10/Pipfile.lock b/spec/fixtures/pipenv_python_version_eol/Pipfile.lock similarity index 55% rename from spec/fixtures/pipenv_python_3.10/Pipfile.lock rename to spec/fixtures/pipenv_python_version_eol/Pipfile.lock index 7b07be0dc..cf0460577 100644 --- a/spec/fixtures/pipenv_python_3.10/Pipfile.lock +++ b/spec/fixtures/pipenv_python_version_eol/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "0975b9caa75f76ce36c7cf6ee6fc4b63510c0970ff8e322e0b5e6ee7af2d32e8" + "sha256": "319488142f6df2cfe3244f5ebb99c88eaa0ef808d4560570175577ce045dd02e" }, "pipfile-spec": 6, "requires": { - "python_version": "3.10" + "python_version": "3.7" }, "sources": [ { @@ -16,14 +16,14 @@ ] }, "default": { - "urllib3": { + "typing-extensions": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==4.12.2" } }, "develop": {} diff --git a/spec/fixtures/pipenv_python_version_invalid/Pipfile b/spec/fixtures/pipenv_python_version_invalid/Pipfile index 326889f7a..43fe5b889 100644 --- a/spec/fixtures/pipenv_python_version_invalid/Pipfile +++ b/spec/fixtures/pipenv_python_version_invalid/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -urllib3 = "*" +typing-extensions = "*" [dev-packages] diff --git a/spec/fixtures/pipenv_python_version_invalid/Pipfile.lock b/spec/fixtures/pipenv_python_version_invalid/Pipfile.lock index 47d197cdd..054e8d6cc 100644 --- a/spec/fixtures/pipenv_python_version_invalid/Pipfile.lock +++ b/spec/fixtures/pipenv_python_version_invalid/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "584096cd3f6a5c68b5f2e21993fc104773efb507ed1945ca26c789d86b22d883" + "sha256": "c86b61d10a1742dfbc75378ee86a81c24420506b154b9c19d69aa6e6c02c61b2" }, "pipfile-spec": 6, "requires": { @@ -16,14 +16,14 @@ ] }, "default": { - "urllib3": { + "typing-extensions": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==4.12.2" } }, "develop": {} diff --git a/spec/fixtures/python_2.7/requirements.txt b/spec/fixtures/pyproject_toml_only/pyproject.toml similarity index 100% rename from spec/fixtures/python_2.7/requirements.txt rename to spec/fixtures/pyproject_toml_only/pyproject.toml diff --git a/spec/fixtures/runtime_txt_only/subdir/some-file b/spec/fixtures/pyproject_toml_only/subdir/some-file similarity index 100% rename from spec/fixtures/runtime_txt_only/subdir/some-file rename to spec/fixtures/pyproject_toml_only/subdir/some-file diff --git a/spec/fixtures/python_3.10/.python-version b/spec/fixtures/python_3.10/.python-version new file mode 100644 index 000000000..3aa278d1a --- /dev/null +++ b/spec/fixtures/python_3.10/.python-version @@ -0,0 +1,2 @@ +# Comments are supported +3.10 diff --git a/spec/fixtures/python_3.10/requirements.txt b/spec/fixtures/python_3.10/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/python_3.10/requirements.txt +++ b/spec/fixtures/python_3.10/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/python_3.10/runtime.txt b/spec/fixtures/python_3.10/runtime.txt deleted file mode 100644 index bb60b7f69..000000000 --- a/spec/fixtures/python_3.10/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.10.15 diff --git a/spec/fixtures/python_3.11/.python-version b/spec/fixtures/python_3.11/.python-version new file mode 100644 index 000000000..576fc2bfe --- /dev/null +++ b/spec/fixtures/python_3.11/.python-version @@ -0,0 +1,2 @@ +# Comments are supported +3.11 diff --git a/spec/fixtures/python_3.11/requirements.txt b/spec/fixtures/python_3.11/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/python_3.11/requirements.txt +++ b/spec/fixtures/python_3.11/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/python_3.11/runtime.txt b/spec/fixtures/python_3.11/runtime.txt deleted file mode 100644 index e34519556..000000000 --- a/spec/fixtures/python_3.11/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.11.10 diff --git a/spec/fixtures/python_3.12/.python-version b/spec/fixtures/python_3.12/.python-version new file mode 100644 index 000000000..936a75516 --- /dev/null +++ b/spec/fixtures/python_3.12/.python-version @@ -0,0 +1,2 @@ +# Comments are supported +3.12 diff --git a/spec/fixtures/python_3.12/requirements.txt b/spec/fixtures/python_3.12/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/python_3.12/requirements.txt +++ b/spec/fixtures/python_3.12/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/python_3.12/runtime.txt b/spec/fixtures/python_3.12/runtime.txt deleted file mode 100644 index 32905d6e0..000000000 --- a/spec/fixtures/python_3.12/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.12.7 diff --git a/spec/fixtures/python_3.13/.python-version b/spec/fixtures/python_3.13/.python-version new file mode 100644 index 000000000..d23c34390 --- /dev/null +++ b/spec/fixtures/python_3.13/.python-version @@ -0,0 +1,2 @@ +# Comments are supported +3.13 diff --git a/spec/fixtures/python_3.13/requirements.txt b/spec/fixtures/python_3.13/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/python_3.13/requirements.txt +++ b/spec/fixtures/python_3.13/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/python_3.13/runtime.txt b/spec/fixtures/python_3.13/runtime.txt deleted file mode 100644 index dae7fec92..000000000 --- a/spec/fixtures/python_3.13/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.13.0 diff --git a/spec/fixtures/python_3.7/runtime.txt b/spec/fixtures/python_3.7/runtime.txt deleted file mode 100644 index 113b0bcca..000000000 --- a/spec/fixtures/python_3.7/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.7.17 diff --git a/spec/fixtures/python_3.8/.python-version b/spec/fixtures/python_3.8/.python-version new file mode 100644 index 000000000..6a8981ea9 --- /dev/null +++ b/spec/fixtures/python_3.8/.python-version @@ -0,0 +1,2 @@ +# Comments are supported +3.8 diff --git a/spec/fixtures/python_3.8/requirements.txt b/spec/fixtures/python_3.8/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/python_3.8/requirements.txt +++ b/spec/fixtures/python_3.8/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/python_3.8/runtime.txt b/spec/fixtures/python_3.8/runtime.txt deleted file mode 100644 index 7494875b9..000000000 --- a/spec/fixtures/python_3.8/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.8.20 diff --git a/spec/fixtures/python_3.9/.python-version b/spec/fixtures/python_3.9/.python-version new file mode 100644 index 000000000..54f2f0246 --- /dev/null +++ b/spec/fixtures/python_3.9/.python-version @@ -0,0 +1,2 @@ +# Comments are supported +3.9 diff --git a/spec/fixtures/python_3.9/requirements.txt b/spec/fixtures/python_3.9/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/python_3.9/requirements.txt +++ b/spec/fixtures/python_3.9/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/python_3.9/runtime.txt b/spec/fixtures/python_3.9/runtime.txt deleted file mode 100644 index 57f558859..000000000 --- a/spec/fixtures/python_3.9/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.9.20 diff --git a/spec/fixtures/python_version_eol/.python-version b/spec/fixtures/python_version_eol/.python-version new file mode 100644 index 000000000..81cd8ab6c --- /dev/null +++ b/spec/fixtures/python_version_eol/.python-version @@ -0,0 +1,8 @@ +# Comments are supported. + # Even when indented +# +# So are empty lines, and leading/trailing whitespace. + + + 3.7 + diff --git a/spec/fixtures/python_3.7/requirements.txt b/spec/fixtures/python_version_eol/requirements.txt similarity index 100% rename from spec/fixtures/python_3.7/requirements.txt rename to spec/fixtures/python_version_eol/requirements.txt diff --git a/spec/fixtures/python_version_file_invalid_version/.python-version b/spec/fixtures/python_version_file_invalid_version/.python-version new file mode 100644 index 000000000..4ffb299eb --- /dev/null +++ b/spec/fixtures/python_version_file_invalid_version/.python-version @@ -0,0 +1,10 @@ +# Comments are supported. + # Even when indented +# +# So are empty lines, and leading/trailing whitespace. + + + 3.12.0invalid + + +# 2.7.18 diff --git a/spec/fixtures/python_version_invalid/requirements.txt b/spec/fixtures/python_version_file_invalid_version/requirements.txt similarity index 100% rename from spec/fixtures/python_version_invalid/requirements.txt rename to spec/fixtures/python_version_file_invalid_version/requirements.txt diff --git a/spec/fixtures/python_version_file_multiple_versions/.python-version b/spec/fixtures/python_version_file_multiple_versions/.python-version new file mode 100644 index 000000000..99b9a4e4c --- /dev/null +++ b/spec/fixtures/python_version_file_multiple_versions/.python-version @@ -0,0 +1,4 @@ +// invalid comment +# Valid comment +3.12 +2.7 diff --git a/spec/fixtures/python_version_file_multiple_versions/requirements.txt b/spec/fixtures/python_version_file_multiple_versions/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/python_version_file_no_version/.python-version b/spec/fixtures/python_version_file_no_version/.python-version new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/python_version_file_no_version/requirements.txt b/spec/fixtures/python_version_file_no_version/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/python_version_non_existent_major/.python-version b/spec/fixtures/python_version_non_existent_major/.python-version new file mode 100644 index 000000000..f1f7e4608 --- /dev/null +++ b/spec/fixtures/python_version_non_existent_major/.python-version @@ -0,0 +1 @@ +3.999 diff --git a/spec/fixtures/python_version_non_existent_major/runtime.txt b/spec/fixtures/python_version_non_existent_major/runtime.txt deleted file mode 100644 index b6885d9e1..000000000 --- a/spec/fixtures/python_version_non_existent_major/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.999.0 diff --git a/spec/fixtures/python_version_non_existent_patch/.python-version b/spec/fixtures/python_version_non_existent_patch/.python-version new file mode 100644 index 000000000..bb40ea005 --- /dev/null +++ b/spec/fixtures/python_version_non_existent_patch/.python-version @@ -0,0 +1 @@ +3.12.999 diff --git a/spec/fixtures/python_version_non_existent_patch/runtime.txt b/spec/fixtures/python_version_non_existent_patch/runtime.txt deleted file mode 100644 index f9907fe4a..000000000 --- a/spec/fixtures/python_version_non_existent_patch/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.12.999 diff --git a/spec/fixtures/python_version_outdated/.python-version b/spec/fixtures/python_version_outdated/.python-version new file mode 100644 index 000000000..a5c4c7633 --- /dev/null +++ b/spec/fixtures/python_version_outdated/.python-version @@ -0,0 +1 @@ +3.9.0 diff --git a/spec/fixtures/python_version_outdated/runtime.txt b/spec/fixtures/python_version_outdated/runtime.txt deleted file mode 100644 index f72c5111f..000000000 --- a/spec/fixtures/python_version_outdated/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.9.0 diff --git a/spec/fixtures/python_version_unspecified/requirements.txt b/spec/fixtures/python_version_unspecified/requirements.txt index a42590beb..eec3a2223 100644 --- a/spec/fixtures/python_version_unspecified/requirements.txt +++ b/spec/fixtures/python_version_unspecified/requirements.txt @@ -1 +1,2 @@ -urllib3 +# This package has been picked since it has no dependencies and is small/fast to install. +typing-extensions==4.12.2 diff --git a/spec/fixtures/requirements_basic/.python-version b/spec/fixtures/requirements_basic/.python-version new file mode 100644 index 000000000..e4fba2183 --- /dev/null +++ b/spec/fixtures/requirements_basic/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/spec/fixtures/runtime_txt_and_python_version_file/.python-version b/spec/fixtures/runtime_txt_and_python_version_file/.python-version new file mode 100644 index 000000000..154e97ed3 --- /dev/null +++ b/spec/fixtures/runtime_txt_and_python_version_file/.python-version @@ -0,0 +1,2 @@ +# The version in runtime.txt should take precedence over this. +3.9 diff --git a/spec/fixtures/runtime_txt_and_python_version_file/requirements.txt b/spec/fixtures/runtime_txt_and_python_version_file/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/runtime_txt_and_python_version_file/runtime.txt b/spec/fixtures/runtime_txt_and_python_version_file/runtime.txt new file mode 100644 index 000000000..406dcdde4 --- /dev/null +++ b/spec/fixtures/runtime_txt_and_python_version_file/runtime.txt @@ -0,0 +1,3 @@ + + python-3.13 + \ No newline at end of file diff --git a/spec/fixtures/runtime_txt_eol_version/requirements.txt b/spec/fixtures/runtime_txt_eol_version/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/python_2.7/runtime.txt b/spec/fixtures/runtime_txt_eol_version/runtime.txt similarity index 100% rename from spec/fixtures/python_2.7/runtime.txt rename to spec/fixtures/runtime_txt_eol_version/runtime.txt diff --git a/spec/fixtures/runtime_txt_invalid_version/requirements.txt b/spec/fixtures/runtime_txt_invalid_version/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/python_version_invalid/runtime.txt b/spec/fixtures/runtime_txt_invalid_version/runtime.txt similarity index 100% rename from spec/fixtures/python_version_invalid/runtime.txt rename to spec/fixtures/runtime_txt_invalid_version/runtime.txt diff --git a/spec/fixtures/runtime_txt_only/runtime.txt b/spec/fixtures/runtime_txt_only/runtime.txt deleted file mode 100644 index 32905d6e0..000000000 --- a/spec/fixtures/runtime_txt_only/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.12.7 diff --git a/spec/fixtures/runtime_txt_with_stray_whitespace/requirements.txt b/spec/fixtures/runtime_txt_with_stray_whitespace/requirements.txt deleted file mode 100644 index a42590beb..000000000 --- a/spec/fixtures/runtime_txt_with_stray_whitespace/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -urllib3 diff --git a/spec/fixtures/runtime_txt_with_stray_whitespace/runtime.txt b/spec/fixtures/runtime_txt_with_stray_whitespace/runtime.txt deleted file mode 100644 index 05a24a304..000000000 --- a/spec/fixtures/runtime_txt_with_stray_whitespace/runtime.txt +++ /dev/null @@ -1,3 +0,0 @@ - - python-3.12.7 - \ No newline at end of file diff --git a/spec/fixtures/setup_py_only/.python-version b/spec/fixtures/setup_py_only/.python-version new file mode 100644 index 000000000..e4fba2183 --- /dev/null +++ b/spec/fixtures/setup_py_only/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/spec/hatchet/ci_spec.rb b/spec/hatchet/ci_spec.rb index 444b65e55..80a97706c 100644 --- a/spec/hatchet/ci_spec.rb +++ b/spec/hatchet/ci_spec.rb @@ -12,8 +12,7 @@ app.run_ci do |test_run| expect(test_run.output).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) -----> Python app detected - -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} - To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} -----> Installing SQLite3 @@ -70,8 +69,7 @@ expect(test_run.output).to include(<<~OUTPUT) -----> Python app detected - -----> No Python version was specified. Using the same version as the last build: Python #{DEFAULT_PYTHON_FULL_VERSION} - To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> No change in requirements detected, installing from cache -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} @@ -92,8 +90,7 @@ app.run_ci do |test_run| expect(test_run.output).to match(Regexp.new(<<~REGEX)) -----> Python app detected - -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} - To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} -----> Installing Pipenv #{PIPENV_VERSION} @@ -148,8 +145,7 @@ expect(test_run.output).to match(Regexp.new(<<~REGEX)) -----> Python app detected - -----> No Python version was specified. Using the same version as the last build: Python #{DEFAULT_PYTHON_FULL_VERSION} - To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} -----> Installing Pipenv #{PIPENV_VERSION} diff --git a/spec/hatchet/package_manager_spec.rb b/spec/hatchet/package_manager_spec.rb index 70473fc81..cde1f154b 100644 --- a/spec/hatchet/package_manager_spec.rb +++ b/spec/hatchet/package_manager_spec.rb @@ -4,7 +4,7 @@ RSpec.describe 'Package manager support' do context 'when there are no supported package manager files' do - let(:app) { Hatchet::Runner.new('spec/fixtures/runtime_txt_only', allow_failure: true) } + let(:app) { Hatchet::Runner.new('spec/fixtures/pyproject_toml_only', allow_failure: true) } it 'fails the build with an informative error message' do app.deploy do |app| @@ -19,7 +19,7 @@ remote: ! remote: ! Currently the root directory of your app contains: remote: ! - remote: ! runtime.txt + remote: ! pyproject.toml remote: ! subdir/ remote: ! remote: ! If your app already has a package manager file, check that it: diff --git a/spec/hatchet/pip_spec.rb b/spec/hatchet/pip_spec.rb index 178be593d..59b2ba0cc 100644 --- a/spec/hatchet/pip_spec.rb +++ b/spec/hatchet/pip_spec.rb @@ -22,8 +22,7 @@ app.deploy do |app| expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing SQLite3 @@ -63,8 +62,7 @@ app.push! expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the same version as the last build: Python #{DEFAULT_PYTHON_FULL_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version remote: -----> No change in requirements detected, installing from cache remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} @@ -86,8 +84,7 @@ app.push! expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the same version as the last build: Python #{DEFAULT_PYTHON_FULL_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version remote: -----> Requirements file has been changed, clearing cached dependencies remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} @@ -107,19 +104,19 @@ end context 'when the package manager has changed from Pipenv to pip since the last build' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified') } + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_basic') } # TODO: Fix this case so the cache is actually cleared. it 'clears the cache before installing with pip' do app.deploy do |app| FileUtils.rm(['Pipfile', 'Pipfile.lock']) + FileUtils.cp(FIXTURE_DIR.join('requirements_basic/.python-version'), '.') FileUtils.cp(FIXTURE_DIR.join('requirements_basic/requirements.txt'), '.') app.commit! app.push! expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the same version as the last build: Python #{DEFAULT_PYTHON_FULL_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing SQLite3 @@ -142,7 +139,7 @@ it 'rewrites .pth, .egg-link and finder paths correctly for hooks, later buildpacks, runtime and cached builds' do app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Running post-compile hook remote: easy-install.pth:/app/.heroku/src/gunicorn remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py @@ -181,7 +178,7 @@ # Test that the cached .pth files work correctly. app.commit! app.push! - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Running post-compile hook remote: easy-install.pth:/app/.heroku/src/gunicorn remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py @@ -214,8 +211,7 @@ app.deploy do |app| expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing SQLite3 diff --git a/spec/hatchet/pipenv_spec.rb b/spec/hatchet/pipenv_spec.rb index 91d4fbb1a..8abe6fb6d 100644 --- a/spec/hatchet/pipenv_spec.rb +++ b/spec/hatchet/pipenv_spec.rb @@ -2,64 +2,20 @@ require_relative '../spec_helper' -RSpec.shared_examples 'builds using Pipenv with the requested Python version' do |requested_version, resolved_version| - it "builds with Python #{requested_version}" do - app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) - remote: -----> Python app detected - remote: -----> Using Python #{requested_version} specified in Pipfile.lock - remote: -----> Installing Python #{resolved_version} - remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - remote: -----> Installing Pipenv #{PIPENV_VERSION} - remote: -----> Installing SQLite3 - remote: -----> Installing dependencies with Pipenv - remote: Installing dependencies from Pipfile.lock \\(.+\\)... - REGEX - end - end -end - RSpec.describe 'Pipenv support' do - context 'without a Pipfile.lock' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_no_lockfile') } - - it 'builds with the default Python version using just the Pipfile' do - app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) - remote: -----> Python app detected - remote: ! No 'Pipfile.lock' found! We recommend you commit this into your repository. - remote: -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes - remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} - remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - remote: -----> Installing Pipenv #{PIPENV_VERSION} - remote: -----> Installing SQLite3 - remote: -----> Installing dependencies with Pipenv - remote: The flag --skip-lock has been reintroduced \\(but is not recommended\\). Without - remote: the lock resolver it is difficult to manage multiple package indexes, and hash - remote: checking is not provided. However it can help manage installs with current - remote: deficiencies in locking across platforms. - remote: Pipfile.lock not found, creating... - .+ - remote: Installing dependencies from Pipfile... - REGEX - end - end - end - - context 'with a Pipfile.lock but no Python version specified' do + context 'with a Pipfile.lock that is unchanged since the last build' do let(:buildpacks) { [:default, 'heroku-community/inline'] } - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified', buildpacks:) } + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_basic', buildpacks:) } # TODO: Run this on Heroku-22 too, once it has also migrated to the new build infrastructure. # (Currently the test fails on the old infrastructure due to subtle differences in system PATH elements.) - it 'builds with the default Python version', stacks: %w[heroku-20 heroku-24] do + it 'builds with the specified python_version and re-uses packages from the cache', + stacks: %w[heroku-20 heroku-24] do app.deploy do |app| # TODO: We should not be leaking the Pipenv installation into the app environment. expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in Pipfile.lock remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing Pipenv #{PIPENV_VERSION} @@ -98,127 +54,81 @@ remote: remote: \\ REGEX + app.commit! + app.push! + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Python app detected + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in Pipfile.lock + remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} + remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + remote: -----> Installing Pipenv #{PIPENV_VERSION} + remote: -----> Installing SQLite3 + remote: -----> Installing dependencies with Pipenv + remote: Installing dependencies from Pipfile.lock \\(.+\\)... + remote: -----> Inline app detected + REGEX end end end - context 'with a Pipfile.lock containing an EOL python_version' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.7', allow_failure: true) } + # As well as testing the Pipfile.lock `python_full_version` field, this also tests: + # 1. That `python_full_version` takes precedence over the `python_version` field. + # 2. That Pipenv works on the oldest Python version supported by all stacks. + # 3. That the security update available message works for Pipenv too. + context 'with a Pipfile.lock containing python_full_version 3.9.0' do + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_full_version') } - it 'aborts the build with an EOL message' do + it 'builds with the outdated Python version specified and displays a deprecation warning' do app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~OUTPUT)) + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected - remote: -----> Using Python 3.7 specified in Pipfile.lock - remote: - remote: ! Error: The requested Python version has reached end-of-life. - remote: ! - remote: ! Python 3.7 has reached its upstream end-of-life, and is - remote: ! therefore no longer receiving security updates: - remote: ! https://devguide.python.org/versions/#supported-versions - remote: ! - remote: ! As such, it is no longer supported by this buildpack. + remote: -----> Using Python 3.9.0 specified in Pipfile.lock remote: ! - remote: ! Please upgrade to a newer Python version by updating the - remote: ! version configured via the 'Pipfile.lock' file. + remote: ! A Python security update is available! Upgrade as soon as possible to: Python #{LATEST_PYTHON_3_9} + remote: ! See: https://devcenter.heroku.com/articles/python-runtimes remote: ! - remote: ! For a list of the supported Python versions, see: - remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes - remote: - remote: ! Push rejected, failed to compile Python app. - OUTPUT + remote: -----> Installing Python 3.9.0 + remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} + remote: -----> Installing Pipenv #{PIPENV_VERSION} + remote: -----> Installing SQLite3 + remote: -----> Installing dependencies with Pipenv + remote: Installing dependencies from Pipfile.lock \\(.+\\)... + REGEX end end end - context 'with a Pipfile.lock containing python_version 3.8' do - let(:allow_failure) { false } - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.8', allow_failure:) } + context 'when there is a both a Pipfile.lock python_version and a .python-version file' do + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_and_python_version_file') } - context 'when using Heroku-20', stacks: %w[heroku-20] do - it 'builds with the latest Python 3.8 but shows a deprecation warning' do - app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) - remote: -----> Python app detected - remote: -----> Using Python 3.8 specified in Pipfile.lock - remote: ! - remote: ! Python 3.8 will reach its upstream end-of-life in October 2024, at which - remote: ! point it will no longer receive security updates: - remote: ! https://devguide.python.org/versions/#supported-versions - remote: ! - remote: ! Support for Python 3.8 will be removed from this buildpack on December 4th, 2024. - remote: ! - remote: ! Upgrade to a newer Python version as soon as possible to keep your app secure. - remote: ! See: https://devcenter.heroku.com/articles/python-runtimes - remote: ! - remote: -----> Installing Python #{LATEST_PYTHON_3_8} - remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - remote: -----> Installing Pipenv #{PIPENV_VERSION} - remote: -----> Installing SQLite3 - remote: -----> Installing dependencies with Pipenv - remote: Installing dependencies from Pipfile.lock \\(.+\\)... - REGEX - end - end - end - - context 'when using Heroku-22 or newer', stacks: %w[heroku-22 heroku-24] do - let(:allow_failure) { true } - - # We only support Python 3.8 on Heroku-20 and older. - it 'aborts the build with a version not available message' do - app.deploy do |app| - expect(clean_output(app.output)).to include(<<~OUTPUT) - remote: -----> Python app detected - remote: -----> Using Python 3.8 specified in Pipfile.lock - remote: - remote: ! Error: Python #{LATEST_PYTHON_3_8} is not available for this stack (#{app.stack}). - remote: ! - remote: ! For a list of the supported Python versions, see: - remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes - remote: - remote: ! Push rejected, failed to compile Python app. - OUTPUT - end + it 'builds with the Python version from the .python-version file' 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 .python-version + 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 - context 'with a Pipfile.lock containing python_version 3.9' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.9') } - - include_examples 'builds using Pipenv with the requested Python version', '3.9', LATEST_PYTHON_3_9 - end - - context 'with a Pipfile.lock containing python_version 3.10' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.10') } - - include_examples 'builds using Pipenv with the requested Python version', '3.10', LATEST_PYTHON_3_10 - end - - context 'with a Pipfile.lock containing python_version 3.11' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.11') } - - include_examples 'builds using Pipenv with the requested Python version', '3.11', LATEST_PYTHON_3_11 - end - - context 'with a Pipfile.lock containing python_version 3.12' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.12') } - - 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') } + context 'with a Pipfile.lock but no Python version specified' do + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified') } - it 'builds with latest Python 3.13' do + it 'builds with the default Python version' 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: -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} + remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} + remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing Pipenv #{PIPENV_VERSION} + remote: -----> Installing SQLite3 remote: -----> Installing dependencies with Pipenv remote: Installing dependencies from Pipfile.lock \\(.+\\)... REGEX @@ -226,29 +136,28 @@ 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. - # 3. That the security update available message works for Pipenv too. - context 'with a Pipfile.lock containing python_full_version 3.9.0' do - let(:allow_failure) { false } - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_full_version', allow_failure:) } + context 'without a Pipfile.lock' do + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_no_lockfile') } - it 'builds with the outdated Python version specified and displays a deprecation warning' do + it 'builds with the default Python version using just the Pipfile' do app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) remote: -----> Python app detected - remote: -----> Using Python 3.9.0 specified in Pipfile.lock - remote: ! - remote: ! A Python security update is available! Upgrade as soon as possible to: Python #{LATEST_PYTHON_3_9} - remote: ! See: https://devcenter.heroku.com/articles/python-runtimes - remote: ! - remote: -----> Installing Python 3.9.0 + remote: ! No 'Pipfile.lock' found! We recommend you commit this into your repository. + remote: -----> No Python version was specified. Using the buildpack default: Python #{DEFAULT_PYTHON_MAJOR_VERSION} + remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing Pipenv #{PIPENV_VERSION} remote: -----> Installing SQLite3 remote: -----> Installing dependencies with Pipenv - remote: Installing dependencies from Pipfile.lock \\(.+\\)... + remote: The flag --skip-lock has been reintroduced \\(but is not recommended\\). Without + remote: the lock resolver it is difficult to manage multiple package indexes, and hash + remote: checking is not provided. However it can help manage installs with current + remote: deficiencies in locking across platforms. + remote: Pipfile.lock not found, creating... + .+ + remote: Installing dependencies from Pipfile... REGEX end end @@ -263,9 +172,9 @@ expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected remote: - remote: ! Error: Cannot parse Pipfile.lock. + remote: ! Error: Can't parse Pipfile.lock. remote: ! - remote: ! A Pipfile.lock file was found, however, it could not be parsed: + remote: ! A Pipfile.lock file was found, however, it couldn't be parsed: remote: ! (jq: )?parse error: Invalid numeric literal at line 1, column 8 remote: ! remote: ! This is likely due to it not being valid JSON. @@ -289,14 +198,14 @@ remote: ! Error: Invalid Python version in Pipfile / Pipfile.lock. remote: ! remote: ! The Python version specified in Pipfile / Pipfile.lock by the - remote: ! 'python_version' or 'python_full_version' field is not valid. + remote: ! 'python_version' or 'python_full_version' field isn't valid. remote: ! remote: ! The following version was found: remote: ! ^3.12 remote: ! remote: ! However, the version must be specified as either: - remote: ! 1. '.' (recommended, for automatic security updates) - remote: ! 2. '..' (to pin to an exact Python version) + remote: ! 1. '.' (recommended, for automatic patch updates) + remote: ! 2. '..' (to pin to an exact patch version) remote: ! remote: ! Please update your 'Pipfile' to use a valid Python version and remote: ! then run 'pipenv lock' to regenerate the lockfile. @@ -321,14 +230,14 @@ remote: ! Error: Invalid Python version in Pipfile / Pipfile.lock. remote: ! remote: ! The Python version specified in Pipfile / Pipfile.lock by the - remote: ! 'python_version' or 'python_full_version' field is not valid. + remote: ! 'python_version' or 'python_full_version' field isn't valid. remote: ! remote: ! The following version was found: remote: ! 3.9.* remote: ! remote: ! However, the version must be specified as either: - remote: ! 1. '.' (recommended, for automatic security updates) - remote: ! 2. '..' (to pin to an exact Python version) + remote: ! 1. '.' (recommended, for automatic patch updates) + remote: ! 2. '..' (to pin to an exact patch version) remote: ! remote: ! Please update your 'Pipfile' to use a valid Python version and remote: ! then run 'pipenv lock' to regenerate the lockfile. @@ -342,63 +251,50 @@ end end - context 'when there is a both a Pipfile.lock python_version and a runtime.txt' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_and_runtime_txt') } - - it 'builds with the Python version from runtime.txt' do - app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) - remote: -----> Python app detected - remote: -----> Using Python #{LATEST_PYTHON_3_12} specified in runtime.txt - remote: -----> Installing Python #{LATEST_PYTHON_3_12} - remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - remote: -----> Installing Pipenv #{PIPENV_VERSION} - remote: -----> Installing SQLite3 - remote: -----> Installing dependencies with Pipenv - remote: Installing dependencies from Pipfile.lock \\(.+\\)... - REGEX - end - end - end - - context 'when Pipfile.lock is unchanged since the last build' do - let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified') } + context 'with a Pipfile.lock containing an EOL python_version' do + let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_eol', allow_failure: true) } - it 're-uses packages from the cache' do + it 'aborts the build with an EOL message' do app.deploy do |app| - app.commit! - app.push! - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + expect(clean_output(app.output)).to match(Regexp.new(<<~OUTPUT)) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the same version as the last build: Python #{DEFAULT_PYTHON_FULL_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes - remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} - remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - remote: -----> Installing Pipenv #{PIPENV_VERSION} - remote: -----> Installing SQLite3 - remote: -----> Installing dependencies with Pipenv - remote: Installing dependencies from Pipfile.lock \\(.+\\)... - remote: -----> Discovering process types - REGEX + remote: -----> Using Python 3.7 specified in Pipfile.lock + remote: + remote: ! Error: The requested Python version has reached end-of-life. + remote: ! + remote: ! Python 3.7 has reached its upstream end-of-life, and is + remote: ! therefore no longer receiving security updates: + remote: ! https://devguide.python.org/versions/#supported-versions + remote: ! + remote: ! As such, it is no longer supported by this buildpack. + remote: ! + remote: ! Please upgrade to a newer Python version by updating the + remote: ! version configured via the 'Pipfile.lock' file. + remote: ! + remote: ! For a list of the supported Python versions, see: + remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT end end end context 'when the package manager has changed from pip to Pipenv since the last build' do - let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified') } + let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_basic') } # TODO: Fix this case so the cache is actually cleared. it 'clears the cache before installing with Pipenv' do app.deploy do |app| + FileUtils.rm('.python-version') FileUtils.rm('requirements.txt') - FileUtils.cp(FIXTURE_DIR.join('pipenv_python_version_unspecified/Pipfile'), '.') - FileUtils.cp(FIXTURE_DIR.join('pipenv_python_version_unspecified/Pipfile.lock'), '.') + FileUtils.cp(FIXTURE_DIR.join('pipenv_basic/Pipfile'), '.') + FileUtils.cp(FIXTURE_DIR.join('pipenv_basic/Pipfile.lock'), '.') app.commit! app.push! expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected - remote: -----> No Python version was specified. Using the same version as the last build: Python #{DEFAULT_PYTHON_FULL_VERSION} - remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in Pipfile.lock remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing Pipenv #{PIPENV_VERSION} @@ -459,7 +355,7 @@ it 'rewrites .pth, .egg-link and finder paths correctly for hooks, later buildpacks, runtime and cached builds' do app.deploy do |app| - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Running post-compile hook remote: easy-install.pth:/app/.heroku/src/gunicorn remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py @@ -498,7 +394,7 @@ # Test that the cached .pth files work correctly. app.commit! app.push! - expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Running post-compile hook remote: easy-install.pth:/app/.heroku/src/gunicorn remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py diff --git a/spec/hatchet/python_update_warning_spec.rb b/spec/hatchet/python_update_warning_spec.rb index 787f24a18..664c15bc1 100644 --- a/spec/hatchet/python_update_warning_spec.rb +++ b/spec/hatchet/python_update_warning_spec.rb @@ -46,7 +46,7 @@ remote: -----> Python app detected remote: -----> Using Python 3.8.0 specified in runtime.txt remote: - remote: ! Error: Python 3.8.0 is not available for this stack (#{app.stack}). + remote: ! Error: Python 3.8.0 isn't available for this stack (#{app.stack}). remote: ! remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes @@ -58,14 +58,14 @@ end end - context 'with a runtime.txt containing an outdated patch version' do + context 'with a .python-version file containing an outdated patch version' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_outdated') } it 'warns there is a Python update available' do app.deploy do |app| expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> Using Python 3.9.0 specified in runtime.txt + remote: -----> Using Python 3.9.0 specified in .python-version remote: ! remote: ! A Python security update is available! Upgrade as soon as possible to: Python #{LATEST_PYTHON_3_9} remote: ! See: https://devcenter.heroku.com/articles/python-runtimes diff --git a/spec/hatchet/python_version_spec.rb b/spec/hatchet/python_version_spec.rb index e9751a920..ec6313d30 100644 --- a/spec/hatchet/python_version_spec.rb +++ b/spec/hatchet/python_version_spec.rb @@ -2,30 +2,30 @@ require_relative '../spec_helper' -RSpec.shared_examples 'builds with the requested Python version' do |requested_version| +RSpec.shared_examples 'builds with the requested Python version' do |requested_version, resolved_version| it "builds with Python #{requested_version}" do app.deploy do |app| - if requested_version.start_with?('3.13.') + if requested_version == '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: -----> Using Python #{requested_version} specified in .python-version + remote: -----> Installing Python #{resolved_version} remote: -----> Installing pip #{PIP_VERSION} remote: -----> Installing requirements with pip - remote: Collecting urllib3 (from -r requirements.txt (line 1)) + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) 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: -----> Using Python #{requested_version} specified in .python-version + remote: -----> Installing Python #{resolved_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)) + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) OUTPUT end - expect(app.run('python -V')).to include("Python #{requested_version}") + expect(app.run('python -V')).to include("Python #{resolved_version}") end end end @@ -76,74 +76,16 @@ end end - context 'when runtime.txt contains python-2.7.18' do - let(:app) { Hatchet::Runner.new('spec/fixtures/python_2.7', allow_failure: true) } - - it 'aborts the build with an EOL message' do - app.deploy do |app| - expect(clean_output(app.output)).to include(<<~OUTPUT) - remote: -----> Python app detected - remote: -----> Using Python 2.7.18 specified in runtime.txt - remote: - remote: ! Error: The requested Python version has reached end-of-life. - remote: ! - remote: ! Python 2.7 has reached its upstream end-of-life, and is - remote: ! therefore no longer receiving security updates: - remote: ! https://devguide.python.org/versions/#supported-versions - remote: ! - remote: ! As such, it is no longer supported by this buildpack. - remote: ! - remote: ! Please upgrade to a newer Python version by updating the - remote: ! version configured via the 'runtime.txt' file. - remote: ! - remote: ! For a list of the supported Python versions, see: - remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes - remote: - remote: ! Push rejected, failed to compile Python app. - OUTPUT - end - end - end - - context 'when runtime.txt contains python-3.7.17' do - let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.7', allow_failure: true) } - - it 'aborts the build with an EOL message' do - app.deploy do |app| - expect(clean_output(app.output)).to include(<<~OUTPUT) - remote: -----> Python app detected - remote: -----> Using Python 3.7.17 specified in runtime.txt - remote: - remote: ! Error: The requested Python version has reached end-of-life. - remote: ! - remote: ! Python 3.7 has reached its upstream end-of-life, and is - remote: ! therefore no longer receiving security updates: - remote: ! https://devguide.python.org/versions/#supported-versions - remote: ! - remote: ! As such, it is no longer supported by this buildpack. - remote: ! - remote: ! Please upgrade to a newer Python version by updating the - remote: ! version configured via the 'runtime.txt' file. - remote: ! - remote: ! For a list of the supported Python versions, see: - remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes - remote: - remote: ! Push rejected, failed to compile Python app. - OUTPUT - end - end - end - - context 'when runtime.txt contains python-3.8.20' do + context 'when .python-version contains Python 3.8' do let(:allow_failure) { false } let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.8', allow_failure:) } context 'when using Heroku-20', stacks: %w[heroku-20] do - it 'builds with Python 3.8.20 but shows a deprecation warning' do + it 'builds with latest Python 3.8 but shows a deprecation warning' do app.deploy do |app| expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> Using Python #{LATEST_PYTHON_3_8} specified in runtime.txt + remote: -----> Using Python 3.8 specified in .python-version remote: ! remote: ! Python 3.8 will reach its upstream end-of-life in October 2024, at which remote: ! point it will no longer receive security updates: @@ -158,7 +100,7 @@ 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)) + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) OUTPUT expect(app.run('python -V')).to include("Python #{LATEST_PYTHON_3_8}") end @@ -173,9 +115,9 @@ app.deploy do |app| expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> Using Python #{LATEST_PYTHON_3_8} specified in runtime.txt + remote: -----> Using Python 3.8 specified in .python-version remote: - remote: ! Error: Python #{LATEST_PYTHON_3_8} is not available for this stack (#{app.stack}). + remote: ! Error: Python #{LATEST_PYTHON_3_8} isn't available for this stack (#{app.stack}). remote: ! remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes @@ -187,62 +129,143 @@ end end - context 'when runtime.txt contains python-3.9.20' do + context 'when .python-version contains Python 3.9' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.9') } - include_examples 'builds with the requested Python version', LATEST_PYTHON_3_9 + include_examples 'builds with the requested Python version', '3.9', LATEST_PYTHON_3_9 end - context 'when runtime.txt contains python-3.10.15' do + context 'when .python-version contains Python 3.10' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.10') } - include_examples 'builds with the requested Python version', LATEST_PYTHON_3_10 + include_examples 'builds with the requested Python version', '3.10', LATEST_PYTHON_3_10 end - context 'when runtime.txt contains python-3.11.10' do + context 'when .python-version contains Python 3.11' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.11') } - include_examples 'builds with the requested Python version', LATEST_PYTHON_3_11 + include_examples 'builds with the requested Python version', '3.11', LATEST_PYTHON_3_11 end - context 'when runtime.txt contains python-3.12.7' do + context 'when .python-version contains Python 3.12' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.12') } - include_examples 'builds with the requested Python version', LATEST_PYTHON_3_12 + include_examples 'builds with the requested Python version', '3.12', LATEST_PYTHON_3_12 end - context 'when runtime.txt contains python-3.13.0' do + context 'when .python-version contains Python 3.13' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.13') } - include_examples 'builds with the requested Python version', LATEST_PYTHON_3_13 + include_examples 'builds with the requested Python version', '3.13', 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) } + context 'when .python-version contains an invalid Python version string' do + let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_file_invalid_version', allow_failure: true) } - it 'aborts the build with an invalid runtime.txt message' do + it 'aborts the build with an invalid .python-version message' do app.deploy do |app| expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: - remote: ! Error: Invalid Python version in runtime.txt. + remote: ! Error: Invalid Python version in .python-version. remote: ! - remote: ! The Python version specified in 'runtime.txt' is not in + remote: ! The Python version specified in '.python-version' isn't in remote: ! the correct format. remote: ! - remote: ! The following file contents were found: - remote: ! python-3.12.0invalid + remote: ! The following version was found: + remote: ! 3.12.0invalid + remote: ! + remote: ! However, the version must be specified as either: + remote: ! 1. '.' (recommended, for automatic patch updates) + remote: ! 2. '..' (to pin to an exact patch version) + remote: ! + remote: ! Don't include quotes or a 'python-' prefix. To include + remote: ! comments, add them on their own line, prefixed with '#'. + remote: ! + remote: ! For example, to request the latest version of Python #{DEFAULT_PYTHON_MAJOR_VERSION}, + remote: ! update the '.python-version' file so it contains: + remote: ! #{DEFAULT_PYTHON_MAJOR_VERSION} + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end + + context 'when .python-version does not contain a Python version' do + let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_file_no_version', allow_failure: true) } + + it 'aborts the build with a no version string found message' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: + remote: ! Error: Invalid Python version in .python-version. + remote: ! + remote: ! No Python version was found in the '.python-version' file. + remote: ! + remote: ! Update the file so that it contains a valid Python version + remote: ! such as '3.12'. + remote: ! + remote: ! If the file already contains a version, check the line doesn't + remote: ! begin with a '#', otherwise it will be treated as a comment. + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end + + context 'when .python-version contains multiple Python versions' do + let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_file_multiple_versions', allow_failure: true) } + + it 'aborts the build with a multiple versions not supported message' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: + remote: ! Error: Invalid Python version in .python-version. remote: ! - remote: ! However, the version string must begin with a 'python-' prefix, - remote: ! followed by the version specified as '..'. - remote: ! Comments are not supported. + remote: ! Multiple Python versions were found in the '.python-version' + remote: ! file: remote: ! - remote: ! For example, to request Python 3.12.7, use: - remote: ! python-3.12.7 + remote: ! // invalid comment + remote: ! 3.12 + remote: ! 2.7 remote: ! - remote: ! Please update 'runtime.txt' to use a valid version string, or - remote: ! else remove the file to instead use the default version - remote: ! (currently Python 3.12.7). + remote: ! Update the file so it contains only one Python version. + remote: ! + remote: ! If the additional versions are actually comments, prefix + remote: ! those lines with '#'. + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end + + context 'when .python-version contains an EOL Python 3.x version' do + let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_eol', allow_failure: true) } + + it 'aborts the build with an EOL message' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: -----> Using Python 3.7 specified in .python-version + remote: + remote: ! Error: The requested Python version has reached end-of-life. + remote: ! + remote: ! Python 3.7 has reached its upstream end-of-life, and is + remote: ! therefore no longer receiving security updates: + remote: ! https://devguide.python.org/versions/#supported-versions + remote: ! + remote: ! As such, it is no longer supported by this buildpack. + remote: ! + remote: ! Please upgrade to a newer Python version by updating the + remote: ! version configured via the '.python-version' file. + remote: ! + remote: ! For a list of the supported Python versions, see: + remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes remote: remote: ! Push rejected, failed to compile Python app. OUTPUT @@ -250,18 +273,18 @@ end end - context 'when runtime.txt contains an non-existent Python major version' do + context 'when .python-version contains an non-existent Python major version' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_non_existent_major', allow_failure: true) } - it 'aborts the build with an invalid runtime.txt message' do + it 'aborts the build with an invalid .python-version message' do app.deploy do |app| expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> Using Python 3.999.0 specified in runtime.txt + remote: -----> Using Python 3.999 specified in .python-version remote: - remote: ! Error: The requested Python version is not recognised. + remote: ! Error: The requested Python version isn't recognised. remote: ! - remote: ! The requested Python version 3.999 is not recognised. + remote: ! The requested Python version 3.999 isn't recognised. remote: ! remote: ! Check that this Python version has been officially released, remote: ! and that the Python buildpack has added support for it: @@ -272,8 +295,8 @@ remote: ! of this buildpack: remote: ! https://devcenter.heroku.com/articles/python-support#checking-the-python-buildpack-version remote: ! - remote: ! Otherwise, switch to a supported version (such as Python 3.12) - remote: ! by updating the version configured via the 'runtime.txt' file. + remote: ! Otherwise, switch to a supported version (such as Python #{DEFAULT_PYTHON_MAJOR_VERSION}) + remote: ! by updating the version configured via the '.python-version' file. remote: remote: ! Push rejected, failed to compile Python app. OUTPUT @@ -281,16 +304,77 @@ end end - context 'when runtime.txt contains a non-existent Python patch version' do + context 'when .python-version contains a non-existent Python patch version' do let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_non_existent_patch', allow_failure: true) } it 'aborts the build with a version not available message' do app.deploy do |app| expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> Using Python 3.12.999 specified in runtime.txt + remote: -----> Using Python 3.12.999 specified in .python-version + remote: + remote: ! Error: Python 3.12.999 isn't available for this stack (#{app.stack}). + remote: ! + remote: ! For a list of the supported Python versions, see: + remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end + + context 'when runtime.txt contains an invalid Python version string' do + let(:app) { Hatchet::Runner.new('spec/fixtures/runtime_txt_invalid_version', allow_failure: true) } + + it 'aborts the build with an invalid runtime.txt message' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected remote: - remote: ! Error: Python 3.12.999 is not available for this stack (#{app.stack}). + remote: ! Error: Invalid Python version in runtime.txt. + remote: ! + remote: ! The Python version specified in 'runtime.txt' isn't in + remote: ! the correct format. + remote: ! + remote: ! The following file contents were found: + remote: ! python-3.12.0invalid + remote: ! + remote: ! However, the version must be specified as either: + remote: ! 1. 'python-.' (recommended, for automatic patch updates) + remote: ! 2. 'python-..' (to pin to an exact patch version) + remote: ! + remote: ! Remember to include the 'python-' prefix. Comments aren't supported. + remote: ! + remote: ! For example, to request the latest version of Python #{DEFAULT_PYTHON_MAJOR_VERSION}, + remote: ! update the 'runtime.txt' file so it contains: + remote: ! python-#{DEFAULT_PYTHON_MAJOR_VERSION} + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end + + context 'when runtime.txt contains an EOL Python 2.x version' do + let(:app) { Hatchet::Runner.new('spec/fixtures/runtime_txt_eol_version', allow_failure: true) } + + it 'aborts the build with an EOL message' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: -----> Using Python 2.7.18 specified in runtime.txt + remote: + remote: ! Error: The requested Python version has reached end-of-life. + remote: ! + remote: ! Python 2.7 has reached its upstream end-of-life, and is + remote: ! therefore no longer receiving security updates: + remote: ! https://devguide.python.org/versions/#supported-versions + remote: ! + remote: ! As such, it is no longer supported by this buildpack. + remote: ! + remote: ! Please upgrade to a newer Python version by updating the + remote: ! version configured via the 'runtime.txt' file. remote: ! remote: ! For a list of the supported Python versions, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes @@ -301,10 +385,22 @@ end end - context 'when runtime.txt contains stray whitespace' do - let(:app) { Hatchet::Runner.new('spec/fixtures/runtime_txt_with_stray_whitespace') } + # This also tests runtime.txt support for the major version only syntax, as well as the handling + # of runtime.txt files that contain stray whitespace. + context 'when there is both a runtime.txt and .python-version file' do + let(:app) { Hatchet::Runner.new('spec/fixtures/runtime_txt_and_python_version_file') } - include_examples 'builds with the requested Python version', LATEST_PYTHON_3_12 + it 'builds with the version from runtime.txt' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: -----> Using Python 3.13 specified in runtime.txt + remote: -----> Installing Python #{LATEST_PYTHON_3_13} + remote: -----> Installing pip #{PIP_VERSION} + remote: -----> Installing requirements with pip + OUTPUT + end + end end context 'when the requested Python version has changed since the last build' do @@ -312,20 +408,19 @@ it 'builds with the new Python version after removing the old install' do app.deploy do |app| - File.write('runtime.txt', "python-#{LATEST_PYTHON_3_12}") + File.write('.python-version', '3.13') app.commit! app.push! # TODO: The output shouldn't say "installing from cache", since it's not. expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected - remote: -----> Using Python #{LATEST_PYTHON_3_12} specified in runtime.txt - remote: -----> Python version has changed from #{LATEST_PYTHON_3_9} to #{LATEST_PYTHON_3_12}, clearing cache + remote: -----> Using Python 3.13 specified in .python-version + remote: -----> Python version has changed from #{LATEST_PYTHON_3_9} to #{LATEST_PYTHON_3_13}, clearing cache remote: -----> No change in requirements detected, installing from cache - remote: -----> Installing Python #{LATEST_PYTHON_3_12} - remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} - remote: -----> Installing SQLite3 + remote: -----> Installing Python #{LATEST_PYTHON_3_13} + remote: -----> Installing pip #{PIP_VERSION} remote: -----> Installing requirements with pip - remote: Collecting urllib3 (from -r requirements.txt (line 1)) + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) OUTPUT end end diff --git a/spec/hatchet/stack_spec.rb b/spec/hatchet/stack_spec.rb index 0135e0628..3f55f5337 100644 --- a/spec/hatchet/stack_spec.rb +++ b/spec/hatchet/stack_spec.rb @@ -34,7 +34,7 @@ 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)) + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) OUTPUT end end @@ -59,7 +59,7 @@ 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)) + remote: Collecting typing-extensions==4.12.2 (from -r requirements.txt (line 2)) OUTPUT end end