Skip to content

Commit 8546b11

Browse files
committed
Cache hook state to skip redundant pyenv sh-activate calls
Mtime-based caching: stat the directory tree once per prompt (~2ms) and skip full activation when nothing has changed. - Compare PYENV_VERSION, VIRTUAL_ENV, PWD; shortcut when PYENV_VERSION matches - Walk PWD to / for .python-version (stop at first; include broken symlinks) - Path list built once on miss, stored as array (spaces in paths) - Global version file skipped when local version active - version-name hooks checked at init; if present, caching disabled - Covers bash, zsh, and fish
1 parent 38f3333 commit 8546b11

3 files changed

Lines changed: 209 additions & 2 deletions

File tree

bin/pyenv-virtualenv-init

Lines changed: 108 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,50 @@ 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 -e "\$d/.python-version"; or test -L "\$d/.python-version"
162+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d/.python-version"
163+
set _pvh_found_local 1
164+
break
165+
end
166+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d"
167+
test "\$d" = "/"; and break
168+
set d (string replace -r '/[^/]*\$' '' -- "\$d")
169+
test -z "\$d"; and set d "/"
170+
end
171+
if test "\$_pvh_found_local" = "0"
172+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$PYENV_ROOT/version"
173+
end
174+
set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)
175+
return \$ret
176+
end
177+
EOS
178+
fi
117179
;;
118180
ksh )
119181
cat <<EOS
@@ -128,7 +190,8 @@ EOS
128190
esac
129191

130192
if [[ "$shell" != "fish" ]]; then
131-
cat <<EOS
193+
if [ -n "$_has_version_hooks" ]; then
194+
cat <<EOS
132195
local ret=\$?
133196
if [ -n "\${VIRTUAL_ENV-}" ]; then
134197
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
@@ -138,6 +201,49 @@ if [[ "$shell" != "fish" ]]; then
138201
return \$ret
139202
};
140203
EOS
204+
else
205+
cat <<EOS
206+
local ret=\$?
207+
# Cache: env vars checked once, path list and stat rebuilt on miss only
208+
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
209+
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
210+
if [ -n "\${PYENV_VERSION-}" ]; then
211+
return \$ret
212+
fi
213+
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
214+
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
215+
return \$ret
216+
fi
217+
fi
218+
if [ -n "\${VIRTUAL_ENV-}" ]; then
219+
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
220+
else
221+
eval "\$(pyenv sh-activate --quiet || true)" || true
222+
fi
223+
_PYENV_VH_PWD="\${PWD}"
224+
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
225+
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
226+
local _pvh_d="\${PWD}" _pvh_found_local=0
227+
_PYENV_VH_PATHS=()
228+
while :; do
229+
if [ -e "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
230+
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
231+
_pvh_found_local=1
232+
break
233+
fi
234+
_PYENV_VH_PATHS+=("\${_pvh_d}")
235+
[ "\${_pvh_d}" = "/" ] && break
236+
_pvh_d="\${_pvh_d%/*}"
237+
[ -z "\${_pvh_d}" ] && _pvh_d="/"
238+
done
239+
if [ "\${_pvh_found_local}" = "0" ]; then
240+
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
241+
fi
242+
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
243+
return \$ret
244+
};
245+
EOS
246+
fi
141247

142248
case "$shell" in
143249
bash )

test/init.bats

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,42 @@ 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 [ -e "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
80+
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
81+
_pvh_found_local=1
82+
break
83+
fi
84+
_PYENV_VH_PATHS+=("\${_pvh_d}")
85+
[ "\${_pvh_d}" = "/" ] && break
86+
_pvh_d="\${_pvh_d%/*}"
87+
[ -z "\${_pvh_d}" ] && _pvh_d="/"
88+
done
89+
if [ "\${_pvh_found_local}" = "0" ]; then
90+
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
91+
fi
92+
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
6293
return \$ret
6394
};
6495
if ! [[ "\${PROMPT_COMMAND-}" =~ _pyenv_virtualenv_hook ]]; then
@@ -78,11 +109,42 @@ set -gx PATH '${TMP}/pyenv/plugins/pyenv-virtualenv/shims' \$PATH;
78109
set -gx PYENV_VIRTUALENV_INIT 1;
79110
function _pyenv_virtualenv_hook --on-event fish_prompt;
80111
set -l ret \$status
112+
if test "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\
113+
-a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV"
114+
if test -n "\$PYENV_VERSION"
115+
return \$ret
116+
end
117+
if test "\$PWD" = "\$_PYENV_VH_PWD" \\
118+
-a "(stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)" = "\$_PYENV_VH_MTIMES"
119+
return \$ret
120+
end
121+
end
81122
if [ -n "\$VIRTUAL_ENV" ]
82123
pyenv activate --quiet; or pyenv deactivate --quiet; or true
83124
else
84125
pyenv activate --quiet; or true
85126
end
127+
set -g _PYENV_VH_PWD "\$PWD"
128+
set -g _PYENV_VH_VERSION "\$PYENV_VERSION"
129+
set -g _PYENV_VH_VENV "\$VIRTUAL_ENV"
130+
set -l d "\$PWD"
131+
set -l _pvh_found_local 0
132+
set -g _PYENV_VH_PATHS
133+
while true
134+
if test -e "\$d/.python-version"; or test -L "\$d/.python-version"
135+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d/.python-version"
136+
set _pvh_found_local 1
137+
break
138+
end
139+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d"
140+
test "\$d" = "/"; and break
141+
set d (string replace -r '/[^/]*\$' '' -- "\$d")
142+
test -z "\$d"; and set d "/"
143+
end
144+
if test "\$_pvh_found_local" = "0"
145+
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$PYENV_ROOT/version"
146+
end
147+
set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)
86148
return \$ret
87149
end
88150
EOS
@@ -97,11 +159,42 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}";
97159
export PYENV_VIRTUALENV_INIT=1;
98160
_pyenv_virtualenv_hook() {
99161
local ret=\$?
162+
# Cache: env vars checked once, path list and stat rebuilt on miss only
163+
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
164+
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
165+
if [ -n "\${PYENV_VERSION-}" ]; then
166+
return \$ret
167+
fi
168+
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
169+
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
170+
return \$ret
171+
fi
172+
fi
100173
if [ -n "\${VIRTUAL_ENV-}" ]; then
101174
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
102175
else
103176
eval "\$(pyenv sh-activate --quiet || true)" || true
104177
fi
178+
_PYENV_VH_PWD="\${PWD}"
179+
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
180+
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
181+
local _pvh_d="\${PWD}" _pvh_found_local=0
182+
_PYENV_VH_PATHS=()
183+
while :; do
184+
if [ -e "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
185+
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
186+
_pvh_found_local=1
187+
break
188+
fi
189+
_PYENV_VH_PATHS+=("\${_pvh_d}")
190+
[ "\${_pvh_d}" = "/" ] && break
191+
_pvh_d="\${_pvh_d%/*}"
192+
[ -z "\${_pvh_d}" ] && _pvh_d="/"
193+
done
194+
if [ "\${_pvh_found_local}" = "0" ]; then
195+
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
196+
fi
197+
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
105198
return \$ret
106199
};
107200
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)