From dc873cf5681761ae2950955b8a541514077fb2ca Mon Sep 17 00:00:00 2001 From: native-api Date: Mon, 16 Dec 2024 02:32:45 +0300 Subject: [PATCH] Support missing versions being present and set in a local .python-version (#3134) Requested in https://github.com/pyenv/pyenv/issues/2680 for deployments with a stock `.pyenv-version` that can use any of a number of Python versions and for compatibility with `uv`. * Support `pyenv local --force` * Support `pyenv-version-file-write --force` * Support `pyenv version-name --force` * Ignore missing versions when searching for executables * Display "commmand not found" even when there are nonexistent versions * exec.bats: replace `python` and `rspec` with something that doesn't exist globally, either in Ubuntu Github CI, `python` exists globally --- COMMANDS.md | 4 ++++ libexec/pyenv-exec | 4 ++-- libexec/pyenv-local | 19 ++++++++++++++++-- libexec/pyenv-version-file-write | 20 +++++++++++++++++-- libexec/pyenv-version-name | 33 ++++++++++++++++++++++++++------ libexec/pyenv-which | 1 - test/exec.bats | 14 ++++++++++---- test/local.bats | 12 ++++++++++++ test/version-file-write.bats | 9 ++++++++- test/version-name.bats | 5 +++++ test/which.bats | 18 ++++++++++++++++- 11 files changed, 120 insertions(+), 19 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 203250d4..1d634471 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -91,6 +91,10 @@ or, if you prefer 3.3.3 over 2.7.6, Python 3.3.3 +You can use the `-f/--force` flag to force setting versions even if some aren't installed. +This is mainly useful in special cases like provisioning scripts. + + ## `pyenv global` Sets the global version of Python to be used in all shells by writing diff --git a/libexec/pyenv-exec b/libexec/pyenv-exec index 251a4f7c..6ed5ac20 100755 --- a/libexec/pyenv-exec +++ b/libexec/pyenv-exec @@ -21,7 +21,7 @@ if [ "$1" = "--complete" ]; then exec pyenv-shims --short fi -PYENV_VERSION="$(pyenv-version-name)" +PYENV_VERSION="$(pyenv-version-name -f)" PYENV_COMMAND="$1" if [ -z "$PYENV_COMMAND" ]; then @@ -29,9 +29,9 @@ if [ -z "$PYENV_COMMAND" ]; then exit 1 fi -export PYENV_VERSION PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")" PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}" +export PYENV_VERSION OLDIFS="$IFS" IFS=$'\n' scripts=(`pyenv-hooks exec`) diff --git a/libexec/pyenv-local b/libexec/pyenv-local index c0389281..a44576a6 100755 --- a/libexec/pyenv-local +++ b/libexec/pyenv-local @@ -2,9 +2,11 @@ # # Summary: Set or show the local application-specific Python version(s) # -# Usage: pyenv local <..> +# Usage: pyenv local [-f|--force] [ [...]] # pyenv local --unset # +# -f/--force Do not verify that the versions being set exist +# # Sets the local application-specific Python version(s) by writing the # version name to a file named `.python-version'. # @@ -36,12 +38,25 @@ if [ "$1" = "--complete" ]; then exec pyenv-versions --bare fi +while [[ $# -gt 0 ]] +do + case "$1" in + -f|--force) + FORCE=1 + shift + ;; + *) + break + ;; + esac +done + versions=("$@") if [ "$versions" = "--unset" ]; then rm -f .python-version elif [ -n "$versions" ]; then - pyenv-version-file-write .python-version "${versions[@]}" + pyenv-version-file-write ${FORCE:+-f }.python-version "${versions[@]}" else if version_file="$(pyenv-version-file "$PWD")"; then IFS=: versions=($(pyenv-version-file-read "$version_file")) diff --git a/libexec/pyenv-version-file-write b/libexec/pyenv-version-file-write index 7095140f..db9e0f3b 100755 --- a/libexec/pyenv-version-file-write +++ b/libexec/pyenv-version-file-write @@ -1,9 +1,25 @@ #!/usr/bin/env bash -# Usage: pyenv version-file-write +# Usage: pyenv version-file-write [-f|--force] [...] +# +# -f/--force Don't verify that the versions exist set -e [ -n "$PYENV_DEBUG" ] && set -x +while [[ $# -gt 0 ]] +do + case "$1" in + -f|--force) + FORCE=1 + shift + ;; + *) + break + ;; + esac +done + + PYENV_VERSION_FILE="$1" shift || true versions=("$@") @@ -14,7 +30,7 @@ if [ -z "$versions" ] || [ -z "$PYENV_VERSION_FILE" ]; then fi # Make sure the specified version is installed. -pyenv-prefix "${versions[@]}" >/dev/null +[[ -z $FORCE ]] && pyenv-prefix "${versions[@]}" >/dev/null # Write the version out to disk. # Create an empty file. Using "rm" might cause a permission error. diff --git a/libexec/pyenv-version-name b/libexec/pyenv-version-name index b3866dbd..f7c57efb 100755 --- a/libexec/pyenv-version-name +++ b/libexec/pyenv-version-name @@ -1,8 +1,25 @@ #!/usr/bin/env bash # Summary: Show the current Python version +# +# -f/--force (Internal) If a version doesn't exist, print it as is rather than produce an error + set -e [ -n "$PYENV_DEBUG" ] && set -x +while [[ $# -gt 0 ]] +do + case "$1" in + -f|--force) + FORCE=1 + shift + ;; + *) + break + ;; + esac +done + + if [ -z "$PYENV_VERSION" ]; then PYENV_VERSION_FILE="$(pyenv-version-file)" PYENV_VERSION="$(pyenv-version-file-read "$PYENV_VERSION_FILE" || true)" @@ -33,16 +50,20 @@ OLDIFS="$IFS" # Remove the explicit 'python-' prefix from versions like 'python-3.12'. normalised_version="${version#python-}" if version_exists "${version}" || [ "$version" = "system" ]; then - versions=("${versions[@]}" "${version}") + versions+=("${version}") elif version_exists "${normalised_version}"; then - versions=("${versions[@]}" "${normalised_version}") + versions+=("${normalised_version}") elif resolved_version="$(pyenv-latest -b "${version}")"; then - versions=("${versions[@]}" "${resolved_version}") + versions+=("${resolved_version}") elif resolved_version="$(pyenv-latest -b "${normalised_version}")"; then - versions=("${versions[@]}" "${resolved_version}") + versions+=("${resolved_version}") else - echo "pyenv: version \`$version' is not installed (set by $(pyenv-version-origin))" >&2 - any_not_installed=1 + if [[ -n $FORCE ]]; then + versions+=("${normalised_version}") + else + echo "pyenv: version \`$version' is not installed (set by $(pyenv-version-origin))" >&2 + any_not_installed=1 + fi fi done } diff --git a/libexec/pyenv-which b/libexec/pyenv-which index 6f12761e..4f012596 100755 --- a/libexec/pyenv-which +++ b/libexec/pyenv-which @@ -96,7 +96,6 @@ else for version in "${nonexistent_versions[@]}"; do echo "pyenv: version \`$version' is not installed (set by $(pyenv-version-origin))" >&2 done - exit 1 fi echo "pyenv: $PYENV_COMMAND: command not found" >&2 diff --git a/test/exec.bats b/test/exec.bats index 56a0f61e..b643d31e 100644 --- a/test/exec.bats +++ b/test/exec.bats @@ -16,16 +16,22 @@ create_executable() { @test "fails with invalid version" { export PYENV_VERSION="3.4" - run pyenv-exec python -V - assert_failure "pyenv: version \`3.4' is not installed (set by PYENV_VERSION environment variable)" + run pyenv-exec nonexistent + assert_failure < .python-version - run pyenv-exec rspec - assert_failure "pyenv: version \`2.7' is not installed (set by $PWD/.python-version)" + run pyenv-exec nonexistent + assert_failure < .python-version mkdir -p "${PYENV_ROOT}/versions/1.2.3" diff --git a/test/version-file-write.bats b/test/version-file-write.bats index aa7100d6..1b62e977 100644 --- a/test/version-file-write.bats +++ b/test/version-file-write.bats @@ -9,7 +9,7 @@ setup() { @test "invocation without 2 arguments prints usage" { run pyenv-version-file-write - assert_failure "Usage: pyenv version-file-write " + assert_failure "Usage: pyenv version-file-write [-f|--force] [...]" run pyenv-version-file-write "one" "" assert_failure } @@ -21,6 +21,13 @@ setup() { assert [ ! -e ".python-version" ] } +@test "setting nonexistent version succeeds with force" { + assert [ ! -e ".python-version" ] + run pyenv-version-file-write --force ".python-version" "2.7.6" + assert_success + assert [ -e ".python-version" ] +} + @test "writes value to arbitrary file" { mkdir -p "${PYENV_ROOT}/versions/2.7.6" assert [ ! -e "my-version" ] diff --git a/test/version-name.bats b/test/version-name.bats index 720f6c86..285794e4 100644 --- a/test/version-name.bats +++ b/test/version-name.bats @@ -73,6 +73,11 @@ SH assert_failure "pyenv: version \`1.2' is not installed (set by PYENV_VERSION environment variable)" } +@test "missing version with --force" { + PYENV_VERSION=1.2 run pyenv-version-name -f + assert_success "1.2" +} + @test "one missing version (second missing)" { create_version "3.5.1" PYENV_VERSION="3.5.1:1.2" run pyenv-version-name diff --git a/test/which.bats b/test/which.bats index e6c4ee69..cdcd24e3 100644 --- a/test/which.bats +++ b/test/which.bats @@ -71,7 +71,16 @@ create_executable() { @test "version not installed" { create_executable "3.4" "py.test" PYENV_VERSION=3.3 run pyenv-which py.test - assert_failure "pyenv: version \`3.3' is not installed (set by PYENV_VERSION environment variable)" + assert_failure <