From 563bcff28eec35669406a9161250892c6dc723ee Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:50:18 -0500 Subject: [PATCH 01/35] Improve performance of pyenv-virtualenvs Use the code from pyenv-versions for efficiency and consistent output. The main performance problem was in the call to pyenv-virtualenv-prefix, which called pyenv-prefix, which then enumerated every virtual environment. This was done inside a loop, compounding the problem. Simply the virtual environment listing so that it does not have to call pyenv-virtualenv-prefix anymore. --- bin/pyenv-virtualenvs | 127 ++++++++++++++++++++++++++++++++---------- 1 file changed, 98 insertions(+), 29 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index fec12868..eae9b672 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -47,24 +47,60 @@ done versions_dir="${PYENV_ROOT}/versions" -if [ -d "$versions_dir" ]; then - versions_dir="$(realpath "$versions_dir")" +if ! enable -f "${BASH_SOURCE%/*}"/pyenv-realpath.dylib realpath 2>/dev/null; then + if [ -n "$PYENV_NATIVE_EXT" ]; then + echo "pyenv: failed to load \`realpath' builtin" >&2 + exit 1 + fi + + READLINK=$(type -P readlink) + if [ -z "$READLINK" ]; then + echo "pyenv: cannot find readlink - are you missing GNU coreutils?" >&2 + exit 1 + fi + + resolve_link() { + $READLINK "$1" + } + + realpath() { + local path="$1" + local name + + # Use a subshell to avoid changing the current path + ( + while [ -n "$path" ]; do + name="${path##*/}" + [ "$name" = "$path" ] || cd "${path%/*}" + path="$(resolve_link "$name" || true)" + done + + echo "${PWD}/$name" + ) + } fi -if [ -n "$bare" ]; then - hit_prefix="" - miss_prefix="" +if ((BASH_VERSINFO[0] > 3)); then + declare -A current_versions +else current_versions=() - unset print_origin +fi +if [ -n "$bare" ]; then include_system="" else hit_prefix="* " miss_prefix=" " OLDIFS="$IFS" - IFS=: current_versions=($(pyenv-version-name || true)) + IFS=: + if ((BASH_VERSINFO[0] > 3)); then + for i in $(pyenv-version-name || true); do + current_versions["$i"]="1" + done + else + read -r -a current_versions <<< "$(pyenv-version-name || true)" + fi IFS="$OLDIFS" - print_origin="1" - include_system="" + include_system="1" fi num_versions=0 @@ -82,35 +118,68 @@ exists() { } print_version() { - if exists "$1" "${current_versions[@]}"; then - echo "${hit_prefix}${1}${print_origin+$2}" + local version="${1:?}" + if [[ -n $bare ]]; then + echo "$version" + return + fi + local path="${2:?}" + if [[ -L "$path" ]]; then + # Only resolve the link itself for printing, do not resolve further. + # Doing otherwise would misinform the user of what the link contains. + version_repr="$version --> $(readlink "$path")" + else + version_repr="$version" + fi + if [[ ${BASH_VERSINFO[0]} -ge 4 && ${current_versions["$1"]} ]]; then + echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))" + elif (( BASH_VERSINFO[0] <= 3 )) && exists "$1" "${current_versions[@]}"; then + echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))" else - echo "${miss_prefix}${1}${print_origin+$2}" + echo "${miss_prefix}${version_repr}" fi num_versions=$((num_versions + 1)) } shopt -s dotglob shopt -s nullglob -for path in "$versions_dir"/*; do - if [ -d "$path" ]; then - if [ -n "$skip_aliases" ] && [ -L "$path" ]; then - target="$(realpath "$path")" - [ "${target%/*/envs/*}" != "$versions_dir" ] || continue +version_dir_entries=("$versions_dir"/*) +venv_dir_entries=("$versions_dir"/*/envs/*) + +if sort --version-sort /dev/null 2>&1; then + # system sort supports version sorting + OLDIFS="$IFS" + IFS='||' + + read -r -a version_dir_entries <<< "$( + printf "%s||" "${version_dir_entries[@]}" | + sort --version-sort + )" + + read -r -a venv_dir_entries <<< "$( + printf "%s||" "${venv_dir_entries[@]}" | + sort --version-sort + )" + + IFS="$OLDIFS" +fi + +if [ -z "$only_aliases" ]; then + for env_path in "${venv_dir_entries[@]}"; do + if [ -d "${env_path}" ]; then + print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" fi - virtualenv_prefix="$(pyenv-virtualenv-prefix "${path##*/}" 2>/dev/null || true)" - if [ -d "${virtualenv_prefix}" ]; then - print_version "${path##*/}" " (created from ${virtualenv_prefix})" + done +fi + +if [ -z "$skip_aliases" ]; then + for env_path in "${version_dir_entries[@]}"; do + if [ -d "${env_path}" ] && [ -L "${env_path}" ]; then + print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" fi - for venv_path in "${path}/envs/"*; do - venv="${path##*/}/envs/${venv_path##*/}" - virtualenv_prefix="$(pyenv-virtualenv-prefix "${venv}" 2>/dev/null || true)" - if [ -d "${virtualenv_prefix}" ]; then - print_version "${venv}" " (created from ${virtualenv_prefix})" - fi - done - fi -done + done +fi + shopt -u dotglob shopt -u nullglob From 97e1333197af6d92ee25c2e0cb5d30668ee61d6c Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:51:24 -0500 Subject: [PATCH 02/35] Shellcheck fixes, mostly quoting to avoid word splitting --- bin/pyenv-virtualenvs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index eae9b672..a43ece2c 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -7,7 +7,7 @@ set -e [ -n "$PYENV_DEBUG" ] && set -x -if [ -L "${BASH_SOURCE}" ]; then +if [ -L "${BASH_SOURCE[0]}" ]; then READLINK=$(type -p greadlink readlink | head -1) if [ -z "$READLINK" ]; then echo "pyenv: cannot find readlink - are you missing GNU coreutils?" >&2 @@ -16,12 +16,12 @@ if [ -L "${BASH_SOURCE}" ]; then resolve_link() { $READLINK -f "$1" } - script_path=$(resolve_link ${BASH_SOURCE}) + script_path=$(resolve_link "${BASH_SOURCE[0]}") else - script_path=${BASH_SOURCE} + script_path="${BASH_SOURCE[0]}" fi -. ${script_path%/*}/../libexec/pyenv-virtualenv-realpath +. "${script_path%/*}"/../libexec/pyenv-virtualenv-realpath if [ -z "$PYENV_ROOT" ]; then PYENV_ROOT="${HOME}/.pyenv" @@ -187,3 +187,4 @@ if [ "$num_versions" -eq 0 ] && [ -n "$include_system" ]; then echo "Warning: no Python virtualenv detected on the system" >&2 exit 1 fi + From 6d2cb280f8c27bf9c2dd6c0571e5e36f328960bd Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:57:26 -0500 Subject: [PATCH 03/35] Add --only-aliases argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is useful for listing the “frinedly” virtualenv names instead of the short path which includes the Python version. --- bin/pyenv-virtualenvs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index a43ece2c..d4b7ba00 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # Summary: List all Python virtualenvs found in `$PYENV_ROOT/versions/*'. -# Usage: pyenv virtualenvs [--bare] [--skip-aliases] +# Usage: pyenv virtualenvs [--bare] [--skip-aliases] [--only-aliases] # # List all virtualenvs found in `$PYENV_ROOT/versions/*' and its `$PYENV_ROOT/versions/envs/*'. @@ -34,10 +34,11 @@ for arg; do case "$arg" in --complete ) echo --bare - echo --skip-aliases + echo --only-aliases exit ;; --bare ) bare=1 ;; --skip-aliases ) skip_aliases=1 ;; + --only-aliases ) only_aliases=1 ;; * ) pyenv-help --usage virtualenvs >&2 exit 1 From ad14880ab2b8eb613a6f157c102e25c1a7861d02 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:59:59 -0500 Subject: [PATCH 04/35] Only show Python versions for completion when creating new virtual environments Having the current virtual environments listed as options in the competion is noisy since only bare Python versions, such as 3.11.1, make sense as suggested completions for `pyenv virtualenv [version]`. --- bin/pyenv-virtualenv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pyenv-virtualenv b/bin/pyenv-virtualenv index f1cbe3dc..69c61d17 100755 --- a/bin/pyenv-virtualenv +++ b/bin/pyenv-virtualenv @@ -24,7 +24,7 @@ fi # Provide pyenv completions if [ "$1" = "--complete" ]; then - exec pyenv-versions --bare + exec pyenv-versions --bare --skip-envs --skip-aliases fi unset PIP_REQUIRE_VENV From 39f35841f993f26f50321e33e496fb85cd720b9d Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 12:03:24 -0500 Subject: [PATCH 05/35] Only show aliases as completions for `pyenv activate` This makes the suggested completetions cleaner. --- bin/pyenv-activate | 2 +- bin/pyenv-sh-activate | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/pyenv-activate b/bin/pyenv-activate index 0f5d324b..c46eb6dd 100755 --- a/bin/pyenv-activate +++ b/bin/pyenv-activate @@ -17,7 +17,7 @@ set -e # Provide pyenv completions if [ "$1" = "--complete" ]; then echo --unset - exec pyenv-virtualenvs --bare + exec pyenv-virtualenvs --bare --only-aliases fi { printf "\x1B[31;1m" diff --git a/bin/pyenv-sh-activate b/bin/pyenv-sh-activate index 5c53b500..868bc5d7 100755 --- a/bin/pyenv-sh-activate +++ b/bin/pyenv-sh-activate @@ -51,7 +51,7 @@ while [ $# -gt 0 ]; do "--complete" ) # Provide pyenv completions echo --unset - exec pyenv-virtualenvs --bare + exec pyenv-virtualenvs --bare --only-aliases ;; "-f" | "--force" ) FORCE=1 From 6443dea0b90a5bfdcde9a247064214c57c292b6a Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 12:10:39 -0500 Subject: [PATCH 06/35] Update tests --- test/virtualenvs.bats | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 9079efb1..51bc44d4 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -4,10 +4,15 @@ load test_helper setup() { export PYENV_ROOT="${TMP}/pyenv" - mkdir -p "${PYENV_ROOT}/versions/2.7.6" - mkdir -p "${PYENV_ROOT}/versions/3.3.3" - mkdir -p "${PYENV_ROOT}/versions/venv27" - mkdir -p "${PYENV_ROOT}/versions/venv33" + mkdir -p "${PYENV_ROOT}/versions/2.7.6/envs/venv27" + mkdir -p "${PYENV_ROOT}/versions/3.3.3/envs/venv33" + ln -s "venv27" "${PYENV_ROOT}/versions/venv27" + ln -s "venv33" "${PYENV_ROOT}/versions/venv33" +} + +create_alias() { + mkdir -p "${PYENV_ROOT}/versions" + ln -s "$2" "${PYENV_ROOT}/versions/$1" } @test "list virtual environments only" { @@ -21,40 +26,40 @@ setup() { assert_success assert_output < Date: Tue, 11 Mar 2025 22:57:10 -0400 Subject: [PATCH 07/35] Add skip-aliases to complete output This was removed by accident. --- bin/pyenv-virtualenvs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index d4b7ba00..7081d325 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -34,6 +34,7 @@ for arg; do case "$arg" in --complete ) echo --bare + echo --skip-aliases echo --only-aliases exit ;; --bare ) bare=1 ;; From 36eb43ebad959e68803664137a85f2c9321fea6e Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 11 Mar 2025 23:24:28 -0400 Subject: [PATCH 08/35] Use conditional expressions For consistency, use conditional expressions instead of arithmetic evaluation when comparing bash versions. --- bin/pyenv-virtualenvs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 7081d325..46718107 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -82,7 +82,7 @@ if ! enable -f "${BASH_SOURCE%/*}"/pyenv-realpath.dylib realpath 2>/dev/null; th } fi -if ((BASH_VERSINFO[0] > 3)); then +if [[ ${BASH_VERSINFO[0]} -gt 3 ]]; then declare -A current_versions else current_versions=() @@ -94,7 +94,7 @@ else miss_prefix=" " OLDIFS="$IFS" IFS=: - if ((BASH_VERSINFO[0] > 3)); then + if [[ ${BASH_VERSINFO[0]} -gt 3 ]]; then for i in $(pyenv-version-name || true); do current_versions["$i"]="1" done @@ -135,7 +135,7 @@ print_version() { fi if [[ ${BASH_VERSINFO[0]} -ge 4 && ${current_versions["$1"]} ]]; then echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))" - elif (( BASH_VERSINFO[0] <= 3 )) && exists "$1" "${current_versions[@]}"; then + elif [[ ${BASH_VERSINFO[0]} -le 3 ]] && exists "$1" "${current_versions[@]}"; then echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))" else echo "${miss_prefix}${version_repr}" From 4d2fd5dd3efaac05c8868824679ce25e7f7bea02 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 11 Mar 2025 23:55:12 -0400 Subject: [PATCH 09/35] Remove unused code The new implementation is not using realpath. --- bin/pyenv-virtualenvs | 48 ------------------------------------------- 1 file changed, 48 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 46718107..6898f730 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -7,21 +7,6 @@ set -e [ -n "$PYENV_DEBUG" ] && set -x -if [ -L "${BASH_SOURCE[0]}" ]; then - READLINK=$(type -p greadlink readlink | head -1) - if [ -z "$READLINK" ]; then - echo "pyenv: cannot find readlink - are you missing GNU coreutils?" >&2 - exit 1 - fi - resolve_link() { - $READLINK -f "$1" - } - script_path=$(resolve_link "${BASH_SOURCE[0]}") -else - script_path="${BASH_SOURCE[0]}" -fi - -. "${script_path%/*}"/../libexec/pyenv-virtualenv-realpath if [ -z "$PYENV_ROOT" ]; then PYENV_ROOT="${HOME}/.pyenv" @@ -49,39 +34,6 @@ done versions_dir="${PYENV_ROOT}/versions" -if ! enable -f "${BASH_SOURCE%/*}"/pyenv-realpath.dylib realpath 2>/dev/null; then - if [ -n "$PYENV_NATIVE_EXT" ]; then - echo "pyenv: failed to load \`realpath' builtin" >&2 - exit 1 - fi - - READLINK=$(type -P readlink) - if [ -z "$READLINK" ]; then - echo "pyenv: cannot find readlink - are you missing GNU coreutils?" >&2 - exit 1 - fi - - resolve_link() { - $READLINK "$1" - } - - realpath() { - local path="$1" - local name - - # Use a subshell to avoid changing the current path - ( - while [ -n "$path" ]; do - name="${path##*/}" - [ "$name" = "$path" ] || cd "${path%/*}" - path="$(resolve_link "$name" || true)" - done - - echo "${PWD}/$name" - ) - } -fi - if [[ ${BASH_VERSINFO[0]} -gt 3 ]]; then declare -A current_versions else From 9ea8bb6546d0a7605fe884db2698e63bb2e5d297 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 11 Mar 2025 23:58:38 -0400 Subject: [PATCH 10/35] Use the newer [[ command https://mywiki.wooledge.org/BashFAQ/031 --- bin/pyenv-virtualenvs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 6898f730..8da8986e 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -6,9 +6,9 @@ # List all virtualenvs found in `$PYENV_ROOT/versions/*' and its `$PYENV_ROOT/versions/envs/*'. set -e -[ -n "$PYENV_DEBUG" ] && set -x +[[ -n $PYENV_DEBUG ]] && set -x -if [ -z "$PYENV_ROOT" ]; then +if [[ -z $PYENV_ROOT ]]; then PYENV_ROOT="${HOME}/.pyenv" fi @@ -39,7 +39,7 @@ if [[ ${BASH_VERSINFO[0]} -gt 3 ]]; then else current_versions=() fi -if [ -n "$bare" ]; then +if [[ -n $bare ]]; then include_system="" else hit_prefix="* " @@ -118,17 +118,17 @@ if sort --version-sort /dev/null 2>&1; then IFS="$OLDIFS" fi -if [ -z "$only_aliases" ]; then +if [[ -z $only_aliases ]]; then for env_path in "${venv_dir_entries[@]}"; do - if [ -d "${env_path}" ]; then + if [[ -d ${env_path} ]]; then print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" fi done fi -if [ -z "$skip_aliases" ]; then +if [[ -z "$skip_aliases" ]]; then for env_path in "${version_dir_entries[@]}"; do - if [ -d "${env_path}" ] && [ -L "${env_path}" ]; then + if [[ -d ${env_path} ]] && [[ -L ${env_path} ]]; then print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" fi done @@ -137,7 +137,7 @@ fi shopt -u dotglob shopt -u nullglob -if [ "$num_versions" -eq 0 ] && [ -n "$include_system" ]; then +if [[ $num_versions -eq 0 ]] && [[ -n $include_system ]]; then echo "Warning: no Python virtualenv detected on the system" >&2 exit 1 fi From d7b37871887e74dd6e04c1a1cb0ab0352c000362 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Mar 2025 00:43:18 -0400 Subject: [PATCH 11/35] Remove --only-aliases option for later discussion --- bin/pyenv-sh-activate | 2 +- bin/pyenv-virtualenvs | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/bin/pyenv-sh-activate b/bin/pyenv-sh-activate index 868bc5d7..5c53b500 100755 --- a/bin/pyenv-sh-activate +++ b/bin/pyenv-sh-activate @@ -51,7 +51,7 @@ while [ $# -gt 0 ]; do "--complete" ) # Provide pyenv completions echo --unset - exec pyenv-virtualenvs --bare --only-aliases + exec pyenv-virtualenvs --bare ;; "-f" | "--force" ) FORCE=1 diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 8da8986e..71440301 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # Summary: List all Python virtualenvs found in `$PYENV_ROOT/versions/*'. -# Usage: pyenv virtualenvs [--bare] [--skip-aliases] [--only-aliases] +# Usage: pyenv virtualenvs [--bare] [--skip-aliases] # # List all virtualenvs found in `$PYENV_ROOT/versions/*' and its `$PYENV_ROOT/versions/envs/*'. @@ -20,11 +20,9 @@ for arg; do --complete ) echo --bare echo --skip-aliases - echo --only-aliases exit ;; --bare ) bare=1 ;; --skip-aliases ) skip_aliases=1 ;; - --only-aliases ) only_aliases=1 ;; * ) pyenv-help --usage virtualenvs >&2 exit 1 @@ -118,13 +116,11 @@ if sort --version-sort /dev/null 2>&1; then IFS="$OLDIFS" fi -if [[ -z $only_aliases ]]; then - for env_path in "${venv_dir_entries[@]}"; do - if [[ -d ${env_path} ]]; then - print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" - fi - done -fi +for env_path in "${venv_dir_entries[@]}"; do + if [[ -d ${env_path} ]]; then + print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" + fi +done if [[ -z "$skip_aliases" ]]; then for env_path in "${version_dir_entries[@]}"; do From f9d1686cfa562dcf267a35bfbd04a53b1db2210f Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 15 Apr 2026 11:04:15 -0400 Subject: [PATCH 12/35] Correct test setup so aliases are properly symlinked --- test/virtualenvs.bats | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 51bc44d4..e4dd10a2 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -6,13 +6,8 @@ setup() { export PYENV_ROOT="${TMP}/pyenv" mkdir -p "${PYENV_ROOT}/versions/2.7.6/envs/venv27" mkdir -p "${PYENV_ROOT}/versions/3.3.3/envs/venv33" - ln -s "venv27" "${PYENV_ROOT}/versions/venv27" - ln -s "venv33" "${PYENV_ROOT}/versions/venv33" -} - -create_alias() { - mkdir -p "${PYENV_ROOT}/versions" - ln -s "$2" "${PYENV_ROOT}/versions/$1" + ln -s "${PYENV_ROOT}/versions/2.7.6/envs/venv27" "${PYENV_ROOT}/versions/venv27" + ln -s "${PYENV_ROOT}/versions/3.3.3/envs/venv33" "${PYENV_ROOT}/versions/venv33" } @test "list virtual environments only" { From f966c97adf875ba739442984e852cdeca30e6208 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 15 Apr 2026 12:25:18 -0400 Subject: [PATCH 13/35] Update tests and add more test cases Update the existing tests based on new behavior/output format. Add additional tests to cover scenarios that were not previously tested. --- test/virtualenvs.bats | 112 +++++++++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 23 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index e4dd10a2..8863c94a 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -10,12 +10,8 @@ setup() { ln -s "${PYENV_ROOT}/versions/3.3.3/envs/venv33" "${PYENV_ROOT}/versions/venv33" } -@test "list virtual environments only" { +@test "list virtual environments" { stub pyenv-version-name ": echo system" - stub pyenv-virtualenv-prefix "2.7.6 : false" - stub pyenv-virtualenv-prefix "3.3.3 : false" - stub pyenv-virtualenv-prefix "venv27 : echo \"${PYENV_ROOT}/versions/2.7.6\"" - stub pyenv-virtualenv-prefix "venv33 : echo \"${PYENV_ROOT}/versions/3.3.3\"" run pyenv-virtualenvs @@ -23,44 +19,114 @@ setup() { assert_output < ${PYENV_ROOT}/versions/2.7.6/envs/venv27 + venv33 --> ${PYENV_ROOT}/versions/3.3.3/envs/venv33 OUT - # unstub pyenv-version-name - # unstub pyenv-virtualenv-prefix + unstub pyenv-version-name } @test "list virtual environments with hit prefix" { - stub pyenv-version-name ": echo venv33" - stub pyenv-virtualenv-prefix "2.7.6 : false" - stub pyenv-virtualenv-prefix "3.3.3 : false" - stub pyenv-virtualenv-prefix "venv27 : echo \"${PYENV_ROOT}/versions/2.7.6\"" - stub pyenv-virtualenv-prefix "venv33 : echo \"${PYENV_ROOT}/versions/3.3.3\"" + stub pyenv-version-name ": echo 3.3.3/envs/venv33" + stub pyenv-version-origin ": echo PYENV_VERSION" run pyenv-virtualenvs assert_success assert_output < ${PYENV_ROOT}/versions/2.7.6/envs/venv27 + venv33 --> ${PYENV_ROOT}/versions/3.3.3/envs/venv33 OUT - # unstub pyenv-version-name - # unstub pyenv-virtualenv-prefix + unstub pyenv-version-name + unstub pyenv-version-origin } -@test "list virtual environments with --bare" { - stub pyenv-virtualenv-prefix "2.7.6 : false" - stub pyenv-virtualenv-prefix "3.3.3 : false" - stub pyenv-virtualenv-prefix "venv27 : echo \"${PYENV_ROOT}/versions/2.7.6\"" - stub pyenv-virtualenv-prefix "venv33 : echo \"${PYENV_ROOT}/versions/3.3.3\"" - - run pyenv-virtualenvs --bare --only-aliases +@test "list bare virtual environments" { + run pyenv-virtualenvs --bare assert_success assert_output < ${PYENV_ROOT}/versions/2.7.6/envs/venv27 (set by PYENV_VERSION) + venv33 --> ${PYENV_ROOT}/versions/3.3.3/envs/venv33 +OUT + + unstub pyenv-version-name + unstub pyenv-version-origin +} + +@test "no virtualenvs warning" { + rm -rf "${PYENV_ROOT}/versions" + mkdir -p "${PYENV_ROOT}/versions" + stub pyenv-version-name ": echo system" + + run pyenv-virtualenvs + + assert_failure + assert_output "Warning: no Python virtualenv detected on the system" - unstub pyenv-virtualenv-prefix + unstub pyenv-version-name +} + +@test "no warning with --bare and no virtualenvs" { + rm -rf "${PYENV_ROOT}/versions" + mkdir -p "${PYENV_ROOT}/versions" + + run pyenv-virtualenvs --bare + + assert_success + assert_output "" +} + +@test "completions output" { + run pyenv-virtualenvs --complete + + assert_success + assert_output < Date: Thu, 16 Apr 2026 18:27:50 +0300 Subject: [PATCH 14/35] Revert `activate` completion hint change --- bin/pyenv-activate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pyenv-activate b/bin/pyenv-activate index c46eb6dd..0f5d324b 100755 --- a/bin/pyenv-activate +++ b/bin/pyenv-activate @@ -17,7 +17,7 @@ set -e # Provide pyenv completions if [ "$1" = "--complete" ]; then echo --unset - exec pyenv-virtualenvs --bare --only-aliases + exec pyenv-virtualenvs --bare fi { printf "\x1B[31;1m" From 44c4090095afcf0ad37933fe25cff30bfd2bf2e6 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Thu, 16 Apr 2026 19:03:35 +0300 Subject: [PATCH 15/35] Refactor --- bin/pyenv-virtualenvs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 71440301..09769f10 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -83,9 +83,8 @@ print_version() { else version_repr="$version" fi - if [[ ${BASH_VERSINFO[0]} -ge 4 && ${current_versions["$1"]} ]]; then - echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))" - elif [[ ${BASH_VERSINFO[0]} -le 3 ]] && exists "$1" "${current_versions[@]}"; then + if [[ ${BASH_VERSINFO[0]} -gt 3 && ${current_versions["$1"]} ]] || \ + { [[ ${BASH_VERSINFO[0]} -le 3 ]] && exists "$1" "${current_versions[@]}"; }; then echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))" else echo "${miss_prefix}${version_repr}" From 5fd4975150de248d3acc4d536d2abeeaf28ea454 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Mon, 20 Apr 2026 12:51:59 -0400 Subject: [PATCH 16/35] Include aliases in completetions While it looks a bit funny, it is valid to use an existing virtual environment as the [version] when creating a new one. --- bin/pyenv-virtualenv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pyenv-virtualenv b/bin/pyenv-virtualenv index 69c61d17..0619f622 100755 --- a/bin/pyenv-virtualenv +++ b/bin/pyenv-virtualenv @@ -24,7 +24,7 @@ fi # Provide pyenv completions if [ "$1" = "--complete" ]; then - exec pyenv-versions --bare --skip-envs --skip-aliases + exec pyenv-versions --bare --skip-envs fi unset PIP_REQUIRE_VENV From fed3b04d2f280e843e63e0509a787da857c90feb Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Mon, 20 Apr 2026 13:51:47 -0400 Subject: [PATCH 17/35] Remove warning for no virtualenvs --- bin/pyenv-virtualenvs | 10 +--------- test/virtualenvs.bats | 13 ------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 09769f10..2a95bbba 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -37,9 +37,7 @@ if [[ ${BASH_VERSINFO[0]} -gt 3 ]]; then else current_versions=() fi -if [[ -n $bare ]]; then - include_system="" -else +if [[ -z $bare ]]; then hit_prefix="* " miss_prefix=" " OLDIFS="$IFS" @@ -131,9 +129,3 @@ fi shopt -u dotglob shopt -u nullglob - -if [[ $num_versions -eq 0 ]] && [[ -n $include_system ]]; then - echo "Warning: no Python virtualenv detected on the system" >&2 - exit 1 -fi - diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 8863c94a..3be63596 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -98,19 +98,6 @@ OUT unstub pyenv-version-origin } -@test "no virtualenvs warning" { - rm -rf "${PYENV_ROOT}/versions" - mkdir -p "${PYENV_ROOT}/versions" - stub pyenv-version-name ": echo system" - - run pyenv-virtualenvs - - assert_failure - assert_output "Warning: no Python virtualenv detected on the system" - - unstub pyenv-version-name -} - @test "no warning with --bare and no virtualenvs" { rm -rf "${PYENV_ROOT}/versions" mkdir -p "${PYENV_ROOT}/versions" From 5ac3b3a1d952004ea22569961d955171fd54203f Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 23 Apr 2026 17:28:15 -0400 Subject: [PATCH 18/35] Check for existence of venv directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only call pyenv-prefix if the path doesn’t exist. --- bin/pyenv-virtualenv-prefix | 9 ++++++++- bin/pyenv-virtualenvs | 4 +--- test/virtualenvs.bats | 25 ++++++++++++++----------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/bin/pyenv-virtualenv-prefix b/bin/pyenv-virtualenv-prefix index bbe58719..b46077fc 100755 --- a/bin/pyenv-virtualenv-prefix +++ b/bin/pyenv-virtualenv-prefix @@ -49,7 +49,14 @@ for version in "${versions[@]}"; do echo "pyenv-virtualenv: version \`${version}' is not a virtualenv" 1>&2 exit 1 fi - PYENV_PREFIX_PATH="$(pyenv-prefix "${version}")" + + # Calling pyenv-prefix is expensive. Only do so if the path doesn't exist. + # See https://github.com/pyenv/pyenv-virtualenv/pull/502 + PYENV_PREFIX_PATH="${PYENV_ROOT}/versions/${version}" + if [[ ! -d "$PYENV_PREFIX_PATH" ]]; then + PYENV_PREFIX_PATH="$(pyenv-prefix "${version}")" + fi + if [ -x "${PYENV_PREFIX_PATH}/bin/python" ]; then if [ -f "${PYENV_PREFIX_PATH}/bin/activate" ]; then if [ -f "${PYENV_PREFIX_PATH}/bin/conda" ]; then diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 2a95bbba..b7fa0965 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -75,11 +75,9 @@ print_version() { fi local path="${2:?}" if [[ -L "$path" ]]; then - # Only resolve the link itself for printing, do not resolve further. - # Doing otherwise would misinform the user of what the link contains. version_repr="$version --> $(readlink "$path")" else - version_repr="$version" + version_repr="$version (created from $(pyenv-virtualenv-prefix "$version" 2>/dev/null))" fi if [[ ${BASH_VERSINFO[0]} -gt 3 && ${current_versions["$1"]} ]] || \ { [[ ${BASH_VERSINFO[0]} -le 3 ]] && exists "$1" "${current_versions[@]}"; }; then diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 3be63596..793eaee6 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -4,9 +4,12 @@ load test_helper setup() { export PYENV_ROOT="${TMP}/pyenv" - mkdir -p "${PYENV_ROOT}/versions/2.7.6/envs/venv27" - mkdir -p "${PYENV_ROOT}/versions/3.3.3/envs/venv33" + setup_m_venv "2.7.6/envs/venv27" + echo "home = ${PYENV_ROOT}/versions/2.7.6/bin" > "${PYENV_ROOT}/versions/2.7.6/envs/venv27/pyvenv.cfg" ln -s "${PYENV_ROOT}/versions/2.7.6/envs/venv27" "${PYENV_ROOT}/versions/venv27" + + setup_m_venv "3.3.3/envs/venv33" + echo "home = ${PYENV_ROOT}/versions/3.3.3/bin" > "${PYENV_ROOT}/versions/3.3.3/envs/venv33/pyvenv.cfg" ln -s "${PYENV_ROOT}/versions/3.3.3/envs/venv33" "${PYENV_ROOT}/versions/venv33" } @@ -17,8 +20,8 @@ setup() { assert_success assert_output < ${PYENV_ROOT}/versions/2.7.6/envs/venv27 venv33 --> ${PYENV_ROOT}/versions/3.3.3/envs/venv33 OUT @@ -34,8 +37,8 @@ OUT assert_success assert_output < ${PYENV_ROOT}/versions/2.7.6/envs/venv27 venv33 --> ${PYENV_ROOT}/versions/3.3.3/envs/venv33 OUT @@ -73,8 +76,8 @@ OUT assert_success assert_output < ${PYENV_ROOT}/versions/2.7.6/envs/venv27 (set by PYENV_VERSION) venv33 --> ${PYENV_ROOT}/versions/3.3.3/envs/venv33 OUT @@ -98,7 +101,7 @@ OUT unstub pyenv-version-origin } -@test "no warning with --bare and no virtualenvs" { +@test "no output with --bare and no virtualenvs" { rm -rf "${PYENV_ROOT}/versions" mkdir -p "${PYENV_ROOT}/versions" From c483e44cb4a766796012cc5031618870eff7619c Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 23 Apr 2026 18:02:22 -0400 Subject: [PATCH 19/35] Fix incorrect IFS handling The echo "${VIRTUALENV_PREFIX_PATHS[*]}" statement at the end of the file only worked by accident because IFS=: was set earlier in the file. Properly set/unset IFS. --- bin/pyenv-virtualenv-prefix | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bin/pyenv-virtualenv-prefix b/bin/pyenv-virtualenv-prefix index b46077fc..c565e6f9 100755 --- a/bin/pyenv-virtualenv-prefix +++ b/bin/pyenv-virtualenv-prefix @@ -26,13 +26,17 @@ if [ -z "$PYENV_ROOT" ]; then PYENV_ROOT="${HOME}/.pyenv" fi +OLDIFS="$IFS" +IFS=: if [ -n "$1" ]; then - versions=($@) - IFS=: PYENV_VERSION="${versions[*]}" + # $@ is not affected by IFS + versions=("$@") + PYENV_VERSION="${versions[*]}" export PYENV_VERSION else - IFS=: versions=($(pyenv-version-name)) + versions=($(pyenv-version-name)) fi +IFS="$OLDIFS" append_virtualenv_prefix() { if [ -d "${VIRTUALENV_PREFIX_PATH}" ]; then @@ -101,4 +105,7 @@ for version in "${versions[@]}"; do fi done -IFS=: echo "${VIRTUALENV_PREFIX_PATHS[*]}" +OLDIFS="$IFS" +IFS=: +echo "${VIRTUALENV_PREFIX_PATHS[*]}" +IFS="$OLDIFS" From d8c1a67ed0fc27d756fa187d9bce4fc34a8ce9b2 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Fri, 24 Apr 2026 09:06:40 -0400 Subject: [PATCH 20/35] Remove pyenv-prefix stubs from tests This refelects how call changes have changed in the code. --- test/conda-prefix.bats | 4 ---- test/prefix.bats | 21 --------------------- 2 files changed, 25 deletions(-) diff --git a/test/conda-prefix.bats b/test/conda-prefix.bats index 6965d527..4c2436ac 100644 --- a/test/conda-prefix.bats +++ b/test/conda-prefix.bats @@ -9,7 +9,6 @@ setup() { @test "display conda root" { setup_conda "anaconda-2.3.0" stub pyenv-version-name "echo anaconda-2.3.0" - stub pyenv-prefix "anaconda-2.3.0 : echo \"${PYENV_ROOT}/versions/anaconda-2.3.0\"" PYENV_VERSION="anaconda-2.3.0" run pyenv-virtualenv-prefix @@ -19,14 +18,12 @@ ${PYENV_ROOT}/versions/anaconda-2.3.0 OUT unstub pyenv-version-name - unstub pyenv-prefix teardown_conda "anaconda-2.3.0" } @test "display conda env" { setup_conda "anaconda-2.3.0" "foo" stub pyenv-version-name "echo anaconda-2.3.0/envs/foo" - stub pyenv-prefix "anaconda-2.3.0/envs/foo : echo \"${PYENV_ROOT}/versions/anaconda-2.3.0/envs/foo\"" PYENV_VERSION="anaconda-2.3.0/envs/foo" run pyenv-virtualenv-prefix @@ -36,6 +33,5 @@ ${PYENV_ROOT}/versions/anaconda-2.3.0/envs/foo OUT unstub pyenv-version-name - unstub pyenv-prefix teardown_conda "anaconda-2.3.0" "foo" } diff --git a/test/prefix.bats b/test/prefix.bats index a21c3f93..7a1d50f9 100644 --- a/test/prefix.bats +++ b/test/prefix.bats @@ -72,7 +72,6 @@ remove_conda() { @test "display prefix of virtualenv created by virtualenv" { stub pyenv-version-name "echo foo" - stub pyenv-prefix "foo : echo \"${PYENV_ROOT}/versions/foo\"" create_virtualenv "foo" "2.7.11" PYENV_VERSION="foo" run pyenv-virtualenv-prefix @@ -83,13 +82,11 @@ ${PYENV_ROOT}/versions/2.7.11 OUT unstub pyenv-version-name - unstub pyenv-prefix remove_virtualenv "foo" "2.7.11" } @test "display prefix of virtualenv created by virtualenv (pypy)" { stub pyenv-version-name "echo foo" - stub pyenv-prefix "foo : echo \"${PYENV_ROOT}/versions/foo\"" create_virtualenv_pypy "foo" "pypy-4.0.1" PYENV_VERSION="foo" run pyenv-virtualenv-prefix @@ -100,13 +97,11 @@ ${PYENV_ROOT}/versions/pypy-4.0.1 OUT unstub pyenv-version-name - unstub pyenv-prefix remove_virtualenv "foo" "pypy-4.0.1" } @test "display prefix of virtualenv created by virtualenv (jython)" { stub pyenv-version-name "echo foo" - stub pyenv-prefix "foo : echo \"${PYENV_ROOT}/versions/foo\"" create_virtualenv_jython "foo" "jython-2.7.0" PYENV_VERSION="foo" run pyenv-virtualenv-prefix @@ -117,14 +112,11 @@ ${PYENV_ROOT}/versions/jython-2.7.0 OUT unstub pyenv-version-name - unstub pyenv-prefix remove_virtualenv "foo" "jython-2.7.0" } @test "display prefixes of virtualenv created by virtualenv" { stub pyenv-version-name "echo foo:bar" - stub pyenv-prefix "foo : echo \"${PYENV_ROOT}/versions/foo\"" \ - "bar : echo \"${PYENV_ROOT}/versions/bar\"" create_virtualenv "foo" "2.7.11" create_virtualenv "bar" "3.5.1" @@ -136,14 +128,12 @@ ${PYENV_ROOT}/versions/2.7.11:${PYENV_ROOT}/versions/3.5.1 OUT unstub pyenv-version-name - unstub pyenv-prefix remove_virtualenv "foo" "2.7.11" remove_virtualenv "bar" "3.5.1" } @test "display prefix of virtualenv created by venv" { stub pyenv-version-name "echo foo" - stub pyenv-prefix "foo : echo \"${PYENV_ROOT}/versions/foo\"" create_m_venv "foo" "3.3.6" PYENV_VERSION="foo" run pyenv-virtualenv-prefix @@ -154,14 +144,11 @@ ${PYENV_ROOT}/versions/3.3.6 OUT unstub pyenv-version-name - unstub pyenv-prefix remove_m_venv "foo" "3.3.6" } @test "display prefixes of virtualenv created by venv" { stub pyenv-version-name "echo foo:bar" - stub pyenv-prefix "foo : echo \"${PYENV_ROOT}/versions/foo\"" \ - "bar : echo \"${PYENV_ROOT}/versions/bar\"" create_m_venv "foo" "3.3.6" create_m_venv "bar" "3.4.4" @@ -173,14 +160,12 @@ ${PYENV_ROOT}/versions/3.3.6:${PYENV_ROOT}/versions/3.4.4 OUT unstub pyenv-version-name - unstub pyenv-prefix remove_m_venv "foo" "3.3.6" remove_m_venv "bar" "3.4.4" } @test "display prefix of virtualenv created by conda" { stub pyenv-version-name "echo miniconda3-3.16.0/envs/foo" - stub pyenv-prefix "miniconda3-3.16.0/envs/foo : echo \"${PYENV_ROOT}/versions/miniconda3-3.16.0/envs/foo\"" create_conda "miniconda3-3.16.0/envs/foo" "miniconda3-3.16.0" PYENV_VERSION="miniconda3-3.16.0/envs/foo" run pyenv-virtualenv-prefix @@ -191,7 +176,6 @@ ${PYENV_ROOT}/versions/miniconda3-3.16.0/envs/foo OUT unstub pyenv-version-name - unstub pyenv-prefix remove_conda "miniconda3-3.16.0/envs/foo" "miniconda3-3.16.0" } @@ -210,7 +194,6 @@ OUT @test "should fail if the version is not a virtualenv" { stub pyenv-version-name "echo 3.4.4" - stub pyenv-prefix "3.4.4 : echo \"${PYENV_ROOT}/versions/3.4.4\"" create_version "3.4.4" PYENV_VERSION="3.4.4" run pyenv-virtualenv-prefix @@ -221,14 +204,11 @@ pyenv-virtualenv: version \`3.4.4' is not a virtualenv OUT unstub pyenv-version-name - unstub pyenv-prefix remove_version "3.4.4" } @test "should fail if one of the versions is not a virtualenv" { stub pyenv-version-name "echo venv33:3.4.4" - stub pyenv-prefix "venv33 : echo \"${PYENV_ROOT}/versions/venv33\"" \ - "3.4.4 : echo \"${PYENV_ROOT}/versions/3.4.4\"" create_virtualenv "venv33" "3.3.6" create_version "3.4.4" @@ -240,7 +220,6 @@ pyenv-virtualenv: version \`3.4.4' is not a virtualenv OUT unstub pyenv-version-name - unstub pyenv-prefix remove_virtualenv "venv33" "3.3.6" remove_version "3.4.4" } From 3546afd705d68d76628916d43df7da6196ec1bfa Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Fri, 24 Apr 2026 16:33:19 +0300 Subject: [PATCH 21/35] Add a test for version resolution --- test/prefix.bats | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/prefix.bats b/test/prefix.bats index 7a1d50f9..38e069d1 100644 --- a/test/prefix.bats +++ b/test/prefix.bats @@ -223,3 +223,22 @@ OUT remove_virtualenv "venv33" "3.3.6" remove_version "3.4.4" } + +@test "resolves a version that is not an exact match" { + stub pyenv-version-name "echo 3" + create_version "3.4.4" + + stub pyenv-prefix "3 : echo $PYENV_ROOT/versions/3.4.4" + + run pyenv-virtualenv-prefix + + assert_failure + assert_output < Date: Fri, 24 Apr 2026 16:51:08 +0300 Subject: [PATCH 22/35] fix test in case of path with spaces --- test/prefix.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/prefix.bats b/test/prefix.bats index 38e069d1..eac95aa4 100644 --- a/test/prefix.bats +++ b/test/prefix.bats @@ -228,7 +228,7 @@ OUT stub pyenv-version-name "echo 3" create_version "3.4.4" - stub pyenv-prefix "3 : echo $PYENV_ROOT/versions/3.4.4" + stub pyenv-prefix "3 : echo \"$PYENV_ROOT/versions/3.4.4\"" run pyenv-virtualenv-prefix From 6fb433cc6db9fbc8cd88e4bcc367469a63fde333 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Fri, 24 Apr 2026 17:30:30 +0300 Subject: [PATCH 23/35] - dead code --- bin/pyenv-virtualenvs | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index b7fa0965..4db819d0 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -50,7 +50,6 @@ if [[ -z $bare ]]; then read -r -a current_versions <<< "$(pyenv-version-name || true)" fi IFS="$OLDIFS" - include_system="1" fi num_versions=0 From b52417ea5e5b8dbd6d0055f394d9e982ae30c5a8 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Fri, 24 Apr 2026 11:28:26 -0400 Subject: [PATCH 24/35] Remove unnecessary test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The warning was removed, so testing no empty output doesn’t make much sense. --- test/virtualenvs.bats | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 793eaee6..f4dc7d99 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -101,16 +101,6 @@ OUT unstub pyenv-version-origin } -@test "no output with --bare and no virtualenvs" { - rm -rf "${PYENV_ROOT}/versions" - mkdir -p "${PYENV_ROOT}/versions" - - run pyenv-virtualenvs --bare - - assert_success - assert_output "" -} - @test "completions output" { run pyenv-virtualenvs --complete From a105a58a1b7d635bc4924de898c16872f4f5e783 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Fri, 24 Apr 2026 12:04:16 -0400 Subject: [PATCH 25/35] Add a helper for creating virtual environments --- test/virtualenvs.bats | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index f4dc7d99..46578b84 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -2,15 +2,22 @@ load test_helper +create_m_venv() { + local version="$1" + local venv="$2" + local version_dir="${PYENV_ROOT}/versions/${version}" + local env_dir="${version_dir}/envs/${venv}" + echo "home = ${version_dir}/bin" > "${env_dir}/pyvenv.cfg" + ln -s "${env_dir}" "${PYENV_ROOT}/versions/${venv}" +} + setup() { export PYENV_ROOT="${TMP}/pyenv" setup_m_venv "2.7.6/envs/venv27" - echo "home = ${PYENV_ROOT}/versions/2.7.6/bin" > "${PYENV_ROOT}/versions/2.7.6/envs/venv27/pyvenv.cfg" - ln -s "${PYENV_ROOT}/versions/2.7.6/envs/venv27" "${PYENV_ROOT}/versions/venv27" + create_m_venv "2.7.6" "venv27" setup_m_venv "3.3.3/envs/venv33" - echo "home = ${PYENV_ROOT}/versions/3.3.3/bin" > "${PYENV_ROOT}/versions/3.3.3/envs/venv33/pyvenv.cfg" - ln -s "${PYENV_ROOT}/versions/3.3.3/envs/venv33" "${PYENV_ROOT}/versions/venv33" + create_m_venv "3.3.3" "venv33" } @test "list virtual environments" { From 7e89747ae8cc411c1b68d8a6022f225f76faeaf3 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Fri, 24 Apr 2026 13:15:50 -0400 Subject: [PATCH 26/35] Change the interface of create_m_venv to match setup_m_venv Also have create_m_venv call setup_m_venv. --- test/virtualenvs.bats | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 46578b84..9f667829 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -3,21 +3,22 @@ load test_helper create_m_venv() { - local version="$1" - local venv="$2" + setup_m_venv "$1" + + local version="${1%%/envs/*}" # Get the first part of "2.7.6/envs/venv27" + local venv="${1##*/}" # Get the last part of "2.7.6/envs/venv27" + local version_dir="${PYENV_ROOT}/versions/${version}" local env_dir="${version_dir}/envs/${venv}" + echo "home = ${version_dir}/bin" > "${env_dir}/pyvenv.cfg" ln -s "${env_dir}" "${PYENV_ROOT}/versions/${venv}" } setup() { export PYENV_ROOT="${TMP}/pyenv" - setup_m_venv "2.7.6/envs/venv27" - create_m_venv "2.7.6" "venv27" - - setup_m_venv "3.3.3/envs/venv33" - create_m_venv "3.3.3" "venv33" + create_m_venv "2.7.6/envs/venv27" + create_m_venv "3.3.3/envs/venv33" } @test "list virtual environments" { From 19efcdcd808f6981cf67e1c7ae138e366d6f58f5 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Fri, 24 Apr 2026 16:02:44 -0400 Subject: [PATCH 27/35] Show venvs created from system Python Venvs created from system Python are directories, not sym links. The previous code only displayed venvs that were a directory and a symlink. Now check for directory existence and symlink status, or directory existence and the presence of bin/activate within the directory. --- bin/pyenv-virtualenvs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 4db819d0..6186714f 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -116,13 +116,17 @@ for env_path in "${venv_dir_entries[@]}"; do fi done -if [[ -z "$skip_aliases" ]]; then - for env_path in "${version_dir_entries[@]}"; do - if [[ -d ${env_path} ]] && [[ -L ${env_path} ]]; then +for env_path in "${version_dir_entries[@]}"; do + if [[ -d ${env_path} ]]; then + if [[ -L ${env_path} ]]; then + if [[ -z $skip_aliases ]]; then + print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" + fi + elif [[ -f "${env_path}/bin/activate" ]]; then print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" fi - done -fi + fi +done shopt -u dotglob shopt -u nullglob From 5e134e155c12746712bc75af5d3c03c4b3821f03 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Fri, 24 Apr 2026 16:03:16 -0400 Subject: [PATCH 28/35] Remove counter that is no longer used This was only used by the warning code, which was removed. --- bin/pyenv-virtualenvs | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 6186714f..33b106cb 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -52,8 +52,6 @@ if [[ -z $bare ]]; then IFS="$OLDIFS" fi -num_versions=0 - exists() { local car="$1" local cdar @@ -84,7 +82,6 @@ print_version() { else echo "${miss_prefix}${version_repr}" fi - num_versions=$((num_versions + 1)) } shopt -s dotglob From ba8598a25dd065a7f86f68f6bd6b8daa8c9b4c44 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Mon, 27 Apr 2026 18:06:06 -0400 Subject: [PATCH 29/35] Add setup function for creating a mock system venv Change the main setup to only set PYENV_ROOT. Each test now sets up the required virtual environments. This results in some duplication but makes it clearer what each test is doing. An alternative approach would be to modify the venv created by create_m_venv to make it look like a system venv, but two distinct functions make it much clearer what they are doing even though there is some overlap between the functions. --- test/virtualenvs.bats | 81 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 9f667829..fecff334 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -2,28 +2,69 @@ load test_helper -create_m_venv() { - setup_m_venv "$1" - - local version="${1%%/envs/*}" # Get the first part of "2.7.6/envs/venv27" - local venv="${1##*/}" # Get the last part of "2.7.6/envs/venv27" +create_m_system_venv() { + # Create a mock virtual environment in the same way a venv created from + # the system Python version looks. + # + # The venv is in ${PYENV_ROOT}/versions, is not a symlink, and + # home is /usr/bin in the pyenv.cfg. + create_executable "$2" "python" + create_executable "$2" "activate" + + local version="$1" + local venv="$2" + local venv_dir="${PYENV_ROOT}/versions/${venv}" + + echo "home = /usr/bin" > "${venv_dir}/pyvenv.cfg" +} - local version_dir="${PYENV_ROOT}/versions/${version}" - local env_dir="${version_dir}/envs/${venv}" - echo "home = ${version_dir}/bin" > "${env_dir}/pyvenv.cfg" - ln -s "${env_dir}" "${PYENV_ROOT}/versions/${venv}" +create_m_venv() { + # Create a mock virtual environment from an installed (not system) Python version. + # + # The venv name is a symlink inside ${PYENV_ROOT}/versions that points to + # the real venv directory inside the Python version used to created and + # home points to the bin directory inside the venv dir. + setup_m_venv "$1/envs/$2" + + local version="$1" + local venv="$2" + local venv_dir="${PYENV_ROOT}/versions/${version}/envs/${venv}" + + echo "home = ${PYENV_ROOT}/versions/${version}/bin" > "${venv_dir}/pyvenv.cfg" + ln -s "${venv_dir}" "${PYENV_ROOT}/versions/${venv}" } setup() { export PYENV_ROOT="${TMP}/pyenv" - create_m_venv "2.7.6/envs/venv27" - create_m_venv "3.3.3/envs/venv33" +} + +@test "system venv" { + stub pyenv-version-name ": echo venv314" + stub pyenv-version-origin ": echo PYENV_VERSION" + + create_m_venv "3.14.3" "venv314" + create_m_system_venv "3.9.11" "system_venv" + + run pyenv-virtualenvs + + assert_success + assert_output < ${PYENV_ROOT}/versions/3.14.3/envs/venv314 (set by PYENV_VERSION) +OUT + + unstub pyenv-version-name + } @test "list virtual environments" { stub pyenv-version-name ": echo system" + create_m_venv "2.7.6" "venv27" + create_m_venv "3.3.3" "venv33" + run pyenv-virtualenvs assert_success @@ -41,6 +82,9 @@ OUT stub pyenv-version-name ": echo 3.3.3/envs/venv33" stub pyenv-version-origin ": echo PYENV_VERSION" + create_m_venv "2.7.6" "venv27" + create_m_venv "3.3.3" "venv33" + run pyenv-virtualenvs assert_success @@ -56,6 +100,9 @@ OUT } @test "list bare virtual environments" { + create_m_venv "2.7.6" "venv27" + create_m_venv "3.3.3" "venv33" + run pyenv-virtualenvs --bare assert_success @@ -68,6 +115,9 @@ OUT } @test "list bare virtual environments without aliases" { + create_m_venv "2.7.6" "venv27" + create_m_venv "3.3.3" "venv33" + run pyenv-virtualenvs --bare --skip-aliases assert_success @@ -80,6 +130,9 @@ OUT @test "list virtual environments without aliases" { stub pyenv-version-name ": echo system" + create_m_venv "2.7.6" "venv27" + create_m_venv "3.3.3" "venv33" + run pyenv-virtualenvs --skip-aliases assert_success @@ -95,6 +148,9 @@ OUT stub pyenv-version-name ": echo venv27" stub pyenv-version-origin ": echo PYENV_VERSION" + create_m_venv "2.7.6" "venv27" + create_m_venv "3.3.3" "venv33" + run pyenv-virtualenvs assert_success @@ -110,6 +166,9 @@ OUT } @test "completions output" { + create_m_venv "2.7.6" "venv27" + create_m_venv "3.3.3" "venv33" + run pyenv-virtualenvs --complete assert_success From c6cb3ede68a1a022bcb54730ccf07512b4534e98 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 28 Apr 2026 15:35:30 -0400 Subject: [PATCH 30/35] Force creation of symlink if it exists This makes test setup more resilient. --- test/virtualenvs.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index fecff334..1c698aab 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -32,7 +32,7 @@ create_m_venv() { local venv_dir="${PYENV_ROOT}/versions/${version}/envs/${venv}" echo "home = ${PYENV_ROOT}/versions/${version}/bin" > "${venv_dir}/pyvenv.cfg" - ln -s "${venv_dir}" "${PYENV_ROOT}/versions/${venv}" + ln -sf "${venv_dir}" "${PYENV_ROOT}/versions/${venv}" } setup() { From 6391e0ae280abc3483a54ac70db1215980405f54 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 28 Apr 2026 15:41:41 -0400 Subject: [PATCH 31/35] Add a performance regression test --- test/virtualenvs.bats | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 1c698aab..a3ccad1a 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -78,6 +78,31 @@ OUT unstub pyenv-version-name } +@test "list virtual environments performance" { + local count index start end elapsed max + + count=180 + max_duration=2 + + # Create venvs + for i in $(seq 1 $count); do + create_m_venv "3.14.3" "venv-${i}" + done + + # List virtual environments and record the execution time. + # Run pyenv-virtualenvs without any flags because this is the most + # expensive operation. + start=$(date +%s) + run pyenv-virtualenvs + end=$(date +%s) + elapsed=$((end - start)) + + echo "# Listed $count venvs in ${elapsed}s." >&3 + + assert_success + assert [ "$elapsed" -le "$max_duration" ] +} + @test "list virtual environments with hit prefix" { stub pyenv-version-name ": echo 3.3.3/envs/venv33" stub pyenv-version-origin ": echo PYENV_VERSION" From 372737b30ec85842086c3b5db9dea24c2befaf12 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 28 Apr 2026 17:11:31 -0400 Subject: [PATCH 32/35] Add a few missed unstub calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also stub out calls to pyenv-virtualenv-prefix in the test that’s listing venvs. --- test/virtualenvs.bats | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index a3ccad1a..5985f7cf 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -56,11 +56,13 @@ setup() { OUT unstub pyenv-version-name - + unstub pyenv-version-origin } @test "list virtual environments" { stub pyenv-version-name ": echo system" + stub pyenv-virtualenv-prefix "2.7.6/envs/venv27 : echo \"${PYENV_ROOT}/versions/2.7.6\"" + stub pyenv-virtualenv-prefix "3.3.3/envs/venv33 : echo \"${PYENV_ROOT}/versions/3.3.3\"" create_m_venv "2.7.6" "venv27" create_m_venv "3.3.3" "venv33" @@ -76,6 +78,7 @@ OUT OUT unstub pyenv-version-name + unstub pyenv-virtualenv-prefix } @test "list virtual environments performance" { From 34531d40ba04d6cf7ed4bf111ca9203215576e42 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 28 Apr 2026 17:21:08 -0400 Subject: [PATCH 33/35] Remove performance test Making this an integration test ended up giving mixed results. This test when run without the performance changes in this branch did not reflect the performance gains seen when running pyenv-virtualenvs outside of the test environment. The test was not accomplishing its goal. --- test/virtualenvs.bats | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/test/virtualenvs.bats b/test/virtualenvs.bats index 5985f7cf..b4b3a0bb 100644 --- a/test/virtualenvs.bats +++ b/test/virtualenvs.bats @@ -81,31 +81,6 @@ OUT unstub pyenv-virtualenv-prefix } -@test "list virtual environments performance" { - local count index start end elapsed max - - count=180 - max_duration=2 - - # Create venvs - for i in $(seq 1 $count); do - create_m_venv "3.14.3" "venv-${i}" - done - - # List virtual environments and record the execution time. - # Run pyenv-virtualenvs without any flags because this is the most - # expensive operation. - start=$(date +%s) - run pyenv-virtualenvs - end=$(date +%s) - elapsed=$((end - start)) - - echo "# Listed $count venvs in ${elapsed}s." >&3 - - assert_success - assert [ "$elapsed" -le "$max_duration" ] -} - @test "list virtual environments with hit prefix" { stub pyenv-version-name ": echo 3.3.3/envs/venv33" stub pyenv-version-origin ": echo PYENV_VERSION" From 006e79891b51e0c2dcc50d079ad176c4bedbde88 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Wed, 29 Apr 2026 00:29:31 +0300 Subject: [PATCH 34/35] test_prefix.bats: - remove_* no longer needed now that PYENV_ROOT is under BATS_TEST_DIRNAME --- test/prefix.bats | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/test/prefix.bats b/test/prefix.bats index eac95aa4..51441099 100644 --- a/test/prefix.bats +++ b/test/prefix.bats @@ -12,10 +12,6 @@ create_version() { chmod +x "${PYENV_ROOT}/versions/$1/bin/python" } -remove_version() { - rm -fr "${PYENV_ROOT}/versions/$1" -} - create_virtualenv() { create_version "$1" create_version "${2:-$1}" @@ -40,11 +36,6 @@ create_virtualenv_pypy() { touch "${PYENV_ROOT}/versions/$1/bin/activate" } -remove_virtualenv() { - remove_version "$1" - remove_version "${2:-$1}" -} - create_m_venv() { create_version "$1" create_version "${2:-$1}" @@ -52,10 +43,6 @@ create_m_venv() { touch "${PYENV_ROOT}/versions/$1/bin/activate" } -remove_m_venv() { - remove_version "${2:-$1}" -} - create_conda() { create_version "$1" create_version "${2:-$1}" @@ -66,10 +53,6 @@ create_conda() { touch "${PYENV_ROOT}/versions/${2:-$1}/bin/activate" } -remove_conda() { - remove_version "${2:-$1}" -} - @test "display prefix of virtualenv created by virtualenv" { stub pyenv-version-name "echo foo" create_virtualenv "foo" "2.7.11" @@ -82,7 +65,6 @@ ${PYENV_ROOT}/versions/2.7.11 OUT unstub pyenv-version-name - remove_virtualenv "foo" "2.7.11" } @test "display prefix of virtualenv created by virtualenv (pypy)" { @@ -97,7 +79,6 @@ ${PYENV_ROOT}/versions/pypy-4.0.1 OUT unstub pyenv-version-name - remove_virtualenv "foo" "pypy-4.0.1" } @test "display prefix of virtualenv created by virtualenv (jython)" { @@ -112,7 +93,6 @@ ${PYENV_ROOT}/versions/jython-2.7.0 OUT unstub pyenv-version-name - remove_virtualenv "foo" "jython-2.7.0" } @test "display prefixes of virtualenv created by virtualenv" { @@ -128,8 +108,6 @@ ${PYENV_ROOT}/versions/2.7.11:${PYENV_ROOT}/versions/3.5.1 OUT unstub pyenv-version-name - remove_virtualenv "foo" "2.7.11" - remove_virtualenv "bar" "3.5.1" } @test "display prefix of virtualenv created by venv" { @@ -144,7 +122,6 @@ ${PYENV_ROOT}/versions/3.3.6 OUT unstub pyenv-version-name - remove_m_venv "foo" "3.3.6" } @test "display prefixes of virtualenv created by venv" { @@ -160,8 +137,6 @@ ${PYENV_ROOT}/versions/3.3.6:${PYENV_ROOT}/versions/3.4.4 OUT unstub pyenv-version-name - remove_m_venv "foo" "3.3.6" - remove_m_venv "bar" "3.4.4" } @test "display prefix of virtualenv created by conda" { @@ -176,7 +151,6 @@ ${PYENV_ROOT}/versions/miniconda3-3.16.0/envs/foo OUT unstub pyenv-version-name - remove_conda "miniconda3-3.16.0/envs/foo" "miniconda3-3.16.0" } @test "should fail if the version is the system" { @@ -204,7 +178,6 @@ pyenv-virtualenv: version \`3.4.4' is not a virtualenv OUT unstub pyenv-version-name - remove_version "3.4.4" } @test "should fail if one of the versions is not a virtualenv" { @@ -220,8 +193,6 @@ pyenv-virtualenv: version \`3.4.4' is not a virtualenv OUT unstub pyenv-version-name - remove_virtualenv "venv33" "3.3.6" - remove_version "3.4.4" } @test "resolves a version that is not an exact match" { @@ -239,6 +210,5 @@ OUT unstub pyenv-version-name unstub pyenv-prefix - remove_version "3.4.4" } From adc9e5e2a1b2e448e63188e63e54e41a6d2265e3 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Wed, 29 Apr 2026 07:37:02 +0300 Subject: [PATCH 35/35] + refactor notes --- bin/pyenv-virtualenv-prefix | 8 ++++++-- bin/pyenv-virtualenvs | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/pyenv-virtualenv-prefix b/bin/pyenv-virtualenv-prefix index c565e6f9..198096ad 100755 --- a/bin/pyenv-virtualenv-prefix +++ b/bin/pyenv-virtualenv-prefix @@ -54,8 +54,12 @@ for version in "${versions[@]}"; do exit 1 fi - # Calling pyenv-prefix is expensive. Only do so if the path doesn't exist. - # See https://github.com/pyenv/pyenv-virtualenv/pull/502 + # In the vast majority of cases, there's a direct match and + # not spawning `pyenv-prefix' saves about half the invocation time + # with a signle argument which accumulates when called repeatedly + # (e.g. from `pyenv-virtualenvs'). + # `pyenv-prefix' also does not have hooks to worry about. + # XXX: refactor the test into a shared module? PYENV_PREFIX_PATH="${PYENV_ROOT}/versions/${version}" if [[ ! -d "$PYENV_PREFIX_PATH" ]]; then PYENV_PREFIX_PATH="$(pyenv-prefix "${version}")" diff --git a/bin/pyenv-virtualenvs b/bin/pyenv-virtualenvs index 33b106cb..8676574a 100755 --- a/bin/pyenv-virtualenvs +++ b/bin/pyenv-virtualenvs @@ -119,6 +119,8 @@ for env_path in "${version_dir_entries[@]}"; do if [[ -z $skip_aliases ]]; then print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" fi + # Mimics the test from pyenv-virtualenv-prefix + # XXX: refactor itto a shared module ? elif [[ -f "${env_path}/bin/activate" ]]; then print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}" fi