From 17bea385ad6c13995f261be60bd1c48b8d1a35d6 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 21 Aug 2019 20:58:00 +0200 Subject: [PATCH 01/20] Switch Heroku account used by Travis CI Change the HEROKU_API_KEY and HEROKU_API_USER environment variables used by Travis CI. The encrypted values were generated using the following commands: heroku authorizations:create --description "For Travis" travis login --com travis encrypt HEROKU_API_KEY= --add --com travis encrypt HEROKU_API_USER= --add --com --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 703f4a125..51753c1df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,5 +38,5 @@ env: - IS_RUNNING_ON_CI=true - HATCHET_APP_LIMIT=5 - HATCHET_DEPLOY_STRATEGY=git - - secure: yjtlPE5FbVxTKnjUy/tZUBgSEf4qADD3QOxtgziuid73S0U/1IEXlMGFULsQzIjtlHKmHeywZqpVVEpthIH4RuT7uoX1Pb7SSM/g0T8fT3VoEFbFK1uYl0oZQbUS4Klxv9tPiumj8if3m6ULEGIz1X0wZcMOC0tMLwVCnwmap0E= - - secure: ZeFTHWwnpIKE9nAqs88ocmiQh7bKce84lilGm5J23nf3N6V4wNyLwqlkvsM008WGBCaOg9AUx7ZunasT0ANsR5gLP3eV2UUg7ILdRgV2Gy13eNRFheC4PHdN92RqQ3aKoqlIv2K999xlhVjod0NzhkQQXB6PddfQINbuU7ks6As= + - secure: aICPFKFCmu+OWynYd0QRJVU37Rw3GTCxy3PePj5DSDx3/2S2i0BXXLhn/LECdMY98h24O0xL8k860+gy646Mjq8Cpaf16BUNJhWH5HWS50paymREhnrYd/twSh0i6M2dnlbCjXDHWc8n+64/vu4FmS1UhLw2nQBMCrcIRcxHBpPPHIy3F4pctGBb1LEaExmA3JQRDUoX6uzgt/68gjMDPfvUwM5lzBwRLOdpUwNOvIlYxKioxcqjhMAFuHNo3na50QzPlNE2dqt84iDRj4N7/7v6NzWhG/9O1TxkcVCjn8ZhRIsFQoIEW32X0tqGCsSMpKiR+h6PjGrt+y07xjiRKQPNoFdhjpASO7sGpPrENsOxN263XJPmea9iGJrAvLBYePHdhblzwrXfcM0285RxRJTZg6o7BFW16+PkurpuS7dGvX/7SYAv3IXCksWBbDk+RtpS5KVmYrv6MI+fuGdSFVRq+RS1zmWwaXD1zpc2N4hpV2ZnwI/uwuDwrTd60ou2pXLxttrIw9dqq44CmY5vAu69Fu4dw2BkmUhGUf8ApG6TTgqrt5rdO2W11D4qr/YNZ9Jydh3m+Fa2//JRAGSq0SSW+eqIjCU7D1kqR2lgQD5TdC7mYRFyWGF4YQ8V573Yo0tlL/zQzt07YuyfwIWkb2+AQyZh90cFs+O7T8vg99s= + - secure: cu0TCp7HFTCOJTOwQRm1ZhWL86JawLeaUR0/Jg9ThutphfGWNV8nOfgHUG/PEwgb0jwJz34c0DdfpwCdVuDmMsqptmA9I2Lu59BZidi87dqi+PuvfaUvSM69mPABPxaTE34iFtzpWtab+KsnDd/TG87KVk/1AmK1OjA4LKBkJOqo2XkkSFgE94QM04JRSaRiRD8qiLrXTlzkRWsMh1gGnj5kOP0jozBBWbjIMU42XCd9liei7eHoh3ZmFDLOatfIwhCkadG8vBMD5nV0UDCHt2+U6+YGC8cVI+nyjXNAMuOUXZ3GowQRCtIxokVgzRINVAcmDOc40i/gfy9p7HbU3G2FC9wNo98EKecF2GjYtbQdjEgBfijN4nFYi1R/D5CMl4Qo5GtGDkRPO1E626AoHTTtfpIP6MDtjLWTrriiobW8ZuV852jyl/lWJc/O2wYho/34kSER47r5+4398F+9BUbySLzJ9f3/8eNtPIsQunrgO93/PyfkS/2O+7Amov+NPMQ0JoUQuVJt/aHsrFCgbcHR+tOeT7eYYi7mxhLHsteMbRLfCHIIkg3FDEfPITOLgArKj6l9faTJiawLMG9xMMp3Wi6SYPvosDz6gwYDr+8CHeEn0UoUltYM/msMLiGZ0t8oi2DWjvRdLQe9qfC5N77mtcsI1pWEcyc9SOwrIBs= From 494cc767af9095dd4084cd3e32d659f8ff10de11 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 25 Sep 2019 16:35:32 +0200 Subject: [PATCH 02/20] Use a separate CI job per stack Run the tests in a separate CI job per test suite and stack. Testing on all stacks in a single job exceeds Travis time limits. --- .travis.yml | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 51753c1df..b172e849e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,12 +9,8 @@ before_script: - bundle exec hatchet ci:setup script: - - docker build --pull --tag travis-build-cedar-14 --file $(pwd)/builds/cedar-14.Dockerfile . - - docker run --rm -e "STACK=cedar-14" -e "USE_STAGING_BINARIES=$USE_STAGING_BINARIES" travis-build-cedar-14 bash $TESTFOLDER - - docker build --pull --tag travis-build-heroku-16 --file $(pwd)/builds/heroku-16.Dockerfile . - - docker run --rm -e "STACK=heroku-16" -e "USE_STAGING_BINARIES=$USE_STAGING_BINARIES" travis-build-heroku-16 bash $TESTFOLDER - - docker build --pull --tag travis-build-heroku-18 --file $(pwd)/builds/heroku-18.Dockerfile . - - docker run --rm -e "STACK=heroku-18" -e "USE_STAGING_BINARIES=$USE_STAGING_BINARIES" travis-build-heroku-18 bash $TESTFOLDER + - docker build --pull --tag travis-build-$STACK --file $(pwd)/builds/$STACK.Dockerfile . + - docker run --rm -e "STACK=$STACK" -e "USE_STAGING_BINARIES=$USE_STAGING_BINARIES" travis-build-$STACK bash $TESTFOLDER jobs: include: @@ -27,12 +23,24 @@ jobs: script: "bundle exec rspec" env: matrix: - - TESTFOLDER=test/run-deps - - TESTFOLDER=test/run-deps USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging - - TESTFOLDER=test/run-versions - - TESTFOLDER=test/run-versions USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging - - TESTFOLDER=test/run-features - - TESTFOLDER=test/run-features USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=cedar-14 TESTFOLDER=test/run-deps + - STACK=cedar-14 TESTFOLDER=test/run-deps USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=cedar-14 TESTFOLDER=test/run-versions + - STACK=cedar-14 TESTFOLDER=test/run-versions USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=cedar-14 TESTFOLDER=test/run-features + - STACK=cedar-14 TESTFOLDER=test/run-features USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=heroku-16 TESTFOLDER=test/run-deps + - STACK=heroku-16 TESTFOLDER=test/run-deps USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=heroku-16 TESTFOLDER=test/run-versions + - STACK=heroku-16 TESTFOLDER=test/run-versions USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=heroku-16 TESTFOLDER=test/run-features + - STACK=heroku-16 TESTFOLDER=test/run-features USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=heroku-18 TESTFOLDER=test/run-deps + - STACK=heroku-18 TESTFOLDER=test/run-deps USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=heroku-18 TESTFOLDER=test/run-versions + - STACK=heroku-18 TESTFOLDER=test/run-versions USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging + - STACK=heroku-18 TESTFOLDER=test/run-features + - STACK=heroku-18 TESTFOLDER=test/run-features USE_STAGING_BINARIES=https://lang-python.s3.amazonaws.com/staging global: - HATCHET_RETRIES=3 - IS_RUNNING_ON_CI=true From a887282c6979f4b099bc3c04d07266df0a77e42e Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Fri, 23 Aug 2019 10:36:09 +0200 Subject: [PATCH 03/20] Allow running specific tests by passing TESTS Make it possible to specify individual tests to run, to facilitate debugging. Running the entire test suite every time can take quite long. Tests can be specified by passing the TESTS variable to `make test`. If TESTS is non-empty, the test script defines the `suite` hook to build the test suite using `suite_addTest` with each specified test function. If TESTS is unset or empty, shUnit2's default behaviour is to run all functions beginning with the word `test`. See https://github.com/kward/shunit2#-suites --- Makefile | 6 +++--- README.md | 6 ++++++ test/utils | 10 ++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index bf7c5c7cc..e3da5678f 100644 --- a/Makefile +++ b/Makefile @@ -10,17 +10,17 @@ check: test-cedar-14: @echo "Running tests in docker (cedar-14)..." - @docker run -v $(shell pwd):/buildpack:ro --rm -it -e "STACK=cedar-14" heroku/cedar:14 bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; test/run-deps; test/run-features; test/run-versions;' + @docker run -v $(shell pwd):/buildpack:ro --rm -it -e "STACK=cedar-14" -e TESTS=$(TESTS) heroku/cedar:14 bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; test/run-deps; test/run-features; test/run-versions;' @echo "" test-heroku-16: @echo "Running tests in docker (heroku-16)..." - @docker run -v $(shell pwd):/buildpack:ro --rm -it -e "STACK=heroku-16" heroku/heroku:16-build bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; test/run-deps; test/run-features; test/run-versions;' + @docker run -v $(shell pwd):/buildpack:ro --rm -it -e "STACK=heroku-16" -e TESTS=$(TESTS) heroku/heroku:16-build bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; test/run-deps; test/run-features; test/run-versions;' @echo "" test-heroku-18: @echo "Running tests in docker (heroku-18)..." - @docker run -v $(shell pwd):/buildpack:ro --rm -it -e "STACK=heroku-18" heroku/heroku:18-build bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; test/run-deps; test/run-features; test/run-versions;' + @docker run -v $(shell pwd):/buildpack:ro --rm -it -e "STACK=heroku-18" -e TESTS=$(TESTS) heroku/heroku:18-build bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; test/run-deps; test/run-features; test/run-versions;' @echo "" buildenv-heroku-16: diff --git a/README.md b/README.md index e3ea62a25..4a6fdb8d1 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,12 @@ make test-heroku-18 make test-heroku-16 ``` +You can also specify which tests to run: + +``` +make test TESTS="testPython3_7 testGitEgg" +``` + The tests are run via the vendored [shunit2](https://github.com/kward/shunit2) test framework. diff --git a/test/utils b/test/utils index ff22b7400..6b8614750 100644 --- a/test/utils +++ b/test/utils @@ -264,3 +264,13 @@ release() { assertFile() { assertEquals "$1" "$(cat ${compile_dir}/$2)" } + +# If TESTS is present in the environment, only run the specified tests. +if [ -n "$TESTS" ]; then + suite() { + for shunit_func_ in ${TESTS}; do + suite_addTest ${shunit_func_} + done + unset shunit_func_ + } +fi From cc735a6628f65b3fe77044cbeb9a8f4b30bdeb65 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 25 Sep 2019 14:32:32 +0200 Subject: [PATCH 04/20] Skip undefined functions when processing TESTS Do not attempt to invoke test functions specified via the TESTS environment variable if they are not defined. The Makefile invokes multiple test scripts and each test function is only defined in one of them. --- test/utils | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/utils b/test/utils index 6b8614750..663df14f1 100644 --- a/test/utils +++ b/test/utils @@ -269,7 +269,9 @@ assertFile() { if [ -n "$TESTS" ]; then suite() { for shunit_func_ in ${TESTS}; do - suite_addTest ${shunit_func_} + if declare -F ${shunit_func_} > /dev/null; then + suite_addTest ${shunit_func_} + fi done unset shunit_func_ } From 99d0e51309afeb5bf91edab3bb9a5f5155e9bfe8 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 25 Sep 2019 14:49:14 +0200 Subject: [PATCH 05/20] Do not run all tests when no tests match TESTS Define a noop test function when TESTS is passed and no function in the test script matches the contents of TESTS. The noop function merely prints a line notifying the user that the tests were skipped. This happens because tests were split up into multiple test scripts, but the Makefile passes TESTS to all of them. When the `suite` hook does not invoke `suite_addTest`, shunit2 falls back to running all defined tests. --- test/utils | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/utils b/test/utils index 663df14f1..e2822c311 100644 --- a/test/utils +++ b/test/utils @@ -274,5 +274,12 @@ if [ -n "$TESTS" ]; then fi done unset shunit_func_ + + if [ -z "$__shunit_suite" ]; then + testNoop() { + echo "Skipping tests because they do not match TESTS." + } + suite_addTest testNoop + fi } fi From 193af75a9dc5f60c018f0350f20d93e1b14b9586 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 21 Aug 2019 18:17:40 +0200 Subject: [PATCH 06/20] Upgrade to pip 19.2.3 Bump PIP_UPDATE from 9.0.2 to 19.2.3. This variable is used in bin/steps/python to determine which pip version to install or upgrade to. --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index 9319463af..fc3d82fd2 100755 --- a/bin/compile +++ b/bin/compile @@ -62,7 +62,7 @@ PY27="python-2.7" # Which stack is used (for binary downloading), if none is provided (e.g. outside of Heroku)? DEFAULT_PYTHON_STACK="cedar-14" # If pip doesn't match this version (the version we install), run the installer. -PIP_UPDATE="9.0.2" +PIP_UPDATE="19.2.3" export DEFAULT_PYTHON_STACK PIP_UPDATE export PY37 PY36 PY35 PY27 PY34 From c3dc702d0ecba3a2abaf54d77aa15d20bf82a6db Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Thu, 22 Aug 2019 19:31:17 +0200 Subject: [PATCH 07/20] Upgrade to pip 19.1.1 for Python 3.4 projects Python 3.4 support was dropped in pip >= 19.2. For projects still on this Python version, use pip 19.1.1 instead of pip 19.2.1. --- bin/compile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bin/compile b/bin/compile index fc3d82fd2..5a0292e5a 100755 --- a/bin/compile +++ b/bin/compile @@ -64,6 +64,19 @@ DEFAULT_PYTHON_STACK="cedar-14" # If pip doesn't match this version (the version we install), run the installer. PIP_UPDATE="19.2.3" +for file in "$BUILD_DIR/runtime.txt" "$CACHE_DIR/.heroku/python-version" ; do + [ -f "$file" ] || continue + + version=$(tr -d '[:space:]' < "$file") + + case "$version" in "$PY34"*) + # Python 3.4 support was dropped in pip >= 19.2. + PIP_UPDATE="19.1.1" + break + ;; + esac +done + export DEFAULT_PYTHON_STACK PIP_UPDATE export PY37 PY36 PY35 PY27 PY34 From 32d1e2c02b85de1ed1bb01ee8bab259eb5fd79e9 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Thu, 22 Aug 2019 14:13:45 +0200 Subject: [PATCH 08/20] Adapt pip-pop to changes in pip >= 10 The pip-diff and pip-grep tools from the vendorized `pip-pop` package import internal modules from pip. In pip >= 10, internal modules were moved under `pip._internal`, breaking the imports. Use `try...except ImportError` to handle both import paths. Also, the interface of the `PackageFinder` class from one of these modules changed. Provide a wrapper function to allow creating objects of this type using the old interface. --- vendor/pip-pop/pip-diff | 21 ++++++++++++++++++--- vendor/pip-pop/pip-grep | 22 +++++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/vendor/pip-pop/pip-diff b/vendor/pip-pop/pip-diff index 2bb1877c8..8fc7d6f98 100755 --- a/vendor/pip-pop/pip-diff +++ b/vendor/pip-pop/pip-diff @@ -12,9 +12,24 @@ Options: """ import os from docopt import docopt -from pip.req import parse_requirements -from pip.index import PackageFinder -from pip._vendor.requests import session + +try: # pip >= 10 + from pip._internal.req import parse_requirements + from pip._internal.download import PipSession as session + + def PackageFinder(find_links, index_urls, session=None): + from pip._internal.index import PackageFinder + from pip._internal.models.search_scope import SearchScope + from pip._internal.models.selection_prefs import SelectionPreferences + + search_scope = SearchScope.create(find_links, index_urls) + selection_prefs = SelectionPreferences(allow_yanked=False) + return PackageFinder.create(search_scope, selection_prefs, session=session) + +except ImportError: # pip <= 9.0.3 + from pip.req import parse_requirements + from pip.index import PackageFinder + from pip._vendor.requests import session requests = session() diff --git a/vendor/pip-pop/pip-grep b/vendor/pip-pop/pip-grep index d55000ad8..8e3cf6111 100755 --- a/vendor/pip-pop/pip-grep +++ b/vendor/pip-pop/pip-grep @@ -10,9 +10,25 @@ Options: import os import sys from docopt import docopt -from pip.req import parse_requirements -from pip.index import PackageFinder -from pip._vendor.requests import session + +try: # pip >= 10 + from pip._internal.req import parse_requirements + from pip._internal.download import PipSession as session + + def PackageFinder(find_links, index_urls, session=None): + from pip._internal.index import PackageFinder + from pip._internal.models.search_scope import SearchScope + from pip._internal.models.selection_prefs import SelectionPreferences + + search_scope = SearchScope.create(find_links, index_urls) + selection_prefs = SelectionPreferences(allow_yanked=False) + return PackageFinder.create(search_scope, selection_prefs, session=session) + +except ImportError: # pip <= 9.0.3 + from pip.req import parse_requirements + from pip.index import PackageFinder + from pip._vendor.requests import session + requests = session() From 83ffd2a80dd8144b4b6c2b4f3f657556bbdc8ba1 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Thu, 3 Oct 2019 16:47:29 +0200 Subject: [PATCH 09/20] Pin to pip 9.0.2 for pipenv users only This addresses an issue raised by @CaseyFeist during code review: Updating pip for pipenv users or requiring them to update without a heads up won't be a good experience (our version is old enough that they'll need to uninstall and reinstall pipenv locally to successfully update). If you can refactor this to stay pinned to current version for pipenv users only, I should be able to accept this (and the related project updates). https://github.com/heroku/heroku-buildpack-python/pull/833#issuecomment-537758441 --- bin/compile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/compile b/bin/compile index 5a0292e5a..db2d838bb 100755 --- a/bin/compile +++ b/bin/compile @@ -77,6 +77,11 @@ for file in "$BUILD_DIR/runtime.txt" "$CACHE_DIR/.heroku/python-version" ; do esac done +if [[ -f "$BUILD_DIR/Pipfile" ]]; then + # Do not force pipenv users to re-install pipenv locally. + PIP_UPDATE="9.0.2" +fi + export DEFAULT_PYTHON_STACK PIP_UPDATE export PY37 PY36 PY35 PY27 PY34 From 281b51d0fa66b65bab07f9fa488fcf2bbee65175 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 21 Aug 2019 19:11:57 +0200 Subject: [PATCH 10/20] Support pyproject.toml when detecting Python apps --- bin/detect | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/detect b/bin/detect index eeb965b0f..682585fc5 100755 --- a/bin/detect +++ b/bin/detect @@ -15,7 +15,7 @@ BUILD_DIR=$1 # Exit early if app is clearly not Python. -if [ ! -f "$BUILD_DIR/requirements.txt" ] && [ ! -f "$BUILD_DIR/setup.py" ] && [ ! -f "$BUILD_DIR/Pipfile" ]; then +if [ ! -f "$BUILD_DIR/requirements.txt" ] && [ ! -f "$BUILD_DIR/setup.py" ] && [ ! -f "$BUILD_DIR/Pipfile" ] && [ ! -f "$BUILD_DIR/pyproject.toml" ]; then exit 1 fi From 0bae9f0a3a2b9eb1818fb533adc0e18acc574c94 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Thu, 22 Aug 2019 13:16:08 +0200 Subject: [PATCH 11/20] Avoid editable installs for pyproject.toml-style projects --- bin/compile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index db2d838bb..6e30856b0 100755 --- a/bin/compile +++ b/bin/compile @@ -269,8 +269,13 @@ mtime "pip.uninstall.time" "${start}" # This allows for people to ship a setup.py application to Heroku # (which is rare, but I vouch that it should work!) -if [ ! -f requirements.txt ] && [ ! -f Pipfile ]; then - echo "-e ." > requirements.txt +if [ ! -f requirements.txt ] && [ ! -f Pipfile ] ; then + if [ -f pyproject.toml ] ; then + # Editable installs are not supported for pyproject.toml-style projects. + echo "." > requirements.txt + else + echo "-e ." > requirements.txt + fi fi # Fix egg-links. From b3ed58318bf3a74602617cb003d14173532b490c Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 21 Aug 2019 21:41:20 +0200 Subject: [PATCH 12/20] Add tests for pyproject.toml-style projects using poetry Add two test cases for projects using poetry as the build backend: - testPoetry: project without dependencies - testPoetryLock: project with dependencies, using a poetry.lock file --- test/fixtures/poetry-lock/foobar.py | 0 test/fixtures/poetry-lock/poetry.lock | 19 +++++++++++++++++++ test/fixtures/poetry-lock/pyproject.toml | 15 +++++++++++++++ test/fixtures/poetry/foobar.py | 0 test/fixtures/poetry/pyproject.toml | 14 ++++++++++++++ test/run-features | 10 ++++++++++ 6 files changed, 58 insertions(+) create mode 100644 test/fixtures/poetry-lock/foobar.py create mode 100644 test/fixtures/poetry-lock/poetry.lock create mode 100644 test/fixtures/poetry-lock/pyproject.toml create mode 100644 test/fixtures/poetry/foobar.py create mode 100644 test/fixtures/poetry/pyproject.toml diff --git a/test/fixtures/poetry-lock/foobar.py b/test/fixtures/poetry-lock/foobar.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/poetry-lock/poetry.lock b/test/fixtures/poetry-lock/poetry.lock new file mode 100644 index 000000000..3eb160325 --- /dev/null +++ b/test/fixtures/poetry-lock/poetry.lock @@ -0,0 +1,19 @@ +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.1.0" + +[package.extras] +dev = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface"] + +[metadata] +content-hash = "9c92739040d45f898575877e69198d92bb9477423fe80c60a14376bf1f3d010e" +python-versions = "^3.7" + +[metadata.hashes] +attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] diff --git a/test/fixtures/poetry-lock/pyproject.toml b/test/fixtures/poetry-lock/pyproject.toml new file mode 100644 index 000000000..0a3fd420a --- /dev/null +++ b/test/fixtures/poetry-lock/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.6" +attrs = "^19.1.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/test/fixtures/poetry/foobar.py b/test/fixtures/poetry/foobar.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/poetry/pyproject.toml b/test/fixtures/poetry/pyproject.toml new file mode 100644 index 000000000..4074cf7f7 --- /dev/null +++ b/test/fixtures/poetry/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.6" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/test/run-features b/test/run-features index 66fac4ea7..54b195a8e 100755 --- a/test/run-features +++ b/test/run-features @@ -70,6 +70,16 @@ testPipenvFullVersion() { assertCapturedSuccess } +testPoetry() { + compile "poetry" + assertCapturedSuccess +} + +testPoetryLock() { + compile "poetry-lock" + assertCapturedSuccess +} + testNoRequirements() { compile "no-requirements" assertCapturedError From 9cacd3fdaecf81389b2768db6b6a6fcabf354aa2 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Thu, 22 Aug 2019 18:14:10 +0200 Subject: [PATCH 13/20] Add tests for pyproject.toml-style projects using flit Add two test cases for projects using flit as the build backend: - testFlit: project without dependencies - testFlitRequires: project with dependencies --- test/fixtures/flit-requires/foobar.py | 3 +++ test/fixtures/flit-requires/pyproject.toml | 10 ++++++++++ test/fixtures/flit/foobar.py | 3 +++ test/fixtures/flit/pyproject.toml | 9 +++++++++ test/run-features | 10 ++++++++++ 5 files changed, 35 insertions(+) create mode 100644 test/fixtures/flit-requires/foobar.py create mode 100644 test/fixtures/flit-requires/pyproject.toml create mode 100644 test/fixtures/flit/foobar.py create mode 100644 test/fixtures/flit/pyproject.toml diff --git a/test/fixtures/flit-requires/foobar.py b/test/fixtures/flit-requires/foobar.py new file mode 100644 index 000000000..4b4c67818 --- /dev/null +++ b/test/fixtures/flit-requires/foobar.py @@ -0,0 +1,3 @@ +"""An amazing sample package!""" + +__version__ = '0.1' diff --git a/test/fixtures/flit-requires/pyproject.toml b/test/fixtures/flit-requires/pyproject.toml new file mode 100644 index 000000000..61cc974f1 --- /dev/null +++ b/test/fixtures/flit-requires/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["flit"] +build-backend = "flit.buildapi" + +[tool.flit.metadata] +module = "foobar" +author = "Sir Robin" +author-email = "robin@camelot.uk" +home-page = "https://github.com/sirrobin/foobar" +requires = ["attrs >=19.1.0"] diff --git a/test/fixtures/flit/foobar.py b/test/fixtures/flit/foobar.py new file mode 100644 index 000000000..4b4c67818 --- /dev/null +++ b/test/fixtures/flit/foobar.py @@ -0,0 +1,3 @@ +"""An amazing sample package!""" + +__version__ = '0.1' diff --git a/test/fixtures/flit/pyproject.toml b/test/fixtures/flit/pyproject.toml new file mode 100644 index 000000000..3dca8c957 --- /dev/null +++ b/test/fixtures/flit/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit"] +build-backend = "flit.buildapi" + +[tool.flit.metadata] +module = "foobar" +author = "Sir Robin" +author-email = "robin@camelot.uk" +home-page = "https://github.com/sirrobin/foobar" diff --git a/test/run-features b/test/run-features index 54b195a8e..5c4b1b16d 100755 --- a/test/run-features +++ b/test/run-features @@ -80,6 +80,16 @@ testPoetryLock() { assertCapturedSuccess } +testFlit() { + compile "flit" + assertCapturedSuccess +} + +testFlitRequires() { + compile "flit-requires" + assertCapturedSuccess +} + testNoRequirements() { compile "no-requirements" assertCapturedError From ddcd782b6e8f393dd8d83a0488dbe4e293d9cf56 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 28 Aug 2019 09:48:06 +0200 Subject: [PATCH 14/20] Add test for pyproject.toml-style project using setuptools --- test/fixtures/pyproject-toml/pyproject.toml | 3 +++ test/fixtures/pyproject-toml/setup.py | 4 ++++ test/run-features | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 test/fixtures/pyproject-toml/pyproject.toml create mode 100644 test/fixtures/pyproject-toml/setup.py diff --git a/test/fixtures/pyproject-toml/pyproject.toml b/test/fixtures/pyproject-toml/pyproject.toml new file mode 100644 index 000000000..864b334a8 --- /dev/null +++ b/test/fixtures/pyproject-toml/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta:__legacy__" diff --git a/test/fixtures/pyproject-toml/setup.py b/test/fixtures/pyproject-toml/setup.py new file mode 100644 index 000000000..4d986d25d --- /dev/null +++ b/test/fixtures/pyproject-toml/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + + +setup(name="foobar", version="1.0.0") diff --git a/test/run-features b/test/run-features index 5c4b1b16d..3c7b168ff 100755 --- a/test/run-features +++ b/test/run-features @@ -70,6 +70,11 @@ testPipenvFullVersion() { assertCapturedSuccess } +testPyProjectToml() { + compile "pyproject-toml" + assertCapturedSuccess +} + testPoetry() { compile "poetry" assertCapturedSuccess From 7627068717dd1e8f8fe64fbf9db9960aae6e8d4a Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Fri, 23 Aug 2019 10:10:08 +0200 Subject: [PATCH 15/20] Add failing test for poetry.lock Check that pinned requirements are honored when poetry.lock is present. Request an outdated version of marshmallow in poetry.lock (3.0.0). Check that the outdated version is installed, rather than a newer version also satisfying the version constraint in pyproject.toml (^3.0.0). --- test/fixtures/poetry-lock/poetry.lock | 21 +++++++++++---------- test/fixtures/poetry-lock/pyproject.toml | 2 +- test/run-features | 1 + 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/test/fixtures/poetry-lock/poetry.lock b/test/fixtures/poetry-lock/poetry.lock index 3eb160325..99cd77b43 100644 --- a/test/fixtures/poetry-lock/poetry.lock +++ b/test/fixtures/poetry-lock/poetry.lock @@ -1,19 +1,20 @@ [[package]] category = "main" -description = "Classes Without Boilerplate" -name = "attrs" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +name = "marshmallow" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.1.0" +python-versions = ">=3.5" +version = "3.0.0" [package.extras] -dev = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface"] +dev = ["pytest", "pytz", "simplejson", "flake8 (3.7.8)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.17,<2.0)", "tox"] +docs = ["sphinx (2.2.0)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"] +lint = ["flake8 (3.7.8)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.17,<2.0)"] +tests = ["pytest", "pytz", "simplejson"] [metadata] -content-hash = "9c92739040d45f898575877e69198d92bb9477423fe80c60a14376bf1f3d010e" -python-versions = "^3.7" +content-hash = "11b18d7787605b57a1019436950c60105518d3f3a8a60030493402031355e2f2" +python-versions = "^3.6" [metadata.hashes] -attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] +marshmallow = ["e5e9fd0c2e919b4ece915eb30808206349a49a45df72e99ed20e27a9053d574b", "fa2d8a4b61d09b0e161a14acc5ad8ab7aaaf1477f3dd52819ddd6c6c8275733a"] \ No newline at end of file diff --git a/test/fixtures/poetry-lock/pyproject.toml b/test/fixtures/poetry-lock/pyproject.toml index 0a3fd420a..85695c719 100644 --- a/test/fixtures/poetry-lock/pyproject.toml +++ b/test/fixtures/poetry-lock/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.6" -attrs = "^19.1.0" +marshmallow = "^3.0.0" [tool.poetry.dev-dependencies] diff --git a/test/run-features b/test/run-features index 3c7b168ff..e452a6cbe 100755 --- a/test/run-features +++ b/test/run-features @@ -82,6 +82,7 @@ testPoetry() { testPoetryLock() { compile "poetry-lock" + assertCaptured "marshmallow==3.0.0" assertCapturedSuccess } From 073a42309dc344d9e6dd3afecdf999bf8411d504 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Wed, 28 Aug 2019 18:09:24 +0200 Subject: [PATCH 16/20] Do not write requirements.txt for pyproject.toml-based projects Install pyproject.toml-based projects using `pip install .`. Do not use a requirements.txt for this. The requirements.txt file is needed to handle pinned versions extracted from the poetry.lock file. Note that we cannot simply append `.` to the exported requirements.txt file. Currently pip requires that either all requirements have a hash or none. Including `.` would thus force us to omit hashes for all requirements. --- bin/compile | 10 ++-------- bin/steps/pip-install | 23 ++++++++++++++++++----- bin/steps/pip-uninstall | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/bin/compile b/bin/compile index 6e30856b0..8c9c9b223 100755 --- a/bin/compile +++ b/bin/compile @@ -268,14 +268,8 @@ mtime "pip.uninstall.time" "${start}" # If no requirements.txt file given, assume `setup.py develop` is intended. # This allows for people to ship a setup.py application to Heroku # (which is rare, but I vouch that it should work!) - -if [ ! -f requirements.txt ] && [ ! -f Pipfile ] ; then - if [ -f pyproject.toml ] ; then - # Editable installs are not supported for pyproject.toml-style projects. - echo "." > requirements.txt - else - echo "-e ." > requirements.txt - fi +if [ ! -f requirements.txt ] && [ ! -f Pipfile ] && [ ! -f pyproject.toml ]; then + echo "-e ." > requirements.txt fi # Fix egg-links. diff --git a/bin/steps/pip-install b/bin/steps/pip-install index b950feff0..7be26ebbf 100755 --- a/bin/steps/pip-install +++ b/bin/steps/pip-install @@ -34,15 +34,26 @@ if [ ! "$SKIP_PIP_INSTALL" ]; then mcount "tool.pip" # Count expected build failures. - if grep -q '==0.0.0' requirements.txt; then + if [ -f requirements.txt ] && grep -q '==0.0.0' requirements.txt; then mcount "failure.none-version" fi if [ ! -f "$BUILD_DIR/.heroku/python/bin/pip" ]; then exit 1 fi - /app/.heroku/python/bin/pip install -r "$BUILD_DIR/requirements.txt" --exists-action=w --src=/app/.heroku/src --disable-pip-version-check --no-cache-dir 2>&1 | tee "$WARNINGS_LOG" | cleanup | indent - PIP_STATUS="${PIPESTATUS[0]}" + + if [ -f requirements.txt ]; then + /app/.heroku/python/bin/pip install -r "$BUILD_DIR/requirements.txt" --exists-action=w --src=/app/.heroku/src --disable-pip-version-check --no-cache-dir 2>&1 | tee "$WARNINGS_LOG" | cleanup | indent + PIP_STATUS="${PIPESTATUS[0]}" + else + PIP_STATUS=0 + fi + + if [ "$PIP_STATUS" -eq 0 ] && [ -f pyproject.toml ]; then + /app/.heroku/python/bin/pip install . --exists-action=w --disable-pip-version-check --no-cache-dir 2>&1 | tee -a "$WARNINGS_LOG" | cleanup | indent + PIP_STATUS="${PIPESTATUS[0]}" + fi + set -e show-warnings @@ -53,8 +64,10 @@ if [ ! "$SKIP_PIP_INSTALL" ]; then fi # Smart Requirements handling - cp requirements.txt .heroku/python/requirements-declared.txt - /app/.heroku/python/bin/pip freeze --disable-pip-version-check > .heroku/python/requirements-installed.txt + if [ -f requirements.txt ]; then + cp requirements.txt .heroku/python/requirements-declared.txt + /app/.heroku/python/bin/pip freeze --disable-pip-version-check > .heroku/python/requirements-installed.txt + fi echo diff --git a/bin/steps/pip-uninstall b/bin/steps/pip-uninstall index 2e1ad8db7..ed5fae0cd 100755 --- a/bin/steps/pip-uninstall +++ b/bin/steps/pip-uninstall @@ -5,7 +5,7 @@ set +e # shellcheck source=bin/utils source "$BIN_DIR/utils" -if [ ! "$SKIP_PIP_INSTALL" ]; then +if [ ! "$SKIP_PIP_INSTALL" ] && [ -f requirements.txt ]; then if [[ -f .heroku/python/requirements-declared.txt ]]; then From ebf2831fefb8ff70ebb0670b97d6bcbeee401874 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Fri, 23 Aug 2019 16:58:32 +0200 Subject: [PATCH 17/20] Install pinned dependencies from poetry.lock If a poetry.lock file is present, use the Poetry CLI to export the pinned requirements to a requirements.txt file. If the project already contains a requirements.txt, use that and ignore poetry.lock. Poetry is not used to install the project because it does not clean up stale requirements. This means that requirements need to be exported anyway, for the `pip-uninstall` step. Since we only use Poetry to export a requirements.txt file, ignore the Poetry version specified in pyproject.toml. Install a pre-release of 1.0.0 because the export command is not available before 1.0.0a0. Note that supporting pyproject.toml-based installations is not enough to handle pinned requirements: When pip installs a pyproject.toml-style project using the process described in PEP 517, it only uses Poetry to build a wheel. The wheel contains the version constraints from pyproject.toml, not the pinned versions from poetry.lock. --- bin/compile | 4 +++ bin/steps/poetry | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 bin/steps/poetry diff --git a/bin/compile b/bin/compile index 8c9c9b223..df9e79f54 100755 --- a/bin/compile +++ b/bin/compile @@ -257,6 +257,10 @@ mtime "python.install.time" "${start}" # shellcheck source=bin/steps/pipenv source "$BIN_DIR/steps/pipenv" +# Export requirements.txt from poetry.lock, if present. +# shellcheck source=bin/steps/poetry +source "$BIN_DIR/steps/poetry" + # Uninstall removed dependencies with Pip. # The buildpack will automatically remove any declared dependencies (in requirements.txt) # that were explicitly removed. This machinery is a bit complex, but it is not complicated. diff --git a/bin/steps/poetry b/bin/steps/poetry new file mode 100644 index 000000000..aed1fdc88 --- /dev/null +++ b/bin/steps/poetry @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -e + +# shellcheck source=bin/utils +source "$BIN_DIR/utils" + +if [ ! -f requirements.txt ] && [ -f pyproject.toml ] && [ -f poetry.lock ]; then + # Measure that we're using Poetry. + mcount "tool.poetry" + + # Hash poetry.lock to detect changes. + POETRY_LOCK_SHA=$(openssl dgst -sha256 poetry.lock) + + # Use cached requirements.txt if poetry.lock is unchanged. + CACHED_REQUIREMENTS=$CACHE_DIR/requirements.txt + CACHED_POETRY_LOCK_SHA=$CACHE_DIR/poetry.lock.sha256 + + if [ -f "$CACHED_REQUIREMENTS" ] && [ -f "$CACHED_POETRY_LOCK_SHA" ] && + [ "$POETRY_LOCK_SHA" == "$(cat "$CACHED_POETRY_LOCK_SHA")" ]; then + echo "Skipping requirements export, as poetry.lock hasn't changed since last deploy." | indent + cp "$CACHED_REQUIREMENTS" requirements.txt + else + # Set environment variables for pip + # This reads certain environment variables set on the Heroku app config + # and makes them accessible to the pip install process. + # + # PIP_EXTRA_INDEX_URL allows for an alternate pypi URL to be used. + if [[ -r "$ENV_DIR/PIP_EXTRA_INDEX_URL" ]]; then + PIP_EXTRA_INDEX_URL="$(cat "$ENV_DIR/PIP_EXTRA_INDEX_URL")" + export PIP_EXTRA_INDEX_URL + mcount "buildvar.PIP_EXTRA_INDEX_URL" + fi + + # Set SLUGIFY_USES_TEXT_UNIDECODE, required for Airflow versions >=1.10 + if [[ -r "$ENV_DIR/SLUGIFY_USES_TEXT_UNIDECODE" ]]; then + SLUGIFY_USES_TEXT_UNIDECODE="$(cat "$ENV_DIR/SLUGIFY_USES_TEXT_UNIDECODE")" + export SLUGIFY_USES_TEXT_UNIDECODE + mcount "buildvar.SLUGIFY_USES_TEXT_UNIDECODE" + fi + + # Install Poetry. + # + # Poetry is not used to install the project because it does not clean up + # stale requirements (see sdispater/poetry#648), so we need to export + # requirements.txt anyway for the pip-uninstall step. + # + # Since we only use Poetry to export a requirements.txt file, ignore the + # Poetry version specified in pyproject.toml. Install a pre-release of + # 1.0.0 because the export command is not available before 1.0.0a0. + export POETRY_VERSION="1.0.0b1" + puts-step "Exporting requirements with Poetry $POETRY_VERSION…" + /app/.heroku/python/bin/pip install "poetry==$POETRY_VERSION" \ + --disable-pip-version-check &> /dev/null + + # Export requirements. + /app/.heroku/python/bin/poetry export -f requirements.txt > requirements.txt + + # Write SHA and requirements.txt to cache dir. + echo "$POETRY_LOCK_SHA" > "$CACHED_POETRY_LOCK_SHA" + cp requirements.txt "$CACHED_REQUIREMENTS" + fi +fi From 22407f1b20d5d983e079be11238164919a2b82e4 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Sun, 24 Nov 2019 15:24:00 +0100 Subject: [PATCH 18/20] Upgrade to poetry 1.0.0b7 --- bin/steps/poetry | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/steps/poetry b/bin/steps/poetry index aed1fdc88..2b51ac39b 100644 --- a/bin/steps/poetry +++ b/bin/steps/poetry @@ -48,7 +48,7 @@ if [ ! -f requirements.txt ] && [ -f pyproject.toml ] && [ -f poetry.lock ]; the # Since we only use Poetry to export a requirements.txt file, ignore the # Poetry version specified in pyproject.toml. Install a pre-release of # 1.0.0 because the export command is not available before 1.0.0a0. - export POETRY_VERSION="1.0.0b1" + export POETRY_VERSION="1.0.0b7" puts-step "Exporting requirements with Poetry $POETRY_VERSION…" /app/.heroku/python/bin/pip install "poetry==$POETRY_VERSION" \ --disable-pip-version-check &> /dev/null From 3332d1b1f5ab49d23b7ba8c286827ba6fedf0cb8 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Sun, 24 Nov 2019 15:24:11 +0100 Subject: [PATCH 19/20] Use --output option instead of stream redirection --- bin/steps/poetry | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/steps/poetry b/bin/steps/poetry index 2b51ac39b..32ee98ab2 100644 --- a/bin/steps/poetry +++ b/bin/steps/poetry @@ -54,7 +54,7 @@ if [ ! -f requirements.txt ] && [ -f pyproject.toml ] && [ -f poetry.lock ]; the --disable-pip-version-check &> /dev/null # Export requirements. - /app/.heroku/python/bin/poetry export -f requirements.txt > requirements.txt + /app/.heroku/python/bin/poetry export -f requirements.txt -o requirements.txt # Write SHA and requirements.txt to cache dir. echo "$POETRY_LOCK_SHA" > "$CACHED_POETRY_LOCK_SHA" From 0cffcab93416bd381dd2fc8f544252871edba30c Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Fri, 23 Aug 2019 17:08:47 +0200 Subject: [PATCH 20/20] Detect Python version from pyproject.toml generated by Poetry If no runtime.txt is present, generate it using the pyproject.toml file generated by Poetry. The Python version is specified in the section `[tool.poetry.dependencies]` under the `python` key. We only handle a few simple cases here. If the Python version constraints are any more complicated, the user needs to provide an explicit runtime.txt. --- bin/compile | 7 ++++++ bin/steps/poetry-python-version | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 bin/steps/poetry-python-version diff --git a/bin/compile b/bin/compile index df9e79f54..174ef8e2c 100755 --- a/bin/compile +++ b/bin/compile @@ -224,6 +224,13 @@ export CACHED_PYTHON_STACK # shellcheck source=bin/steps/pipenv-python-version source "$BIN_DIR/steps/pipenv-python-version" +# Poetry Python version support. +# Detect the version of Python requested from a pyproject.toml. +# Convert it to a runtime.txt file. + +# shellcheck source=bin/steps/poetry-python-version +source "$BIN_DIR/steps/poetry-python-version" + # If no runtime was provided by the user, assume the default Python runtime version. if [ ! -f runtime.txt ]; then echo "$DEFAULT_PYTHON_VERSION" > runtime.txt diff --git a/bin/steps/poetry-python-version b/bin/steps/poetry-python-version new file mode 100644 index 000000000..6a19f987c --- /dev/null +++ b/bin/steps/poetry-python-version @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Determine Python version from pyproject.toml, if no runtime.txt is present. +if [ ! -f $BUILD_DIR/runtime.txt ] && + [ -f $BUILD_DIR/pyproject.toml ] && + [ -f $BUILD_DIR/poetry.lock ]; then + # The Python version is specified in the section [tool.poetry.dependencies] + # under the `python` key. + beg='^ *\[tool.poetry.dependencies\]' + end='^ *\[' + pat='^ *python *= *"\([^"]*\)"' + + PYTHON=$(sed -n "/$beg/,/$end/s/$pat/\\1/p" pyproject.toml) + + # We only handle a few simple cases here. If the Python version constraints + # are any more complicated, the user needs to provide an explicit + # runtime.txt. + case $PYTHON in + '^2.7' | '^2.7.'* | '~2.7' | '~2.7.'* | '2.7.*' | "${LATEST_27#python-}") + echo "$LATEST_27" > "$BUILD_DIR/runtime.txt" + ;; + + '^3.4' | '^3.4.'* | '~3.4' | '~3.4.'* | '3.4.*' | "${LATEST_34#python-}") + echo "$LATEST_34" > "$BUILD_DIR/runtime.txt" + ;; + + '^3.5' | '^3.5.'* | '~3.5' | '~3.5.'* | '3.5.*' | "${LATEST_35#python-}") + echo "$LATEST_35" > "$BUILD_DIR/runtime.txt" + ;; + + '^3.6' | '^3.6.'* | '~3.6' | '~3.6.'* | '3.6.*' | "${LATEST_36#python-}") + echo "$LATEST_36" > "$BUILD_DIR/runtime.txt" + ;; + + '^3.7' | '^3.7.'* | '~3.7' | '~3.7.'* | '3.7.*' | "${LATEST_37#python-}") + echo "$LATEST_37" > "$BUILD_DIR/runtime.txt" + ;; + esac +fi