pyenv-init | performance improvements (#3136)

* perf: replace a series of if statements with a case block. Add error
handling for case where unknown option is provided.
Same setup as rbenv-init for reading arguments.

* perf, docs: Recommend users to specify the shell for `pyenv init -`
Speeds up the startup by about 40% (in local testing, from ~50ms to ~30ms).
Reflect this in `pyenv init` hint text.

* style: remove unnecessary `root` variable in pyenv-init
* style: remove unnecessary variable declarations at the top of file in pyenv-init.

* perf: replace `cat <<` calls with `echo`
The builtin `echo` is about 100x faster. In tests, saves about 2-3ms.

* docs: document the `pyenv init - <shell>` performance boost in the Advanced Configuration section.

* style: test_helper.bash: avoid unnecessary ".." in produced PATH

* docs: fix a false statement about completions location in the Advanced Configuration section.
This commit is contained in:
Christian Fredrik Johnsen 2024-12-22 13:23:51 +01:00 committed by GitHub
parent 38421ba6aa
commit c6973391f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 69 additions and 83 deletions

View File

@ -191,27 +191,27 @@ See [Advanced configuration](#advanced-configuration) for details and more confi
```bash ```bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
``` ```
3. Then, if you have `~/.profile`, `~/.bash_profile` or `~/.bash_login`, add the commands there as well. 2. Then, if you have `~/.profile`, `~/.bash_profile` or `~/.bash_login`, add the commands there as well.
If you have none of these, create a `~/.profile` and add the commands there. If you have none of these, create a `~/.profile` and add the commands there.
* to add to `~/.profile`: * to add to `~/.profile`:
``` bash ``` bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init -)"' >> ~/.profile echo 'eval "$(pyenv init - bash)"' >> ~/.profile
``` ```
* to add to `~/.bash_profile`: * to add to `~/.bash_profile`:
```bash ```bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile echo 'eval "$(pyenv init - bash)"' >> ~/.bash_profile
``` ```
**Bash warning**: There are some systems where the `BASH_ENV` variable is configured **Bash warning**: There are some systems where the `BASH_ENV` variable is configured
to point to `.bashrc`. On such systems, you should almost certainly put the to point to `.bashrc`. On such systems, you should almost certainly put the
`eval "$(pyenv init -)"` line into `.bash_profile`, and **not** into `.bashrc`. Otherwise, you `eval "$(pyenv init - bash)"` line into `.bash_profile`, and **not** into `.bashrc`. Otherwise, you
may observe strange behaviour, such as `pyenv` getting into an infinite loop. may observe strange behaviour, such as `pyenv` getting into an infinite loop.
See [#264](https://github.com/pyenv/pyenv/issues/264) for details. See [#264](https://github.com/pyenv/pyenv/issues/264) for details.
@ -224,7 +224,7 @@ See [Advanced configuration](#advanced-configuration) for details and more confi
```zsh ```zsh
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init -)"' >> ~/.zshrc echo 'eval "$(pyenv init - zsh)"' >> ~/.zshrc
``` ```
If you wish to get Pyenv in noninteractive login shells as well, also add the commands to `~/.zprofile` or `~/.zlogin`. If you wish to get Pyenv in noninteractive login shells as well, also add the commands to `~/.zprofile` or `~/.zlogin`.
@ -248,7 +248,7 @@ See [Advanced configuration](#advanced-configuration) for details and more confi
3. Now, add this to `~/.config/fish/config.fish`: 3. Now, add this to `~/.config/fish/config.fish`:
~~~ fish ~~~ fish
pyenv init - | source pyenv init - fish | source
~~~ ~~~
</details> </details>
@ -656,23 +656,25 @@ for the environment variables that control Pyenv's behavior.
extra commands into your shell. Coming from RVM, some of you might be extra commands into your shell. Coming from RVM, some of you might be
opposed to this idea. Here's what `eval "$(pyenv init -)"` actually does: opposed to this idea. Here's what `eval "$(pyenv init -)"` actually does:
1. **Finds current shell.**
`pyenv init` figures out what shell you are using, as the exact commands of `eval "$(pyenv init -)"` vary depending on shell. Specifying which shell you are using (e.g. `eval "$(pyenv init - bash)"`) is preferred, because it reduces launch time significantly.
1. **Sets up the shims path.** This is what allows Pyenv to intercept 2. **Sets up the shims path.** This is what allows Pyenv to intercept
and redirect invocations of `python`, `pip` etc. transparently. and redirect invocations of `python`, `pip` etc. transparently.
It prepends `$(pyenv root)/shims` to your `$PATH`. It prepends `$(pyenv root)/shims` to your `$PATH`.
It also deletes any other instances of `$(pyenv root)/shims` on `PATH` It also deletes any other instances of `$(pyenv root)/shims` on `PATH`
which allows to invoke `eval "$(pyenv init -)"` multiple times without which allows to invoke `eval "$(pyenv init -)"` multiple times without
getting duplicate `PATH` entries. getting duplicate `PATH` entries.
2. **Installs autocompletion.** This is entirely optional but pretty 3. **Installs autocompletion.** This is entirely optional but pretty
useful. Sourcing `$(pyenv root)/completions/pyenv.bash` will set that useful. Sourcing `<pyenv installation prefix>/completions/pyenv.bash` will set that
up. There are also completions for Zsh and Fish. up. There are also completions for Zsh and Fish.
3. **Rehashes shims.** From time to time you'll need to rebuild your 4. **Rehashes shims.** From time to time you'll need to rebuild your
shim files. Doing this on init makes sure everything is up to shim files. Doing this on init makes sure everything is up to
date. You can always run `pyenv rehash` manually. date. You can always run `pyenv rehash` manually.
4. **Installs `pyenv` into the current shell as a shell function.** 5. **Installs `pyenv` into the current shell as a shell function.**
This bit is also optional, but allows This bit is also optional, but allows
pyenv and plugins to change variables in your current shell. pyenv and plugins to change variables in your current shell.
This is required for some commands like `pyenv shell` to work. This is required for some commands like `pyenv shell` to work.
@ -681,7 +683,7 @@ opposed to this idea. Here's what `eval "$(pyenv init -)"` actually does:
for some reason you need `pyenv` to be a real script rather than a for some reason you need `pyenv` to be a real script rather than a
shell function, you can safely skip it. shell function, you can safely skip it.
`eval "$(pyenv init --path)"` only does items 1 and 3. `eval "$(pyenv init --path)"` only does items 2 and 4.
To see exactly what happens under the hood for yourself, run `pyenv init -` To see exactly what happens under the hood for yourself, run `pyenv init -`
or `pyenv init --path`. or `pyenv init --path`.

View File

@ -20,37 +20,32 @@ if [ "$1" = "--complete" ]; then
fi fi
mode="help" mode="help"
no_rehash=""
no_push_path=""
for args in "$@"
do
if [ "$args" = "-" ]; then
mode="print"
shift
fi
if [ "$args" = "--path" ]; then while [ "$#" -gt 0 ]; do
mode="path" case "$1" in
shift -)
fi mode="print"
;;
if [ "$args" = "--detect-shell" ]; then --path)
mode="detect-shell" mode="path"
shift ;;
fi --detect-shell)
mode="detect-shell"
if [ "$args" = "--no-push-path" ]; then ;;
no_push_path=1 --no-push-path)
shift no_push_path=1
fi ;;
--no-rehash)
if [ "$args" = "--no-rehash" ]; then no_rehash=1
no_rehash=1 ;;
shift *)
fi shell="$1"
;;
esac
shift
done done
shell="$1" # If shell is not provided, detect it.
if [ -z "$shell" ]; then if [ -z "$shell" ]; then
shell="$(ps -p "$PPID" -o 'args=' 2>/dev/null || true)" shell="$(ps -p "$PPID" -o 'args=' 2>/dev/null || true)"
shell="${shell%% *}" shell="${shell%% *}"
@ -60,8 +55,6 @@ if [ -z "$shell" ]; then
shell="${shell%%-*}" shell="${shell%%-*}"
fi fi
root="${0%/*}/.."
function main() { function main() {
case "$mode" in case "$mode" in
"help") "help")
@ -150,7 +143,7 @@ function help_() {
echo "# Load pyenv automatically by appending" echo "# Load pyenv automatically by appending"
echo "# the following to ~/.config/fish/config.fish:" echo "# the following to ~/.config/fish/config.fish:"
echo echo
echo 'pyenv init - | source' echo 'pyenv init - fish | source'
echo echo
;; ;;
* ) * )
@ -166,7 +159,7 @@ function help_() {
echo echo
echo 'export PYENV_ROOT="$HOME/.pyenv"' echo 'export PYENV_ROOT="$HOME/.pyenv"'
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"'
echo 'eval "$(pyenv init -)"' echo 'eval "$(pyenv init -'$shell')"'
;; ;;
esac esac
echo echo
@ -244,7 +237,7 @@ function print_env() {
} }
function print_completion() { function print_completion() {
completion="${root}/completions/pyenv.${shell}" completion="${0%/*/*}/completions/pyenv.${shell}"
if [ -r "$completion" ]; then if [ -r "$completion" ]; then
echo "source '$completion'" echo "source '$completion'"
fi fi
@ -260,52 +253,44 @@ function print_shell_function() {
commands=(`pyenv-commands --sh`) commands=(`pyenv-commands --sh`)
case "$shell" in case "$shell" in
fish ) fish )
cat <<EOS echo \
function pyenv 'function pyenv
set command \$argv[1] set command $argv[1]
set -e argv[1] set -e argv[1]
switch "\$command" switch "$command"
case ${commands[*]} case '"${commands[*]}"'
source (pyenv "sh-\$command" \$argv|psub) source (pyenv "sh-$command" $argv|psub)
case '*' case "*"
command pyenv "\$command" \$argv command pyenv "$command" $argv
end end
end end'
EOS
;; ;;
ksh | ksh93 | mksh ) ksh | ksh93 | mksh )
cat <<EOS echo \
function pyenv { 'function pyenv {
typeset command typeset command=${1:-}'
EOS
;; ;;
* ) * )
cat <<EOS echo \
pyenv() { 'pyenv() {
local command local command=${1:-}'
EOS
;; ;;
esac esac
if [ "$shell" != "fish" ]; then if [ "$shell" != "fish" ]; then
IFS="|" IFS="|"
cat <<EOS echo \
command="\${1:-}" ' [ "$#" -gt 0 ] && shift
if [ "\$#" -gt 0 ]; then case "$command" in
shift '"${commands[*]:-/}"')
fi eval "$(pyenv "sh-$command" "$@")"
case "\$command" in
${commands[*]:-/})
eval "\$(pyenv "sh-\$command" "\$@")"
;; ;;
*) *)
command pyenv "\$command" "\$@" command pyenv "$command" "$@"
;; ;;
esac esac
} }'
EOS
fi fi
} }

View File

@ -35,12 +35,11 @@ create_executable() {
assert_line "command pyenv rehash 2>/dev/null" assert_line "command pyenv rehash 2>/dev/null"
} }
@test "setup shell completions" { @test "setup shell completions" {
root="$(cd $BATS_TEST_DIRNAME/.. && pwd)" exec_root="$(cd $BATS_TEST_DIRNAME/.. && pwd)"
run pyenv-init - bash run pyenv-init - bash
assert_success assert_success
assert_line "source '${root}/test/../libexec/../completions/pyenv.bash'" assert_line "source '${exec_root}/completions/pyenv.bash'"
} }
@test "detect parent shell" { @test "detect parent shell" {
@ -63,16 +62,16 @@ OUT
} }
@test "setup shell completions (fish)" { @test "setup shell completions (fish)" {
root="$(cd $BATS_TEST_DIRNAME/.. && pwd)" exec_root="$(cd $BATS_TEST_DIRNAME/.. && pwd)"
run pyenv-init - fish run pyenv-init - fish
assert_success assert_success
assert_line "source '${root}/test/../libexec/../completions/pyenv.fish'" assert_line "source '${exec_root}/completions/pyenv.fish'"
} }
@test "fish instructions" { @test "fish instructions" {
run pyenv-init fish run pyenv-init fish
assert [ "$status" -eq 1 ] assert [ "$status" -eq 1 ]
assert_line 'pyenv init - | source' assert_line 'pyenv init - fish | source'
} }
@test "shell detection for installer" { @test "shell detection for installer" {

View File

@ -21,7 +21,7 @@ if [ -z "$PYENV_TEST_DIR" ]; then
PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
PATH="${PYENV_TEST_DIR}/bin:$PATH" PATH="${PYENV_TEST_DIR}/bin:$PATH"
PATH="${BATS_TEST_DIRNAME}/../libexec:$PATH" PATH="${BATS_TEST_DIRNAME%/*}/libexec:$PATH"
PATH="${BATS_TEST_DIRNAME}/libexec:$PATH" PATH="${BATS_TEST_DIRNAME}/libexec:$PATH"
PATH="${PYENV_ROOT}/shims:$PATH" PATH="${PYENV_ROOT}/shims:$PATH"
export PATH export PATH