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
This commit is contained in:
jakelodwick 2026-03-24 21:39:30 -06:00
parent 38f3333f3a
commit 8546b11429
3 changed files with 209 additions and 2 deletions

View file

@ -9,6 +9,23 @@
set -e
[ -n "$PYENV_DEBUG" ] && set -x
# Detect stat format for mtime: GNU uses -c %Y, BSD uses -f %m
# -L follows symlinks: a symlinked .python-version reflects target changes
if stat -L -c %Y / >/dev/null 2>&1; then
_stat_fmt="-L -c %Y"
else
_stat_fmt="-L -f %m"
fi
# Check for version-name hooks at init time. Hooks can alter version
# resolution in ways the mtime cache cannot track. If present, the hook
# falls back to upstream behavior (no caching). Restart shell after
# installing or removing pyenv plugins.
_has_version_hooks=""
if [ -n "$(pyenv hooks version-name 2>/dev/null)" ]; then
_has_version_hooks=1
fi
resolve_link() {
$(type -p greadlink readlink | head -1) "$1"
}
@ -103,7 +120,8 @@ esac
case "$shell" in
fish )
cat <<EOS
if [ -n "$_has_version_hooks" ]; then
cat <<EOS
function _pyenv_virtualenv_hook --on-event fish_prompt;
set -l ret \$status
if [ -n "\$VIRTUAL_ENV" ]
@ -114,6 +132,50 @@ function _pyenv_virtualenv_hook --on-event fish_prompt;
return \$ret
end
EOS
else
cat <<EOS
function _pyenv_virtualenv_hook --on-event fish_prompt;
set -l ret \$status
if test "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\
-a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV"
if test -n "\$PYENV_VERSION"
return \$ret
end
if test "\$PWD" = "\$_PYENV_VH_PWD" \\
-a "(stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)" = "\$_PYENV_VH_MTIMES"
return \$ret
end
end
if [ -n "\$VIRTUAL_ENV" ]
pyenv activate --quiet; or pyenv deactivate --quiet; or true
else
pyenv activate --quiet; or true
end
set -g _PYENV_VH_PWD "\$PWD"
set -g _PYENV_VH_VERSION "\$PYENV_VERSION"
set -g _PYENV_VH_VENV "\$VIRTUAL_ENV"
set -l d "\$PWD"
set -l _pvh_found_local 0
set -g _PYENV_VH_PATHS
while true
if test -e "\$d/.python-version"; or test -L "\$d/.python-version"
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d/.python-version"
set _pvh_found_local 1
break
end
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d"
test "\$d" = "/"; and break
set d (string replace -r '/[^/]*\$' '' -- "\$d")
test -z "\$d"; and set d "/"
end
if test "\$_pvh_found_local" = "0"
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$PYENV_ROOT/version"
end
set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)
return \$ret
end
EOS
fi
;;
ksh )
cat <<EOS
@ -128,7 +190,8 @@ EOS
esac
if [[ "$shell" != "fish" ]]; then
cat <<EOS
if [ -n "$_has_version_hooks" ]; then
cat <<EOS
local ret=\$?
if [ -n "\${VIRTUAL_ENV-}" ]; then
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
@ -138,6 +201,49 @@ if [[ "$shell" != "fish" ]]; then
return \$ret
};
EOS
else
cat <<EOS
local ret=\$?
# Cache: env vars checked once, path list and stat rebuilt on miss only
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
if [ -n "\${PYENV_VERSION-}" ]; then
return \$ret
fi
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
return \$ret
fi
fi
if [ -n "\${VIRTUAL_ENV-}" ]; then
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
else
eval "\$(pyenv sh-activate --quiet || true)" || true
fi
_PYENV_VH_PWD="\${PWD}"
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
local _pvh_d="\${PWD}" _pvh_found_local=0
_PYENV_VH_PATHS=()
while :; do
if [ -e "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
_pvh_found_local=1
break
fi
_PYENV_VH_PATHS+=("\${_pvh_d}")
[ "\${_pvh_d}" = "/" ] && break
_pvh_d="\${_pvh_d%/*}"
[ -z "\${_pvh_d}" ] && _pvh_d="/"
done
if [ "\${_pvh_found_local}" = "0" ]; then
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
fi
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
return \$ret
};
EOS
fi
case "$shell" in
bash )

View file

@ -54,11 +54,42 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}";
export PYENV_VIRTUALENV_INIT=1;
_pyenv_virtualenv_hook() {
local ret=\$?
# Cache: env vars checked once, path list and stat rebuilt on miss only
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
if [ -n "\${PYENV_VERSION-}" ]; then
return \$ret
fi
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
return \$ret
fi
fi
if [ -n "\${VIRTUAL_ENV-}" ]; then
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
else
eval "\$(pyenv sh-activate --quiet || true)" || true
fi
_PYENV_VH_PWD="\${PWD}"
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
local _pvh_d="\${PWD}" _pvh_found_local=0
_PYENV_VH_PATHS=()
while :; do
if [ -e "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
_pvh_found_local=1
break
fi
_PYENV_VH_PATHS+=("\${_pvh_d}")
[ "\${_pvh_d}" = "/" ] && break
_pvh_d="\${_pvh_d%/*}"
[ -z "\${_pvh_d}" ] && _pvh_d="/"
done
if [ "\${_pvh_found_local}" = "0" ]; then
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
fi
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
return \$ret
};
if ! [[ "\${PROMPT_COMMAND-}" =~ _pyenv_virtualenv_hook ]]; then
@ -78,11 +109,42 @@ set -gx PATH '${TMP}/pyenv/plugins/pyenv-virtualenv/shims' \$PATH;
set -gx PYENV_VIRTUALENV_INIT 1;
function _pyenv_virtualenv_hook --on-event fish_prompt;
set -l ret \$status
if test "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\
-a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV"
if test -n "\$PYENV_VERSION"
return \$ret
end
if test "\$PWD" = "\$_PYENV_VH_PWD" \\
-a "(stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)" = "\$_PYENV_VH_MTIMES"
return \$ret
end
end
if [ -n "\$VIRTUAL_ENV" ]
pyenv activate --quiet; or pyenv deactivate --quiet; or true
else
pyenv activate --quiet; or true
end
set -g _PYENV_VH_PWD "\$PWD"
set -g _PYENV_VH_VERSION "\$PYENV_VERSION"
set -g _PYENV_VH_VENV "\$VIRTUAL_ENV"
set -l d "\$PWD"
set -l _pvh_found_local 0
set -g _PYENV_VH_PATHS
while true
if test -e "\$d/.python-version"; or test -L "\$d/.python-version"
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d/.python-version"
set _pvh_found_local 1
break
end
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$d"
test "\$d" = "/"; and break
set d (string replace -r '/[^/]*\$' '' -- "\$d")
test -z "\$d"; and set d "/"
end
if test "\$_pvh_found_local" = "0"
set -g _PYENV_VH_PATHS \$_PYENV_VH_PATHS "\$PYENV_ROOT/version"
end
set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$_PYENV_VH_PATHS 2>/dev/null)
return \$ret
end
EOS
@ -97,11 +159,42 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}";
export PYENV_VIRTUALENV_INIT=1;
_pyenv_virtualenv_hook() {
local ret=\$?
# Cache: env vars checked once, path list and stat rebuilt on miss only
if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\
&& [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then
if [ -n "\${PYENV_VERSION-}" ]; then
return \$ret
fi
if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\
&& [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then
return \$ret
fi
fi
if [ -n "\${VIRTUAL_ENV-}" ]; then
eval "\$(pyenv sh-activate --quiet || pyenv sh-deactivate --quiet || true)" || true
else
eval "\$(pyenv sh-activate --quiet || true)" || true
fi
_PYENV_VH_PWD="\${PWD}"
_PYENV_VH_VERSION="\${PYENV_VERSION-}"
_PYENV_VH_VENV="\${VIRTUAL_ENV-}"
local _pvh_d="\${PWD}" _pvh_found_local=0
_PYENV_VH_PATHS=()
while :; do
if [ -e "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then
_PYENV_VH_PATHS+=("\${_pvh_d}/.python-version")
_pvh_found_local=1
break
fi
_PYENV_VH_PATHS+=("\${_pvh_d}")
[ "\${_pvh_d}" = "/" ] && break
_pvh_d="\${_pvh_d%/*}"
[ -z "\${_pvh_d}" ] && _pvh_d="/"
done
if [ "\${_pvh_found_local}" = "0" ]; then
_PYENV_VH_PATHS+=("\${PYENV_ROOT}/version")
fi
_PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)"
return \$ret
};
typeset -g -a precmd_functions

View file

@ -1,4 +1,12 @@
export TMP="$BATS_TEST_DIRNAME/tmp"
# Detect stat format for mtime: GNU uses -c %Y, BSD uses -f %m
# Must match the detection in bin/pyenv-virtualenv-init
if stat -L -c %Y / >/dev/null 2>&1; then
_stat_fmt="-L -c %Y"
else
_stat_fmt="-L -f %m"
fi
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
PATH=/usr/bin:/usr/sbin:/bin:/sbin