From bb9a6347b9a089d96f050064057935c4c04f2f3c Mon Sep 17 00:00:00 2001 From: jakelodwick <25925+jakelodwick@users.noreply.github.com> Date: Sat, 14 Mar 2026 05:37:47 -0600 Subject: [PATCH 1/2] Cache prompt hook state to skip redundant pyenv sh-activate calls Track five values between prompts ($PWD, $PYENV_VERSION, .python-version content, $PYENV_ROOT/version content, $VIRTUAL_ENV) using shell builtins. When all match, return immediately without forking. Covers cd, pyenv shell/local/global, and manual venv activate/deactivate. Implemented for bash/zsh/ksh (shared POSIX path) and fish. --- bin/pyenv-virtualenv-init | 40 ++++++++++++++++++++++++++ test/init.bats | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/bin/pyenv-virtualenv-init b/bin/pyenv-virtualenv-init index e12769a8..6061fe72 100755 --- a/bin/pyenv-virtualenv-init +++ b/bin/pyenv-virtualenv-init @@ -106,11 +106,31 @@ fish ) cat </dev/null; or true + end + set -l pvh_global "" + if test -f "\$PYENV_ROOT/version" + read -z pvh_global < "\$PYENV_ROOT/version" 2>/dev/null; or true + end + if test "\$PWD" = "\$_PYENV_VH_PWD" \\ + -a "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\ + -a "\$pvh_local" = "\$_PYENV_VH_LOCAL" \\ + -a "\$pvh_global" = "\$_PYENV_VH_GLOBAL" \\ + -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" + return \$ret + 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_LOCAL "\$pvh_local" + set -g _PYENV_VH_GLOBAL "\$pvh_global" + set -g _PYENV_VH_VENV "\$VIRTUAL_ENV" return \$ret end EOS @@ -130,11 +150,31 @@ esac if [[ "$shell" != "fish" ]]; then cat </dev/null || true + fi + local pvh_global="" + if [ -f "\${PYENV_ROOT}/version" ]; then + pvh_global=\$(< "\${PYENV_ROOT}/version") 2>/dev/null || true + fi + if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ + && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ + && [ "\${pvh_local}" = "\${_PYENV_VH_LOCAL-}" ] \\ + && [ "\${pvh_global}" = "\${_PYENV_VH_GLOBAL-}" ] \\ + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + return \$ret + 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_LOCAL="\${pvh_local}" + _PYENV_VH_GLOBAL="\${pvh_global}" + _PYENV_VH_VENV="\${VIRTUAL_ENV-}" return \$ret }; EOS diff --git a/test/init.bats b/test/init.bats index 85f2b423..7c0e6100 100644 --- a/test/init.bats +++ b/test/init.bats @@ -54,11 +54,31 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}"; export PYENV_VIRTUALENV_INIT=1; _pyenv_virtualenv_hook() { local ret=\$? + local pvh_local="" + if [ -f "\${PWD}/.python-version" ]; then + pvh_local=\$(< "\${PWD}/.python-version") 2>/dev/null || true + fi + local pvh_global="" + if [ -f "\${PYENV_ROOT}/version" ]; then + pvh_global=\$(< "\${PYENV_ROOT}/version") 2>/dev/null || true + fi + if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ + && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ + && [ "\${pvh_local}" = "\${_PYENV_VH_LOCAL-}" ] \\ + && [ "\${pvh_global}" = "\${_PYENV_VH_GLOBAL-}" ] \\ + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + return \$ret + 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_LOCAL="\${pvh_local}" + _PYENV_VH_GLOBAL="\${pvh_global}" + _PYENV_VH_VENV="\${VIRTUAL_ENV-}" return \$ret }; if ! [[ "\${PROMPT_COMMAND-}" =~ _pyenv_virtualenv_hook ]]; then @@ -78,11 +98,31 @@ 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 + set -l pvh_local "" + if test -f "\$PWD/.python-version" + read -z pvh_local < "\$PWD/.python-version" 2>/dev/null; or true + end + set -l pvh_global "" + if test -f "\$PYENV_ROOT/version" + read -z pvh_global < "\$PYENV_ROOT/version" 2>/dev/null; or true + end + if test "\$PWD" = "\$_PYENV_VH_PWD" \\ + -a "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\ + -a "\$pvh_local" = "\$_PYENV_VH_LOCAL" \\ + -a "\$pvh_global" = "\$_PYENV_VH_GLOBAL" \\ + -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" + return \$ret + 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_LOCAL "\$pvh_local" + set -g _PYENV_VH_GLOBAL "\$pvh_global" + set -g _PYENV_VH_VENV "\$VIRTUAL_ENV" return \$ret end EOS @@ -97,11 +137,31 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}"; export PYENV_VIRTUALENV_INIT=1; _pyenv_virtualenv_hook() { local ret=\$? + local pvh_local="" + if [ -f "\${PWD}/.python-version" ]; then + pvh_local=\$(< "\${PWD}/.python-version") 2>/dev/null || true + fi + local pvh_global="" + if [ -f "\${PYENV_ROOT}/version" ]; then + pvh_global=\$(< "\${PYENV_ROOT}/version") 2>/dev/null || true + fi + if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ + && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ + && [ "\${pvh_local}" = "\${_PYENV_VH_LOCAL-}" ] \\ + && [ "\${pvh_global}" = "\${_PYENV_VH_GLOBAL-}" ] \\ + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + return \$ret + 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_LOCAL="\${pvh_local}" + _PYENV_VH_GLOBAL="\${pvh_global}" + _PYENV_VH_VENV="\${VIRTUAL_ENV-}" return \$ret }; typeset -g -a precmd_functions From 12665277d4657387a84e0708a9d6ecb7146d6991 Mon Sep 17 00:00:00 2001 From: jakelodwick <25925+jakelodwick@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:41:31 -0600 Subject: [PATCH 2/2] Revise hook cache: stat-based mtime comparison --- bin/pyenv-virtualenv-init | 60 ++++++++++++++++++----------- test/init.bats | 80 ++++++++++++++++++++++----------------- test/test_helper.bash | 7 ++++ 3 files changed, 89 insertions(+), 58 deletions(-) diff --git a/bin/pyenv-virtualenv-init b/bin/pyenv-virtualenv-init index 6061fe72..725b18e8 100755 --- a/bin/pyenv-virtualenv-init +++ b/bin/pyenv-virtualenv-init @@ -9,6 +9,13 @@ set -e [ -n "$PYENV_DEBUG" ] && set -x +# Detect stat format for mtime: GNU uses -c %Y, BSD uses -f %m +if stat -c %Y / >/dev/null 2>&1; then + _stat_fmt="-c %Y" +else + _stat_fmt="-f %m" +fi + resolve_link() { $(type -p greadlink readlink | head -1) "$1" } @@ -106,19 +113,24 @@ fish ) cat </dev/null; or true + if test -n "\$PYENV_VERSION" \\ + -a "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\ + -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" + return \$ret end - set -l pvh_global "" - if test -f "\$PYENV_ROOT/version" - read -z pvh_global < "\$PYENV_ROOT/version" 2>/dev/null; or true + set -l d "\$PWD" + set -l paths + while true + set paths \$paths "\$d" "\$d/.python-version" + test "\$d" = "/"; and break + set d (string replace -r '/[^/]*\$' '' -- "\$d") + test -z "\$d"; and set d "/" end + set paths \$paths "\$PYENV_ROOT/version" if test "\$PWD" = "\$_PYENV_VH_PWD" \\ -a "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\ - -a "\$pvh_local" = "\$_PYENV_VH_LOCAL" \\ - -a "\$pvh_global" = "\$_PYENV_VH_GLOBAL" \\ - -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" + -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" \\ + -a "(stat ${_stat_fmt} \$paths 2>/dev/null)" = "\$_PYENV_VH_MTIMES" return \$ret end if [ -n "\$VIRTUAL_ENV" ] @@ -128,9 +140,8 @@ function _pyenv_virtualenv_hook --on-event fish_prompt; end set -g _PYENV_VH_PWD "\$PWD" set -g _PYENV_VH_VERSION "\$PYENV_VERSION" - set -g _PYENV_VH_LOCAL "\$pvh_local" - set -g _PYENV_VH_GLOBAL "\$pvh_global" set -g _PYENV_VH_VENV "\$VIRTUAL_ENV" + set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$paths 2>/dev/null) return \$ret end EOS @@ -150,19 +161,23 @@ esac if [[ "$shell" != "fish" ]]; then cat </dev/null || true - fi - local pvh_global="" - if [ -f "\${PYENV_ROOT}/version" ]; then - pvh_global=\$(< "\${PYENV_ROOT}/version") 2>/dev/null || true + if [ -n "\${PYENV_VERSION-}" ] \\ + && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + return \$ret fi + local _pvh_d="\${PWD}" _pvh_paths="" + while :; do + _pvh_paths="\${_pvh_paths} \${_pvh_d} \${_pvh_d}/.python-version" + [ "\${_pvh_d}" = "/" ] && break + _pvh_d="\${_pvh_d%/*}" + [ -z "\${_pvh_d}" ] && _pvh_d="/" + done + _pvh_paths="\${_pvh_paths} \${PYENV_ROOT}/version" if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ - && [ "\${pvh_local}" = "\${_PYENV_VH_LOCAL-}" ] \\ - && [ "\${pvh_global}" = "\${_PYENV_VH_GLOBAL-}" ] \\ - && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ] \\ + && [ "\$(stat ${_stat_fmt} \${_pvh_paths} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi if [ -n "\${VIRTUAL_ENV-}" ]; then @@ -172,9 +187,8 @@ if [[ "$shell" != "fish" ]]; then fi _PYENV_VH_PWD="\${PWD}" _PYENV_VH_VERSION="\${PYENV_VERSION-}" - _PYENV_VH_LOCAL="\${pvh_local}" - _PYENV_VH_GLOBAL="\${pvh_global}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_pvh_paths} 2>/dev/null)" return \$ret }; EOS diff --git a/test/init.bats b/test/init.bats index 7c0e6100..9f5da39e 100644 --- a/test/init.bats +++ b/test/init.bats @@ -54,19 +54,23 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}"; export PYENV_VIRTUALENV_INIT=1; _pyenv_virtualenv_hook() { local ret=\$? - local pvh_local="" - if [ -f "\${PWD}/.python-version" ]; then - pvh_local=\$(< "\${PWD}/.python-version") 2>/dev/null || true - fi - local pvh_global="" - if [ -f "\${PYENV_ROOT}/version" ]; then - pvh_global=\$(< "\${PYENV_ROOT}/version") 2>/dev/null || true + if [ -n "\${PYENV_VERSION-}" ] \\ + && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + return \$ret fi + local _pvh_d="\${PWD}" _pvh_paths="" + while :; do + _pvh_paths="\${_pvh_paths} \${_pvh_d} \${_pvh_d}/.python-version" + [ "\${_pvh_d}" = "/" ] && break + _pvh_d="\${_pvh_d%/*}" + [ -z "\${_pvh_d}" ] && _pvh_d="/" + done + _pvh_paths="\${_pvh_paths} \${PYENV_ROOT}/version" if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ - && [ "\${pvh_local}" = "\${_PYENV_VH_LOCAL-}" ] \\ - && [ "\${pvh_global}" = "\${_PYENV_VH_GLOBAL-}" ] \\ - && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ] \\ + && [ "\$(stat ${_stat_fmt} \${_pvh_paths} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi if [ -n "\${VIRTUAL_ENV-}" ]; then @@ -76,9 +80,8 @@ _pyenv_virtualenv_hook() { fi _PYENV_VH_PWD="\${PWD}" _PYENV_VH_VERSION="\${PYENV_VERSION-}" - _PYENV_VH_LOCAL="\${pvh_local}" - _PYENV_VH_GLOBAL="\${pvh_global}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_pvh_paths} 2>/dev/null)" return \$ret }; if ! [[ "\${PROMPT_COMMAND-}" =~ _pyenv_virtualenv_hook ]]; then @@ -98,19 +101,24 @@ 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 - set -l pvh_local "" - if test -f "\$PWD/.python-version" - read -z pvh_local < "\$PWD/.python-version" 2>/dev/null; or true + if test -n "\$PYENV_VERSION" \\ + -a "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\ + -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" + return \$ret end - set -l pvh_global "" - if test -f "\$PYENV_ROOT/version" - read -z pvh_global < "\$PYENV_ROOT/version" 2>/dev/null; or true + set -l d "\$PWD" + set -l paths + while true + set paths \$paths "\$d" "\$d/.python-version" + test "\$d" = "/"; and break + set d (string replace -r '/[^/]*\$' '' -- "\$d") + test -z "\$d"; and set d "/" end + set paths \$paths "\$PYENV_ROOT/version" if test "\$PWD" = "\$_PYENV_VH_PWD" \\ -a "\$PYENV_VERSION" = "\$_PYENV_VH_VERSION" \\ - -a "\$pvh_local" = "\$_PYENV_VH_LOCAL" \\ - -a "\$pvh_global" = "\$_PYENV_VH_GLOBAL" \\ - -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" + -a "\$VIRTUAL_ENV" = "\$_PYENV_VH_VENV" \\ + -a "(stat ${_stat_fmt} \$paths 2>/dev/null)" = "\$_PYENV_VH_MTIMES" return \$ret end if [ -n "\$VIRTUAL_ENV" ] @@ -120,9 +128,8 @@ function _pyenv_virtualenv_hook --on-event fish_prompt; end set -g _PYENV_VH_PWD "\$PWD" set -g _PYENV_VH_VERSION "\$PYENV_VERSION" - set -g _PYENV_VH_LOCAL "\$pvh_local" - set -g _PYENV_VH_GLOBAL "\$pvh_global" set -g _PYENV_VH_VENV "\$VIRTUAL_ENV" + set -g _PYENV_VH_MTIMES (stat ${_stat_fmt} \$paths 2>/dev/null) return \$ret end EOS @@ -137,19 +144,23 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}"; export PYENV_VIRTUALENV_INIT=1; _pyenv_virtualenv_hook() { local ret=\$? - local pvh_local="" - if [ -f "\${PWD}/.python-version" ]; then - pvh_local=\$(< "\${PWD}/.python-version") 2>/dev/null || true - fi - local pvh_global="" - if [ -f "\${PYENV_ROOT}/version" ]; then - pvh_global=\$(< "\${PYENV_ROOT}/version") 2>/dev/null || true + if [ -n "\${PYENV_VERSION-}" ] \\ + && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + return \$ret fi + local _pvh_d="\${PWD}" _pvh_paths="" + while :; do + _pvh_paths="\${_pvh_paths} \${_pvh_d} \${_pvh_d}/.python-version" + [ "\${_pvh_d}" = "/" ] && break + _pvh_d="\${_pvh_d%/*}" + [ -z "\${_pvh_d}" ] && _pvh_d="/" + done + _pvh_paths="\${_pvh_paths} \${PYENV_ROOT}/version" if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ && [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ - && [ "\${pvh_local}" = "\${_PYENV_VH_LOCAL-}" ] \\ - && [ "\${pvh_global}" = "\${_PYENV_VH_GLOBAL-}" ] \\ - && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then + && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ] \\ + && [ "\$(stat ${_stat_fmt} \${_pvh_paths} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi if [ -n "\${VIRTUAL_ENV-}" ]; then @@ -159,9 +170,8 @@ _pyenv_virtualenv_hook() { fi _PYENV_VH_PWD="\${PWD}" _PYENV_VH_VERSION="\${PYENV_VERSION-}" - _PYENV_VH_LOCAL="\${pvh_local}" - _PYENV_VH_GLOBAL="\${pvh_global}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_pvh_paths} 2>/dev/null)" return \$ret }; typeset -g -a precmd_functions diff --git a/test/test_helper.bash b/test/test_helper.bash index 3d179c9a..b1358b70 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -1,4 +1,11 @@ export TMP="$BATS_TEST_DIRNAME/tmp" + +# Detect stat format for mtime: GNU uses -c %Y, BSD uses -f %m +if stat -c %Y / >/dev/null 2>&1; then + _stat_fmt="-c %Y" +else + _stat_fmt="-f %m" +fi export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' PATH=/usr/bin:/usr/sbin:/bin:/sbin