Incorporate the pyenv-link plugin

* Adapt test_helper for use in plugins
* Add pyenv-link tests
* Add pyenv-link tests to CI
This commit is contained in:
macayu17 2026-09-08 09:42:50 +03:00 committed by Ivan Pozdeev
parent fb7f2071fb
commit 798a1ad603
8 changed files with 158 additions and 79 deletions

View file

@ -10,11 +10,14 @@ TEST_PYTHON_BUILD_DOCKER_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuf
TEST_BINARY_DOCKER_PREFIX = test-binary-docker TEST_BINARY_DOCKER_PREFIX = test-binary-docker
TEST_BINARY_DOCKER_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_BINARY_DOCKER_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_BINARY_DOCKER_PREFIX))) TEST_BINARY_DOCKER_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_BINARY_DOCKER_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_BINARY_DOCKER_PREFIX)))
TEST_LINK_DOCKER_PREFIX = test-link-docker
TEST_LINK_DOCKER_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_LINK_DOCKER_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_LINK_DOCKER_PREFIX)))
TEST_BATS_IMAGE_PREFIX = test-pyenv-docker-image TEST_BATS_IMAGE_PREFIX = test-pyenv-docker-image
TEST_BATS_IMAGE_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_BATS_IMAGE_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_BATS_IMAGE_PREFIX))) TEST_BATS_IMAGE_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_BATS_IMAGE_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_BATS_IMAGE_PREFIX)))
.PHONY: test-docker .PHONY: test-docker
test-docker: $(TEST_UNIT_DOCKER_PREFIX) $(TEST_PYTHON_BUILD_DOCKER_PREFIX) $(TEST_BINARY_DOCKER_PREFIX) test-docker: $(TEST_UNIT_DOCKER_PREFIX) $(TEST_PYTHON_BUILD_DOCKER_PREFIX) $(TEST_BINARY_DOCKER_PREFIX) $(TEST_LINK_DOCKER_PREFIX)
.PHONY: $(TEST_UNIT_DOCKER_PREFIX) .PHONY: $(TEST_UNIT_DOCKER_PREFIX)
$(TEST_UNIT_DOCKER_PREFIX): $(TEST_UNIT_DOCKER_TARGETS) $(TEST_UNIT_DOCKER_PREFIX): $(TEST_UNIT_DOCKER_TARGETS)
@ -88,6 +91,29 @@ $(TEST_BINARY_DOCKER_TARGETS): $(TEST_BINARY_DOCKER_PREFIX)-% : $(TEST_BATS_IMAG
bats $${CI:+-F "/code/test/libexec/bats-format-tap-suite"} \ bats $${CI:+-F "/code/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/pyenv-binary/test/$${BATS_FILE_FILTER} $${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/pyenv-binary/test/$${BATS_FILE_FILTER}
.PHONY: $(TEST_LINK_DOCKER_PREFIX)
$(TEST_LINK_DOCKER_PREFIX): $(TEST_LINK_DOCKER_TARGETS)
.PHONY: $(TEST_LINK_DOCKER_TARGETS)
$(TEST_LINK_DOCKER_TARGETS): DOCKER_IMAGE = $(TEST_BATS_IMAGE_PREFIX)
$(TEST_LINK_DOCKER_TARGETS): GNU = $(if $(findstring -gnu-,$@),True,False)
$(TEST_LINK_DOCKER_TARGETS): BASH = $(filter $(TEST_BASH_VERSIONS),$(subst -, ,$@))
$(TEST_LINK_DOCKER_TARGETS): DOCKER_TAG = bash-$(BASH)-gnu-$(GNU)
$(TEST_LINK_DOCKER_TARGETS): INTERACTIVE = $(if $(findstring true,$(CI)),,-ti)
$(TEST_LINK_DOCKER_TARGETS): $(TEST_LINK_DOCKER_PREFIX)-% : $(TEST_BATS_IMAGE_PREFIX)-%
$(info Running test with docker image '$(DOCKER_IMAGE):$(DOCKER_TAG)')
docker run \
--init \
-v $(PWD):/code:ro \
-v /etc/passwd:/etc/passwd:ro \
-v /etc/group:/etc/group:ro \
-u "$$(id -u $$(whoami)):$$(id -g $$(whoami))" \
$${CI+-e CI="$${CI}"} \
$(INTERACTIVE) \
$(DOCKER_IMAGE):$(DOCKER_TAG) \
bats $${CI:+-F "/code/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/pyenv-link/test/$${BATS_FILE_FILTER}
# Build all images needed for bats under docker # Build all images needed for bats under docker
.PHONY: $(TEST_BATS_IMAGE_PREFIX) .PHONY: $(TEST_BATS_IMAGE_PREFIX)
$(TEST_BATS_IMAGE_PREFIX): $(TEST_BATS_IMAGE_TARGETS) $(TEST_BATS_IMAGE_PREFIX): $(TEST_BATS_IMAGE_TARGETS)
@ -110,13 +136,13 @@ $(TEST_BATS_IMAGE_TARGETS):
./ ; \ ./ ; \
fi fi
.PHONY: test test-unit test-python-build test-binary .PHONY: test test-unit test-python-build test-binary test-link
# Do not pass in user flags to build tests. # Do not pass in user flags to build tests.
unexport PYTHON_CFLAGS unexport PYTHON_CFLAGS
unexport PYTHON_CONFIGURE_OPTS unexport PYTHON_CONFIGURE_OPTS
test: test-unit test-python-build test-binary test: test-unit test-python-build test-binary test-link
test-unit: bats test-unit: bats
PATH="./bats/bin:$$PATH" test/run PATH="./bats/bin:$$PATH" test/run
@ -129,6 +155,10 @@ test-binary: bats
cd plugins/pyenv-binary && $(PWD)/bats/bin/bats $${CI:+-F "$(PWD)/test/libexec/bats-format-tap-suite"} \ cd plugins/pyenv-binary && $(PWD)/bats/bin/bats $${CI:+-F "$(PWD)/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER} $${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER}
test-link: bats
cd plugins/pyenv-link && $(PWD)/bats/bin/bats $${CI:+-F "$(PWD)/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER}
.SECONDARY: bats-$(TEST_BATS_VERSION) .SECONDARY: bats-$(TEST_BATS_VERSION)
bats-$(TEST_BATS_VERSION): bats-$(TEST_BATS_VERSION):
git clone --depth 1 --branch $(TEST_BATS_VERSION) https://github.com/bats-core/bats-core.git bats-$(TEST_BATS_VERSION) git clone --depth 1 --branch $(TEST_BATS_VERSION) https://github.com/bats-core/bats-core.git bats-$(TEST_BATS_VERSION)

1
plugins/.gitignore vendored
View file

@ -3,4 +3,5 @@
!/version-ext-compat !/version-ext-compat
!/python-build !/python-build
!/pyenv-binary !/pyenv-binary
!/pyenv-link
/python-build/test/build /python-build/test/build

View file

@ -1,40 +1,15 @@
# pyenv-link # pyenv-link
A [pyenv](https://github.com/pyenv/pyenv) plugin for linking python versions and virtual envs into your pyenv root. A [pyenv](https://github.com/pyenv/pyenv) plugin for linking Python installations and virtual environments into your pyenv root.
This plugin recommends the [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv/) plugin. This plugin recommends the [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv/) plugin.
## Installation
This will install the latest development version of *pyenv-link* into
the `$(pyenv root)/plugins/pyenv-link` directory.
**Important note:** If you installed pyenv into a non-standard directory, make
sure that you clone this repo into the 'plugins' directory of wherever you
installed into.
From inside that directory you can:
- Check out a specific release tag.
- Get the latest development release by running `git pull` to download the
latest changes.
```bash
git clone https://github.com/real-yfprojects/pyenv-link.git $(pyenv root)/plugins/pyenv-link
```
For the Fish shell:
```fish
git clone https://github.com/real-yfprojects/pyenv-link.git (pyenv root)/plugins/pyenv-link
```
## Usage ## Usage
Make an arbitrary virtualenv available through pyenv. This automatically guesses a fitting name from the prompt, the directory name or the location of the venv. Make an arbitrary virtualenv available through pyenv. This automatically guesses a fitting name from the prompt, the directory name or the location of the venv.
```console ```console
$ pyenv link version .venv $ pyenv link version .venv
Linked new version named myproject-py3.10 Linked new version named myproject
``` ```
You can also specify a name to use for the venv. You can also specify a name to use for the venv.
@ -44,19 +19,15 @@ $ pyenv link version .venv myname
Linked new version named myname Linked new version named myname
``` ```
The same command can give a platform-specific binary installation a shorter name:
```console
$ pyenv link version "$(pyenv root)/versions/3.13.14-linux-x86_64" 3.13.14
Linked new version named 3.13.14
```
Now you can make pyenv activate/use the venv automatically: Now you can make pyenv activate/use the venv automatically:
```console ```console
$ pyenv local myproject-py3.10 $ pyenv local myproject
``` ```
## Contributing
This project is developed in an open-source, community-driven way, as a voluntary effort in the authors free time.
All kinds of contributions are greatly appreciated. This not only includes implementing new features but also refactoring work or improvements to the documentation.
Bug reports, feature requests and other suggestions are welcome too.
### TODO
- [ ] Tests
- [ ] CI

View file

@ -1,13 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Summary: Activate virtual environment # Summary: Link a Python environment as a pyenv version
# #
# Usage: pyenv link version <virtualenv> # Usage: pyenv link version [--dry] [--quiet] <path> [<name>]
# #
# Link a virtual python environment to pyenvs version directory # Link a virtual python environment to pyenvs version directory
# so that the venv can be used like one created with *pyenv-virtualenv*. # so that the venv can be used like one created with *pyenv-virtualenv*.
# #
# <virtualenv> should be a path to a virtual python environments location. # <path> should be a path to a Python installation or virtual environment.
set -e set -e
[ -n "$PYENV_DEBUG" ] && set -x [ -n "$PYENV_DEBUG" ] && set -x

View file

@ -1,13 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Summary: Activate virtual environment # Summary: Link a Python environment as a pyenv version
# #
# Usage: pyenv link-venv [--dry] [--quiet] <virtualenv> [<name>] # Usage: pyenv link version [--dry] [--quiet] <path> [<name>]
# #
# Link a virtual python environment to pyenvs version directory # Link a virtual python environment to pyenvs version directory
# so that the venv can be used like one created with *pyenv-virtualenv*. # so that the venv can be used like one created with *pyenv-virtualenv*.
# #
# <virtualenv> should be a path to a virtual python environments location. # <path> should be a path to a Python installation or virtual environment.
# <name> should be string used as a version name it may not be present # <name> should be string used as a version name it may not be present
# in the pyenv version directory yet. # in the pyenv version directory yet.
# If not specified a matching name is guessed from pyvenv.cfg # If not specified a matching name is guessed from pyvenv.cfg
@ -26,24 +26,10 @@ if ! {
# work-around when realpath is unavailable # work-around when realpath is unavailable
realpath() { realpath() {
echo "$(cd "$1" && pwd)" (cd "$1" && pwd)
} }
fi fi
array_doesnt_contain() {
local array="$1[@]"
local seeking=$2
local in=0
for element in "${!array}"; do
if [[ $element == "$seeking" ]]; then
in=1
break
fi
done
return $in
}
COMMON_VENV_NAMES=('venv' 'env' '.venv' '.env' 'ENV' 'VENV')
guess_name() { guess_name() {
local venv_dir=$1 local venv_dir=$1
local name local name
@ -53,12 +39,20 @@ guess_name() {
if [ -f "$pyvenv_cfg_path" ]; then if [ -f "$pyvenv_cfg_path" ]; then
# if `prompt` key wasn't found the var remains empty # if `prompt` key wasn't found the var remains empty
name=$(cut -b 1-1024 "$pyvenv_cfg_path" | sed -n '/^ *prompt *= */s///p') name=$(cut -b 1-1024 "$pyvenv_cfg_path" | sed -n '/^ *prompt *= */s///p')
case "$name" in
\"*\" | \'*\')
name=${name:1:${#name}-2}
;;
esac
fi fi
# guess venv name from name of venv directory # guess venv name from name of venv directory
local venv_dir_name=${venv_dir##*/} local venv_dir_name=${venv_dir##*/}
if [ -z "$name" ] && array_doesnt_contain COMMON_VENV_NAMES "$venv_dir_name"; then if [ -z "$name" ]; then
name=$venv_dir_name case "$venv_dir_name" in
venv | env | .venv | .env | ENV | VENV) ;;
*) name=$venv_dir_name ;;
esac
fi fi
# guess venv name from name of the parent directory of the venv directory # guess venv name from name of the parent directory of the venv directory
@ -120,26 +114,37 @@ if [ -z "$venv_name" ]; then
venv_name=$(guess_name "$venv_dir") venv_name=$(guess_name "$venv_dir")
fi fi
# check whether version exists already case "$venv_name" in
if pyenv-prefix "$venv_name" >/dev/null 2>&1; then "" | */* | . | .. | *:* | system | *[[:cntrl:]]* )
echo "pyenv-link: invalid version name \`${venv_name}'" >&2
exit 1
;;
esac
versions_dir="$PYENV_ROOT/versions"
pyenv_prefix_path="$versions_dir/$venv_name"
if [ -e "$pyenv_prefix_path" ] || [ -L "$pyenv_prefix_path" ]; then
echo Version "$venv_name" already exists >&2 echo Version "$venv_name" already exists >&2
exit 3 exit 3
fi fi
# construct location to link to case "$venv_dir" in
pyenv_prefix_path="$PYENV_ROOT/versions/$venv_name" "$versions_dir"/*)
link_target=${venv_dir#"$versions_dir"/}
;;
*)
link_target=$venv_dir
;;
esac
# link to pyenv version directory # link to pyenv version directory
if [ -z "$dry" ]; then if [ -z "$dry" ]; then
# Handle OS types mkdir -p "$versions_dir"
case "$(uname -s)" in ln -s "$link_target" "$pyenv_prefix_path"
Darwin*)
# long form flags aren't supported
ln -s "$venv_dir" "$pyenv_prefix_path";;
*)
ln --symbolic "$venv_dir" "$pyenv_prefix_path";;
esac
fi fi
# output # output
[ -z "$quiet" ] && echo Linked new version named "$venv_name" || true if [ -z "$quiet" ]; then
echo "Linked new version named $venv_name"
fi

View file

@ -0,0 +1,61 @@
#!/usr/bin/env bats
load test_helper
@test "link completions use the dispatcher" {
run pyenv completions link
assert_success
assert_output <<OUT
--help
version
OUT
run pyenv completions link version
assert_success
assert_output <<OUT
--help
--dry
--quiet
OUT
}
@test "link requires a version path" {
run pyenv-link version
assert_failure "Usage: pyenv link version [--dry] [--quiet] <path> [<name>]"
}
@test "links an installed binary under its plain version name" {
create_alt_executable_in_version "3.13.14-linux-x86_64" python 'echo linked-python'
run pyenv-link version "$PYENV_ROOT/versions/3.13.14-linux-x86_64" 3.13.14
assert_success "Linked new version named 3.13.14"
assert_equal "3.13.14-linux-x86_64" "$(readlink "$PYENV_ROOT/versions/3.13.14")"
PYENV_VERSION=3.13.14 run pyenv exec python
assert_success "linked-python"
}
@test "links external paths containing spaces into a fresh root" {
mkdir -p "$PYENV_TEST_DIR/external env/bin"
run pyenv-link version "$PYENV_TEST_DIR/external env" custom
assert_success "Linked new version named custom"
assert_equal "$PYENV_TEST_DIR/external env" "$(readlink "$PYENV_ROOT/versions/custom")"
}
@test "refuses a dangling destination link" {
mkdir -p "$PYENV_ROOT/versions" "$PYENV_TEST_DIR/source"
ln -s missing "$PYENV_ROOT/versions/dangling"
run pyenv-link version "$PYENV_TEST_DIR/source" dangling
assert_failure "Version dangling already exists"
assert_equal missing "$(readlink "$PYENV_ROOT/versions/dangling")"
}
@test "rejects invalid version names" {
mkdir -p "$PYENV_TEST_DIR/source"
for name in ../outside . .. 'foo/bar' 'foo:bar' system; do
run pyenv-link version "$PYENV_TEST_DIR/source" "$name"
assert_failure "pyenv-link: invalid version name \`$name'"
assert [ ! -e "$PYENV_ROOT/outside" ]
done
echo "prompt = '../outside'" > "$PYENV_TEST_DIR/source/pyvenv.cfg"
run pyenv-link version "$PYENV_TEST_DIR/source"
assert_failure "pyenv-link: invalid version name \`../outside'"
assert [ ! -e "$PYENV_ROOT/outside" ]
}

View file

@ -0,0 +1 @@
../../../test/test_helper.bash

View file

@ -2,8 +2,14 @@ unset PYENV_VERSION
unset PYENV_DIR unset PYENV_DIR
setup() { setup() {
export _PYENV_INSTALL_PREFIX="${BATS_TEST_DIRNAME%/*}"
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
export _PYENV_INSTALL_PREFIX="${BATS_TEST_DIRNAME%/*}"
local PLUGIN_PREFIX
if [[ $_PYENV_INSTALL_PREFIX =~ /plugins/[^/]+$ ]]; then
PLUGIN_PREFIX="$_PYENV_INSTALL_PREFIX"
_PYENV_INSTALL_PREFIX="${_PYENV_INSTALL_PREFIX::${#_PYENV_INSTALL_PREFIX}-${#BASH_REMATCH[0]}}"
fi
if ! enable -f "${_PYENV_INSTALL_PREFIX}"/libexec/pyenv-realpath.dylib realpath 2>/dev/null; then if ! enable -f "${_PYENV_INSTALL_PREFIX}"/libexec/pyenv-realpath.dylib realpath 2>/dev/null; then
if [ -n "$PYENV_NATIVE_EXT" ]; then if [ -n "$PYENV_NATIVE_EXT" ]; then
echo "pyenv: failed to load \`realpath' builtin" >&2 echo "pyenv: failed to load \`realpath' builtin" >&2
@ -27,6 +33,9 @@ setup() {
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="${_PYENV_INSTALL_PREFIX}/libexec:$PATH" PATH="${_PYENV_INSTALL_PREFIX}/libexec:$PATH"
if [[ -n $PLUGIN_PREFIX ]]; then
PATH="${PLUGIN_PREFIX}/libexec:${PLUGIN_PREFIX}/bin:$PATH"
fi
PATH="${BATS_TEST_DIRNAME}/libexec:$PATH" PATH="${BATS_TEST_DIRNAME}/libexec:$PATH"
PATH="${PYENV_ROOT}/shims:$PATH" PATH="${PYENV_ROOT}/shims:$PATH"
PATH="${BATS_TEST_TMPDIR}/stubs:$PATH" PATH="${BATS_TEST_TMPDIR}/stubs:$PATH"
@ -40,6 +49,7 @@ setup() {
# even if its output is redirected, breaking the comparison logic # even if its output is redirected, breaking the comparison logic
export NO_COLOR=1 export NO_COLOR=1
# If test specific setup exist, run it # If test specific setup exist, run it
if [[ $(type -t _setup) == function ]];then if [[ $(type -t _setup) == function ]];then
_setup _setup