From ae6373c8664d09704cc9dfe6c5f61918289113c4 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:50:18 -0500 Subject: [PATCH 01/11] 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 f442d7f7c6c999b3742954f1c684313c16599970 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:51:24 -0500 Subject: [PATCH 02/11] 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 76a18ae3ed3e9ee9458e9352d4a637a2e7424e55 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:57:26 -0500 Subject: [PATCH 03/11] 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 30a15a5407aa89662664b0ba6205883ba785da7c Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 11:59:59 -0500 Subject: [PATCH 04/11] 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 16bb6c69..ac1ba309 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 db5d69d11837617aa6b1f2b1e316b7d3e5c188ca Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 12:03:24 -0500 Subject: [PATCH 05/11] 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 ee98390b4dda0c02a65fb2d7ed72be4f4c965d43 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Feb 2025 12:10:39 -0500 Subject: [PATCH 06/11] 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/11] 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 a075b3cbfef177942fe595c3d8903e376b9e9a92 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 11 Mar 2025 23:24:28 -0400 Subject: [PATCH 08/11] 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 10a9658a5a333c944a3dee53164e5a023b525e01 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 11 Mar 2025 23:55:12 -0400 Subject: [PATCH 09/11] 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 b459977cde40dde81c22af82300653785e6d9ba1 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 11 Mar 2025 23:58:38 -0400 Subject: [PATCH 10/11] 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 61c1b45a6cf261a3868d6ec68e9abad3a0a75373 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Mar 2025 00:43:18 -0400 Subject: [PATCH 11/11] 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