Skip to content

Commit 72cb35b

Browse files
authored
Drastically speed up precommand hook by caching active version to skip redundant activate calls (#523)
1 parent 38f3333 commit 72cb35b

3 files changed

Lines changed: 224 additions & 2 deletions

File tree

bin/pyenv-virtualenv-init

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@
99
set -e
1010
[ -n "$PYENV_DEBUG" ] && set -x
1111

12+
# Detect stat format for mtime: GNU uses -c %Y, BSD uses -f %m
13+
# -L follows symlinks: a symlinked .python-version reflects target changes
14+
if stat -L -c %Y / >/dev/null 2>&1; then
15+
_stat_fmt="-L -c %Y"
16+
else
17+
_stat_fmt="-L -f %m"
18+
fi
19+
20+
# Check for version-name hooks at init time. Hooks can alter version
21+
# resolution in ways the mtime cache cannot track. If present, the hook
22+
# falls back to upstream behavior (no caching). Restart shell after
23+
# installing or removing pyenv plugins.
24+
_has_version_hooks=""
25+
if [ -n "$(pyenv hooks version-name 2>/dev/null)" ]; then
26+
_has_version_hooks=1
27+
fi
28+
1229
resolve_link() {
1330
$(type -p greadlink readlink | head -1) "$1"
1431
}
@@ -103,7 +120,8 @@ esac
103120

104121
case "$shell" in
105122
fish )
106-
cat <<EOS
123+
if [ -n "$_has_version_hooks" ]; then
124+
cat <<EOS
107125
function _pyenv_virtualenv_hook --on-event fish_prompt;
108126
set -l ret \$status
109127
if [ -n "\$VIRTUAL_ENV" ]
@@ -114,6 +132,53 @@ function _pyenv_virtualenv_hook --on-event fish_prompt;
114132
return \$ret
115133
end
116134
EOS
135+
else
136+
cat <<EOS
137+
function _pyenv_virtualenv_hook --on-event fish_prompt;
138+
set -l ret \$status
139+
if test "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\
140+
-a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV"
141+
if test -n "\$PYENV_VERSION"
142+
return \$ret
143+
end
144+
if test "\$PWD" = "\$_PYENV_VH_PWD" \\
145+
-a "(stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)" = "\$_PYENV_VH_MTIMES"
146+
return \$ret
147+
end
148+
end
149+
if [ -n "\$VIRTUAL_ENV" ]
150+
pyenv activate --quiet; or pyenv deactivate --quiet; or true
151+
else
152+
pyenv activate --quiet; or true
153+
end
154+
set -g _PYENV_VH_PWD "\$PWD"
155+
set -g _PYENV_VH_VERSION "\$PYENV_VERSION"
156+
set -g _PYENV_VH_VENV "\$VIRTUAL_ENV"
157+
set -l d "\$PWD"
158+
set -l _pvh_found_local 0
159+
set -g _PYENV_VH_PATHS
160+
while true
161+
if test -f "\$d/.python-version"; or test -L "\$d/.python-version"
162+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d/.python-version"
163+
if test -f "\$d/.python-version"
164+
set _pvh_found_local 1
165+
break
166+
end
167+
else
168+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d"
169+
end
170+
test "\$d" = "/"; and break
171+
set d (string replace -r '/[^/]*\$' '' -- "\$d")
172+
test -z "\$d"; and set d "/"
173+
end
174+
if test "\$_pvh_found_local" = "0"
175+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$PYENV_ROOT/version"
176+
end
177+
set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)
178+
return \$ret
179+
end
180+
EOS
181+
fi
117182
;;
118183
ksh )
119184
cat <<EOS
@@ -128,7 +193,8 @@ EOS
128193
esac
129194

130195
if [[ "$shell" != "fish" ]]; then
131-
cat <<EOS
196+
if [ -n "$_has_version_hooks" ]; then
197+
cat <<EOS
132198
local ret=\$?
133199
if [ -n "\${VIRTUAL_ENV-}" ]; then
134200
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
@@ -138,6 +204,52 @@ if [[ "$shell" != "fish" ]]; then
138204
return \$ret
139205
};
140206
EOS
207+
else
208+
cat <<EOS
209+
local ret=\$?
210+
# Cache: env vars checked once, path list and stat rebuilt on miss only
211+
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
212+
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
213+
if [ -n "\${PYENV_VERSION-}" ]; then
214+
return \$ret
215+
fi
216+
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
217+
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
218+
return \$ret
219+
fi
220+
fi
221+
if [ -n "\${VIRTUAL_ENV-}" ]; then
222+
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
223+
else
224+
eval "\$(pyenv sh-activate --quiet || true)" || true
225+
fi
226+
_PYENV_VH_PWD="\${PWD}"
227+
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
228+
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
229+
local _pvh_d="\${PWD}" _pvh_found_local=0
230+
_PYENV_VH_PATHS=()
231+
while :; do
232+
if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
233+
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
234+
if [ -f "\${_pvh_d}/.python-version" ]; then
235+
_pvh_found_local=1
236+
break
237+
fi
238+
else
239+
_PYENV_VH_PATHS+=("\${_pvh_d}")
240+
fi
241+
[ "\${_pvh_d}" = "/" ] && break
242+
_pvh_d="\${_pvh_d%/*}"
243+
[ -z "\${_pvh_d}" ] && _pvh_d="/"
244+
done
245+
if [ "\${_pvh_found_local}" = "0" ]; then
246+
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
247+
fi
248+
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
249+
return \$ret
250+
};
251+
EOS
252+
fi
141253

142254
case "$shell" in
143255
bash )

test/init.bats

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,45 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}";
5454
export PYENV_VIRTUALENV_INIT=1;
5555
_pyenv_virtualenv_hook() {
5656
local ret=\$?
57+
# Cache: env vars checked once, path list and stat rebuilt on miss only
58+
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
59+
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
60+
if [ -n "\${PYENV_VERSION-}" ]; then
61+
return \$ret
62+
fi
63+
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
64+
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
65+
return \$ret
66+
fi
67+
fi
5768
if [ -n "\${VIRTUAL_ENV-}" ]; then
5869
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
5970
else
6071
eval "\$(pyenv sh-activate --quiet || true)" || true
6172
fi
73+
_PYENV_VH_PWD="\${PWD}"
74+
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
75+
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
76+
local _pvh_d="\${PWD}" _pvh_found_local=0
77+
_PYENV_VH_PATHS=()
78+
while :; do
79+
if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
80+
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
81+
if [ -f "\${_pvh_d}/.python-version" ]; then
82+
_pvh_found_local=1
83+
break
84+
fi
85+
else
86+
_PYENV_VH_PATHS+=("\${_pvh_d}")
87+
fi
88+
[ "\${_pvh_d}" = "/" ] && break
89+
_pvh_d="\${_pvh_d%/*}"
90+
[ -z "\${_pvh_d}" ] && _pvh_d="/"
91+
done
92+
if [ "\${_pvh_found_local}" = "0" ]; then
93+
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
94+
fi
95+
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
6296
return \$ret
6397
};
6498
if ! [[ "\${PROMPT_COMMAND-}" =~ _pyenv_virtualenv_hook ]]; then
@@ -78,11 +112,45 @@ set -gx PATH '${TMP}/pyenv/plugins/pyenv-virtualenv/shims' \$PATH;
78112
set -gx PYENV_VIRTUALENV_INIT 1;
79113
function _pyenv_virtualenv_hook --on-event fish_prompt;
80114
set -l ret \$status
115+
if test "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\
116+
-a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV"
117+
if test -n "\$PYENV_VERSION"
118+
return \$ret
119+
end
120+
if test "\$PWD" = "\$_PYENV_VH_PWD" \\
121+
-a "(stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)" = "\$_PYENV_VH_MTIMES"
122+
return \$ret
123+
end
124+
end
81125
if [ -n "\$VIRTUAL_ENV" ]
82126
pyenv activate --quiet; or pyenv deactivate --quiet; or true
83127
else
84128
pyenv activate --quiet; or true
85129
end
130+
set -g _PYENV_VH_PWD "\$PWD"
131+
set -g _PYENV_VH_VERSION "\$PYENV_VERSION"
132+
set -g _PYENV_VH_VENV "\$VIRTUAL_ENV"
133+
set -l d "\$PWD"
134+
set -l _pvh_found_local 0
135+
set -g _PYENV_VH_PATHS
136+
while true
137+
if test -f "\$d/.python-version"; or test -L "\$d/.python-version"
138+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d/.python-version"
139+
if test -f "\$d/.python-version"
140+
set _pvh_found_local 1
141+
break
142+
end
143+
else
144+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d"
145+
end
146+
test "\$d" = "/"; and break
147+
set d (string replace -r '/[^/]*\$' '' -- "\$d")
148+
test -z "\$d"; and set d "/"
149+
end
150+
if test "\$_pvh_found_local" = "0"
151+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$PYENV_ROOT/version"
152+
end
153+
set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)
86154
return \$ret
87155
end
88156
EOS
@@ -97,11 +165,45 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}";
97165
export PYENV_VIRTUALENV_INIT=1;
98166
_pyenv_virtualenv_hook() {
99167
local ret=\$?
168+
# Cache: env vars checked once, path list and stat rebuilt on miss only
169+
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
170+
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
171+
if [ -n "\${PYENV_VERSION-}" ]; then
172+
return \$ret
173+
fi
174+
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
175+
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
176+
return \$ret
177+
fi
178+
fi
100179
if [ -n "\${VIRTUAL_ENV-}" ]; then
101180
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
102181
else
103182
eval "\$(pyenv sh-activate --quiet || true)" || true
104183
fi
184+
_PYENV_VH_PWD="\${PWD}"
185+
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
186+
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
187+
local _pvh_d="\${PWD}" _pvh_found_local=0
188+
_PYENV_VH_PATHS=()
189+
while :; do
190+
if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
191+
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
192+
if [ -f "\${_pvh_d}/.python-version" ]; then
193+
_pvh_found_local=1
194+
break
195+
fi
196+
else
197+
_PYENV_VH_PATHS+=("\${_pvh_d}")
198+
fi
199+
[ "\${_pvh_d}" = "/" ] && break
200+
_pvh_d="\${_pvh_d%/*}"
201+
[ -z "\${_pvh_d}" ] && _pvh_d="/"
202+
done
203+
if [ "\${_pvh_found_local}" = "0" ]; then
204+
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
205+
fi
206+
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
105207
return \$ret
106208
};
107209
typeset -g -a precmd_functions

test/test_helper.bash

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
export TMP="$BATS_TEST_DIRNAME/tmp"
2+
3+
# Detect stat format for mtime: GNU uses -c %Y, BSD uses -f %m
4+
# Must match the detection in bin/pyenv-virtualenv-init
5+
if stat -L -c %Y / >/dev/null 2>&1; then
6+
_stat_fmt="-L -c %Y"
7+
else
8+
_stat_fmt="-L -f %m"
9+
fi
210
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
311

412
PATH=/usr/bin:/usr/sbin:/bin:/sbin

0 commit comments

Comments
 (0)