Skip to content

Commit 37ab83f

Browse files
samdorannative-api
andauthored
Improve performance and output of pyenv virtualenvs (#502)
* With 160 envs, cuts time from ~14s to ~2.5s * Change the output to be more like `pyenv-versions' * Shellcheck fixes * virtualenv: Only show non-envs for completion since env engines don't support piggy-backing envs * Remove dead/unused code * Fix IFS handling `IFS=: <command>' doesn't apply that IFS to expansions in <command>, it has to be set separately earlier * Adjust and refactor tests --------- Co-authored-by: Ivan Pozdeev <vano@mail.mipt.ru>
1 parent 99d35f2 commit 37ab83f

6 files changed

Lines changed: 250 additions & 141 deletions

File tree

bin/pyenv-virtualenv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fi
2424

2525
# Provide pyenv completions
2626
if [ "$1" = "--complete" ]; then
27-
exec pyenv-versions --bare
27+
exec pyenv-versions --bare --skip-envs
2828
fi
2929

3030
unset PIP_REQUIRE_VENV

bin/pyenv-virtualenv-prefix

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ if [ -z "$PYENV_ROOT" ]; then
2626
PYENV_ROOT="${HOME}/.pyenv"
2727
fi
2828

29+
OLDIFS="$IFS"
30+
IFS=:
2931
if [ -n "$1" ]; then
30-
versions=($@)
31-
IFS=: PYENV_VERSION="${versions[*]}"
32+
# $@ is not affected by IFS
33+
versions=("$@")
34+
PYENV_VERSION="${versions[*]}"
3235
export PYENV_VERSION
3336
else
34-
IFS=: versions=($(pyenv-version-name))
37+
versions=($(pyenv-version-name))
3538
fi
39+
IFS="$OLDIFS"
3640

3741
append_virtualenv_prefix() {
3842
if [ -d "${VIRTUALENV_PREFIX_PATH}" ]; then
@@ -49,7 +53,18 @@ for version in "${versions[@]}"; do
4953
echo "pyenv-virtualenv: version \`${version}' is not a virtualenv" 1>&2
5054
exit 1
5155
fi
52-
PYENV_PREFIX_PATH="$(pyenv-prefix "${version}")"
56+
57+
# In the vast majority of cases, there's a direct match and
58+
# not spawning `pyenv-prefix' saves about half the invocation time
59+
# with a signle argument which accumulates when called repeatedly
60+
# (e.g. from `pyenv-virtualenvs').
61+
# `pyenv-prefix' also does not have hooks to worry about.
62+
# XXX: refactor the test into a shared module?
63+
PYENV_PREFIX_PATH="${PYENV_ROOT}/versions/${version}"
64+
if [[ ! -d "$PYENV_PREFIX_PATH" ]]; then
65+
PYENV_PREFIX_PATH="$(pyenv-prefix "${version}")"
66+
fi
67+
5368
if [ -x "${PYENV_PREFIX_PATH}/bin/python" ]; then
5469
if [ -f "${PYENV_PREFIX_PATH}/bin/activate" ]; then
5570
if [ -f "${PYENV_PREFIX_PATH}/bin/conda" ]; then
@@ -94,4 +109,7 @@ for version in "${versions[@]}"; do
94109
fi
95110
done
96111

97-
IFS=: echo "${VIRTUALENV_PREFIX_PATHS[*]}"
112+
OLDIFS="$IFS"
113+
IFS=:
114+
echo "${VIRTUALENV_PREFIX_PATHS[*]}"
115+
IFS="$OLDIFS"

bin/pyenv-virtualenvs

Lines changed: 68 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,9 @@
66
# List all virtualenvs found in `$PYENV_ROOT/versions/*' and its `$PYENV_ROOT/versions/envs/*'.
77

88
set -e
9-
[ -n "$PYENV_DEBUG" ] && set -x
10-
if [ -L "${BASH_SOURCE}" ]; then
11-
READLINK=$(type -p greadlink readlink | head -1)
12-
if [ -z "$READLINK" ]; then
13-
echo "pyenv: cannot find readlink - are you missing GNU coreutils?" >&2
14-
exit 1
15-
fi
16-
resolve_link() {
17-
$READLINK -f "$1"
18-
}
19-
script_path=$(resolve_link ${BASH_SOURCE})
20-
else
21-
script_path=${BASH_SOURCE}
22-
fi
23-
24-
. ${script_path%/*}/../libexec/pyenv-virtualenv-realpath
9+
[[ -n $PYENV_DEBUG ]] && set -x
2510

26-
if [ -z "$PYENV_ROOT" ]; then
11+
if [[ -z $PYENV_ROOT ]]; then
2712
PYENV_ROOT="${HOME}/.pyenv"
2813
fi
2914

@@ -47,28 +32,26 @@ done
4732

4833
versions_dir="${PYENV_ROOT}/versions"
4934

50-
if [ -d "$versions_dir" ]; then
51-
versions_dir="$(realpath "$versions_dir")"
52-
fi
53-
54-
if [ -n "$bare" ]; then
55-
hit_prefix=""
56-
miss_prefix=""
57-
current_versions=()
58-
unset print_origin
59-
include_system=""
35+
if [[ ${BASH_VERSINFO[0]} -gt 3 ]]; then
36+
declare -A current_versions
6037
else
38+
current_versions=()
39+
fi
40+
if [[ -z $bare ]]; then
6141
hit_prefix="* "
6242
miss_prefix=" "
6343
OLDIFS="$IFS"
64-
IFS=: current_versions=($(pyenv-version-name || true))
44+
IFS=:
45+
if [[ ${BASH_VERSINFO[0]} -gt 3 ]]; then
46+
for i in $(pyenv-version-name || true); do
47+
current_versions["$i"]="1"
48+
done
49+
else
50+
read -r -a current_versions <<< "$(pyenv-version-name || true)"
51+
fi
6552
IFS="$OLDIFS"
66-
print_origin="1"
67-
include_system=""
6853
fi
6954

70-
num_versions=0
71-
7255
exists() {
7356
local car="$1"
7457
local cdar
@@ -82,39 +65,67 @@ exists() {
8265
}
8366

8467
print_version() {
85-
if exists "$1" "${current_versions[@]}"; then
86-
echo "${hit_prefix}${1}${print_origin+$2}"
68+
local version="${1:?}"
69+
if [[ -n $bare ]]; then
70+
echo "$version"
71+
return
72+
fi
73+
local path="${2:?}"
74+
if [[ -L "$path" ]]; then
75+
version_repr="$version --> $(readlink "$path")"
76+
else
77+
version_repr="$version (created from $(pyenv-virtualenv-prefix "$version" 2>/dev/null))"
78+
fi
79+
if [[ ${BASH_VERSINFO[0]} -gt 3 && ${current_versions["$1"]} ]] || \
80+
{ [[ ${BASH_VERSINFO[0]} -le 3 ]] && exists "$1" "${current_versions[@]}"; }; then
81+
echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))"
8782
else
88-
echo "${miss_prefix}${1}${print_origin+$2}"
83+
echo "${miss_prefix}${version_repr}"
8984
fi
90-
num_versions=$((num_versions + 1))
9185
}
9286

9387
shopt -s dotglob
9488
shopt -s nullglob
95-
for path in "$versions_dir"/*; do
96-
if [ -d "$path" ]; then
97-
if [ -n "$skip_aliases" ] && [ -L "$path" ]; then
98-
target="$(realpath "$path")"
99-
[ "${target%/*/envs/*}" != "$versions_dir" ] || continue
100-
fi
101-
virtualenv_prefix="$(pyenv-virtualenv-prefix "${path##*/}" 2>/dev/null || true)"
102-
if [ -d "${virtualenv_prefix}" ]; then
103-
print_version "${path##*/}" " (created from ${virtualenv_prefix})"
104-
fi
105-
for venv_path in "${path}/envs/"*; do
106-
venv="${path##*/}/envs/${venv_path##*/}"
107-
virtualenv_prefix="$(pyenv-virtualenv-prefix "${venv}" 2>/dev/null || true)"
108-
if [ -d "${virtualenv_prefix}" ]; then
109-
print_version "${venv}" " (created from ${virtualenv_prefix})"
89+
version_dir_entries=("$versions_dir"/*)
90+
venv_dir_entries=("$versions_dir"/*/envs/*)
91+
92+
if sort --version-sort </dev/null >/dev/null 2>&1; then
93+
# system sort supports version sorting
94+
OLDIFS="$IFS"
95+
IFS='||'
96+
97+
read -r -a version_dir_entries <<< "$(
98+
printf "%s||" "${version_dir_entries[@]}" |
99+
sort --version-sort
100+
)"
101+
102+
read -r -a venv_dir_entries <<< "$(
103+
printf "%s||" "${venv_dir_entries[@]}" |
104+
sort --version-sort
105+
)"
106+
107+
IFS="$OLDIFS"
108+
fi
109+
110+
for env_path in "${venv_dir_entries[@]}"; do
111+
if [[ -d ${env_path} ]]; then
112+
print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}"
113+
fi
114+
done
115+
116+
for env_path in "${version_dir_entries[@]}"; do
117+
if [[ -d ${env_path} ]]; then
118+
if [[ -L ${env_path} ]]; then
119+
if [[ -z $skip_aliases ]]; then
120+
print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}"
110121
fi
111-
done
122+
# Mimics the test from pyenv-virtualenv-prefix
123+
# XXX: refactor itto a shared module ?
124+
elif [[ -f "${env_path}/bin/activate" ]]; then
125+
print_version "${env_path#"${PYENV_ROOT}"/versions/}" "${env_path}"
126+
fi
112127
fi
113128
done
129+
114130
shopt -u dotglob
115131
shopt -u nullglob
116-
117-
if [ "$num_versions" -eq 0 ] && [ -n "$include_system" ]; then
118-
echo "Warning: no Python virtualenv detected on the system" >&2
119-
exit 1
120-
fi

test/conda-prefix.bats

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ setup() {
99
@test "display conda root" {
1010
setup_conda "anaconda-2.3.0"
1111
stub pyenv-version-name "echo anaconda-2.3.0"
12-
stub pyenv-prefix "anaconda-2.3.0 : echo \"${PYENV_ROOT}/versions/anaconda-2.3.0\""
1312

1413
PYENV_VERSION="anaconda-2.3.0" run pyenv-virtualenv-prefix
1514

@@ -19,14 +18,12 @@ ${PYENV_ROOT}/versions/anaconda-2.3.0
1918
OUT
2019

2120
unstub pyenv-version-name
22-
unstub pyenv-prefix
2321
teardown_conda "anaconda-2.3.0"
2422
}
2523

2624
@test "display conda env" {
2725
setup_conda "anaconda-2.3.0" "foo"
2826
stub pyenv-version-name "echo anaconda-2.3.0/envs/foo"
29-
stub pyenv-prefix "anaconda-2.3.0/envs/foo : echo \"${PYENV_ROOT}/versions/anaconda-2.3.0/envs/foo\""
3027

3128
PYENV_VERSION="anaconda-2.3.0/envs/foo" run pyenv-virtualenv-prefix
3229

@@ -36,6 +33,5 @@ ${PYENV_ROOT}/versions/anaconda-2.3.0/envs/foo
3633
OUT
3734

3835
unstub pyenv-version-name
39-
unstub pyenv-prefix
4036
teardown_conda "anaconda-2.3.0" "foo"
4137
}

0 commit comments

Comments
 (0)