From f46b669f1d77e87cf0ff99795106b262ca285071 Mon Sep 17 00:00:00 2001 From: yfprojects <62463991+real-yfprojects@users.noreply.github.com> Date: Tue, 6 Jun 2023 11:09:59 +0000 Subject: [PATCH 1/8] Initial commit --- LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b32e7f6d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 yfprojects + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..882dbf34 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# pyenv-link +A pyenv plugin for linking python versions and virtual envs into your pyenv root. From dffc750fa2c21c4d81deeff92aa2453e01204039 Mon Sep 17 00:00:00 2001 From: real-yfprojects Date: Tue, 6 Jun 2023 16:03:16 +0200 Subject: [PATCH 2/8] Initial implementation * bin/pyenv-link * bin/pyenv-link-version --- bin/pyenv-link | 31 +++++++++ bin/pyenv-link-version | 138 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100755 bin/pyenv-link create mode 100755 bin/pyenv-link-version diff --git a/bin/pyenv-link b/bin/pyenv-link new file mode 100755 index 00000000..fffb9692 --- /dev/null +++ b/bin/pyenv-link @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# +# Summary: Activate virtual environment +# +# Usage: pyenv link version +# +# Link a virtual python environment to pyenvs version directory +# so that the venv can be used like one created with *pyenv-virtualenv*. +# +# should be a path to a virtual python environments location. + +set -e +[ -n "$PYENV_DEBUG" ] && set -x + +# Provide pyenv completions +case "$1" in +--complete) + echo --version + pyenv-link-version --complete + exit + ;; +version) + shift + pyenv-link-version "$@" + exit $? + ;; +*) + pyenv-help --usage link >&2 + exit 1 + ;; +esac diff --git a/bin/pyenv-link-version b/bin/pyenv-link-version new file mode 100755 index 00000000..c2fc9c29 --- /dev/null +++ b/bin/pyenv-link-version @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# +# Summary: Activate virtual environment +# +# Usage: pyenv link-venv [--dry] [--quiet] [] +# +# Link a virtual python environment to pyenvs version directory +# so that the venv can be used like one created with *pyenv-virtualenv*. +# +# should be a path to a virtual python environments location. +# should be string used as a version name it may not be present +# in the pyenv version directory yet. +# If not specified a matching name is guessed from pyvenv.cfg +# or directory name of the venv. +set -e +[ -n "$PYENV_DEBUG" ] && set -x + +# functions + +if ! { + enable -f "${BASH_SOURCE%/*}"/../../../libexec/pyenv-realpath.dylib realpath || + type realpath # realpath available? +} >/dev/null 2>&1; then + # realpath requires GNU coreutils to be installed. That's why its bundled with pyenv + echo realpath not available >&2 + + # work-around when realpath is unavailable + realpath() { + echo "$(cd "$1" && pwd)" + } +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() { + local venv_dir=$1 + local name + + # guess venv name from pyvenv.cfg + local pyvenv_cfg_path=$venv_dir/pyvenv.cfg + if [ -f "$pyvenv_cfg_path" ]; then + # if `prompt` key wasn't found the var remains empty + name=$(cut -b 1-1024 "$pyvenv_cfg_path" | sed -n '/^ *prompt *= */s///p') + fi + + # guess venv name from name of venv directory + local venv_dir_name=${venv_dir##*/} + if [ -z "$name" ] && array_doesnt_contain COMMON_VENV_NAMES "$venv_dir_name"; then + name=$venv_dir_name + fi + + # guess venv name from name of the parent directory of the venv directory + local venv_dir_parent=${venv_dir%/*} + local venv_dir_parent_name=${venv_dir_parent##*/} + if [ -z "$name" ]; then + name=$venv_dir_parent_name + fi + + echo "$name" +} + +# process args +POSARGS=() +# Provide pyenv completions +if [ "$1" = "--complete" ]; then + echo --dry + echo --quiet + exit +fi + +for arg in "$@"; do + case $arg in + --dry) + dry=true # any value + ;; + --quiet) + quiet=true # any value + ;; + -*) + pyenv-help --usage link-version >&2 + exit 1 + ;; + *) + POSARGS+=("$arg") + ;; + esac +done + +if [ ! ${#POSARGS[@]} -le 2 ] || [ ! ${#POSARGS[@]} -ge 1 ]; then + pyenv-help --usage link-version >&2 + exit 1 +fi + +venv_dir=${POSARGS[0]%/} +venv_name=${POSARGS[1]} + +# Check venv exists +if [ ! -d "$venv_dir" ]; then + echo "The virtual env you specified doesn't exist" + exit 2 +fi + +# make venv dir absolute +venv_dir=$(realpath "$venv_dir") + +# determine venv name +if [ -z "$venv_name" ]; then + venv_name=$(guess_name "$venv_dir") +fi + +# check whether version exists already +if pyenv-prefix "$venv_name"; then + echo Version "$venv_name" already exists >&2 + exit 3 +fi + +# construct location to link to +pyenv_prefix_path="$PYENV_ROOT/versions/$venv_name" + +# link to pyenv version directory +if [ -z "$dry" ]; then + ln --symbolic "$venv_dir" "$pyenv_prefix_path" +fi + +# output +[ -z "$quiet" ] && echo Linked new version named "$venv_name" || true From d428be59ec28352544348b99ef85b81b3f61c3d2 Mon Sep 17 00:00:00 2001 From: real-yfprojects Date: Tue, 6 Jun 2023 16:39:52 +0200 Subject: [PATCH 3/8] Write README. * README.md --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 882dbf34..67fa6379 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,62 @@ # pyenv-link -A pyenv plugin for linking python versions and virtual envs into your pyenv root. +A [pyenv](https://github.com/pyenv/pyenv) plugin for linking python versions and virtual envs into your pyenv root. + +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 + +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 +$ pyenv link version .venv +Linked new version named myproject-py3.10 +``` + +You can also specify a name to use for the venv. + +```console +$ pyenv link version .venv myname +Linked new version named myname +``` + +Now you can make pyenv activate/use the venv automatically: + +```console +$ pyenv local myproject-py3.10 +``` + +## 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 \ No newline at end of file From ba9099392a152841930eabf782726f70f04d3b71 Mon Sep 17 00:00:00 2001 From: real-yfprojects Date: Tue, 6 Jun 2023 16:48:19 +0200 Subject: [PATCH 4/8] Fix completions * bin/pyenv-link --- bin/pyenv-link | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bin/pyenv-link b/bin/pyenv-link index fffb9692..1992e229 100755 --- a/bin/pyenv-link +++ b/bin/pyenv-link @@ -15,9 +15,12 @@ set -e # Provide pyenv completions case "$1" in --complete) - echo --version - pyenv-link-version --complete - exit + if [ -z "$2" ]; then + echo version + else + shift 2 + pyenv-link-version --complete "$@" + fi ;; version) shift From b8d8dee191c04a1fc10ba2b2f39f131dceb18e8a Mon Sep 17 00:00:00 2001 From: real-yfprojects Date: Tue, 6 Jun 2023 16:59:25 +0200 Subject: [PATCH 5/8] Fix output * bin/pyenv-link-version --- bin/pyenv-link-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pyenv-link-version b/bin/pyenv-link-version index c2fc9c29..55a1102d 100755 --- a/bin/pyenv-link-version +++ b/bin/pyenv-link-version @@ -121,7 +121,7 @@ if [ -z "$venv_name" ]; then fi # check whether version exists already -if pyenv-prefix "$venv_name"; then +if pyenv-prefix "$venv_name" >/dev/null 2>&1; then echo Version "$venv_name" already exists >&2 exit 3 fi From 2f8cb2cf1f093e955a72babf0b65a07f84f33522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20M=C3=A1t=C3=A9=20R=C3=B3bert?= Date: Fri, 7 Jul 2023 16:18:02 +0200 Subject: [PATCH 6/8] Handle osx specific `ln` option name (#1) On OSX `ln` does not have `--symbolic` only `-s`. * bin/pyenv-link-version --------- Co-authored-by: yfprojects <62463991+real-yfprojects@users.noreply.github.com> --- bin/pyenv-link-version | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/pyenv-link-version b/bin/pyenv-link-version index 55a1102d..45fd4f67 100755 --- a/bin/pyenv-link-version +++ b/bin/pyenv-link-version @@ -131,7 +131,14 @@ pyenv_prefix_path="$PYENV_ROOT/versions/$venv_name" # link to pyenv version directory if [ -z "$dry" ]; then - ln --symbolic "$venv_dir" "$pyenv_prefix_path" + # Handle OS types + case "$(uname -s)" in + Darwin*) + # long form flags aren't supported + ln -s "$venv_dir" "$pyenv_prefix_path";; + *) + ln --symbolic "$venv_dir" "$pyenv_prefix_path";; + esac fi # output From 798a1ad60398a38c28afc94bab7ab55982bdbf69 Mon Sep 17 00:00:00 2001 From: macayu17 Date: Tue, 8 Sep 2026 09:42:50 +0300 Subject: [PATCH 7/8] Incorporate the `pyenv-link` plugin * Adapt test_helper for use in plugins * Add pyenv-link tests * Add pyenv-link tests to CI --- Makefile | 36 +++++++++++- plugins/.gitignore | 1 + plugins/pyenv-link/README.md | 49 ++++------------ plugins/pyenv-link/bin/pyenv-link | 6 +- plugins/pyenv-link/bin/pyenv-link-version | 71 ++++++++++++----------- plugins/pyenv-link/test/link.bats | 61 +++++++++++++++++++ plugins/pyenv-link/test/test_helper.bash | 1 + test/test_helper.bash | 12 +++- 8 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 plugins/pyenv-link/test/link.bats create mode 120000 plugins/pyenv-link/test/test_helper.bash diff --git a/Makefile b/Makefile index 6a8ae7e9..cfbc0ec7 100644 --- a/Makefile +++ b/Makefile @@ -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_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_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_BATS_IMAGE_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_BATS_IMAGE_PREFIX))) .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) $(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_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 .PHONY: $(TEST_BATS_IMAGE_PREFIX) $(TEST_BATS_IMAGE_PREFIX): $(TEST_BATS_IMAGE_TARGETS) @@ -110,13 +136,13 @@ $(TEST_BATS_IMAGE_TARGETS): ./ ; \ 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. unexport PYTHON_CFLAGS 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 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"} \ $${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) bats-$(TEST_BATS_VERSION): git clone --depth 1 --branch $(TEST_BATS_VERSION) https://github.com/bats-core/bats-core.git bats-$(TEST_BATS_VERSION) diff --git a/plugins/.gitignore b/plugins/.gitignore index 2a5caf55..40bac1a7 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -3,4 +3,5 @@ !/version-ext-compat !/python-build !/pyenv-binary +!/pyenv-link /python-build/test/build diff --git a/plugins/pyenv-link/README.md b/plugins/pyenv-link/README.md index 67fa6379..9e4cd294 100644 --- a/plugins/pyenv-link/README.md +++ b/plugins/pyenv-link/README.md @@ -1,40 +1,15 @@ # 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. -## 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 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 $ 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. @@ -44,19 +19,15 @@ $ pyenv link version .venv 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: ```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 \ No newline at end of file diff --git a/plugins/pyenv-link/bin/pyenv-link b/plugins/pyenv-link/bin/pyenv-link index 1992e229..85b008e3 100755 --- a/plugins/pyenv-link/bin/pyenv-link +++ b/plugins/pyenv-link/bin/pyenv-link @@ -1,13 +1,13 @@ #!/usr/bin/env bash # -# Summary: Activate virtual environment +# Summary: Link a Python environment as a pyenv version # -# Usage: pyenv link version +# Usage: pyenv link version [--dry] [--quiet] [] # # Link a virtual python environment to pyenvs version directory # so that the venv can be used like one created with *pyenv-virtualenv*. # -# should be a path to a virtual python environments location. +# should be a path to a Python installation or virtual environment. set -e [ -n "$PYENV_DEBUG" ] && set -x diff --git a/plugins/pyenv-link/bin/pyenv-link-version b/plugins/pyenv-link/bin/pyenv-link-version index 45fd4f67..7df1d16e 100755 --- a/plugins/pyenv-link/bin/pyenv-link-version +++ b/plugins/pyenv-link/bin/pyenv-link-version @@ -1,13 +1,13 @@ #!/usr/bin/env bash # -# Summary: Activate virtual environment +# Summary: Link a Python environment as a pyenv version # -# Usage: pyenv link-venv [--dry] [--quiet] [] +# Usage: pyenv link version [--dry] [--quiet] [] # # Link a virtual python environment to pyenvs version directory # so that the venv can be used like one created with *pyenv-virtualenv*. # -# should be a path to a virtual python environments location. +# should be a path to a Python installation or virtual environment. # should be string used as a version name it may not be present # in the pyenv version directory yet. # If not specified a matching name is guessed from pyvenv.cfg @@ -26,24 +26,10 @@ if ! { # work-around when realpath is unavailable realpath() { - echo "$(cd "$1" && pwd)" + (cd "$1" && pwd) } 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() { local venv_dir=$1 local name @@ -53,12 +39,20 @@ guess_name() { if [ -f "$pyvenv_cfg_path" ]; then # if `prompt` key wasn't found the var remains empty name=$(cut -b 1-1024 "$pyvenv_cfg_path" | sed -n '/^ *prompt *= */s///p') + case "$name" in + \"*\" | \'*\') + name=${name:1:${#name}-2} + ;; + esac fi # guess venv name from name of venv directory local venv_dir_name=${venv_dir##*/} - if [ -z "$name" ] && array_doesnt_contain COMMON_VENV_NAMES "$venv_dir_name"; then - name=$venv_dir_name + if [ -z "$name" ]; then + case "$venv_dir_name" in + venv | env | .venv | .env | ENV | VENV) ;; + *) name=$venv_dir_name ;; + esac fi # 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") fi -# check whether version exists already -if pyenv-prefix "$venv_name" >/dev/null 2>&1; then +case "$venv_name" in +"" | */* | . | .. | *:* | 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 exit 3 fi -# construct location to link to -pyenv_prefix_path="$PYENV_ROOT/versions/$venv_name" +case "$venv_dir" in +"$versions_dir"/*) + link_target=${venv_dir#"$versions_dir"/} + ;; +*) + link_target=$venv_dir + ;; +esac # link to pyenv version directory if [ -z "$dry" ]; then - # Handle OS types - case "$(uname -s)" in - Darwin*) - # long form flags aren't supported - ln -s "$venv_dir" "$pyenv_prefix_path";; - *) - ln --symbolic "$venv_dir" "$pyenv_prefix_path";; - esac + mkdir -p "$versions_dir" + ln -s "$link_target" "$pyenv_prefix_path" fi # output -[ -z "$quiet" ] && echo Linked new version named "$venv_name" || true +if [ -z "$quiet" ]; then + echo "Linked new version named $venv_name" +fi diff --git a/plugins/pyenv-link/test/link.bats b/plugins/pyenv-link/test/link.bats new file mode 100644 index 00000000..0612943d --- /dev/null +++ b/plugins/pyenv-link/test/link.bats @@ -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 < []" +} + +@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" ] +} diff --git a/plugins/pyenv-link/test/test_helper.bash b/plugins/pyenv-link/test/test_helper.bash new file mode 120000 index 00000000..f6bb7c46 --- /dev/null +++ b/plugins/pyenv-link/test/test_helper.bash @@ -0,0 +1 @@ +../../../test/test_helper.bash \ No newline at end of file diff --git a/test/test_helper.bash b/test/test_helper.bash index ee0a698e..2015907f 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -2,8 +2,14 @@ unset PYENV_VERSION unset PYENV_DIR setup() { - export _PYENV_INSTALL_PREFIX="${BATS_TEST_DIRNAME%/*}" 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 [ -n "$PYENV_NATIVE_EXT" ]; then echo "pyenv: failed to load \`realpath' builtin" >&2 @@ -27,6 +33,9 @@ setup() { PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin PATH="${PYENV_TEST_DIR}/bin:$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="${PYENV_ROOT}/shims:$PATH" PATH="${BATS_TEST_TMPDIR}/stubs:$PATH" @@ -40,6 +49,7 @@ setup() { # even if its output is redirected, breaking the comparison logic export NO_COLOR=1 + # If test specific setup exist, run it if [[ $(type -t _setup) == function ]];then _setup From 88e9ab13e8fd603c732a4f43f1ff3d1c2d264b6a Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Wed, 9 Sep 2026 02:17:04 +0300 Subject: [PATCH 8/8] pwsh: Fix error in shell integration if pyenv is on PATH multiple times Fixes "The term 'pyenv pyenv' is not recognized..." --- libexec/pyenv-init | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libexec/pyenv-init b/libexec/pyenv-init index b45181fa..34f1173e 100755 --- a/libexec/pyenv-init +++ b/libexec/pyenv-init @@ -473,12 +473,12 @@ function pyenv { } if ( ("${commands[*]}" -split ' ') -contains \$command ) { - \$shell_cmds = (& (get-command -commandtype application pyenv) sh-\$command \$args) + \$shell_cmds = (& (get-command -commandtype application pyenv -totalcount 1) sh-\$command \$args) if ( \$shell_cmds.Count -gt 0 ) { iex (\$shell_cmds -join "\`n") } } else { - & (get-command -commandtype application pyenv) \$command \$args + & (get-command -commandtype application pyenv -totalcount 1) \$command \$args } } EOS