Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,54 @@ jobs:
run: |
(& ./uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression

integration-test-nushell:
timeout-minutes: 10
needs: build-binary-linux-libc
name: "integration test | activate nushell venv"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Install nushell
env:
# This token only needs read access to the GitHub repository nushell/nushell.
# This token is used (via gh-cli) to avoid hitting GitHub REST API rate limits.
GITHUB_TOKEN: ${{ github.token }}
run: |-
# get latest nushell tag name
nu_latest=$(gh release list --repo nushell/nushell --limit 1 --exclude-pre-releases --exclude-drafts --json "tagName" --jq '.[0].tagName')
# trim any trailing whitespace from output
nu_tag=${nu_latest%%[[:space:]]*}

# download binary for x86_64-unknown-linux-gnu target
gh release download ${nu_tag} --repo nushell/nushell --pattern "nu-${nu_tag}-x86_64-unknown-linux-gnu.tar.gz"

# extract nu binary from tar.gz
tar -xf "nu-${nu_tag}-x86_64-unknown-linux-gnu.tar.gz"
# make the binary executable
chmod +x "./nu-${nu_tag}-x86_64-unknown-linux-gnu/nu"
# add it to PATH
echo "${{ github.workspace }}/nu-${nu_tag}-x86_64-unknown-linux-gnu" >> "${GITHUB_PATH}"

- name: Download binary
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: uv-linux-libc-${{ github.sha }}

- name: Prepare binary
run: chmod +x ./uv

- name: Create venv
# The python version is arbitrary for this test.
# We only want to ensure the activation script behaves properly
run: ./uv venv

- name: Activate venv
shell: nu {0}
run: overlay use ${{ github.workspace }}/.venv/bin/activate.nu

integration-test-conda:
timeout-minutes: 10
needs: build-binary-linux-libc
Expand Down
52 changes: 52 additions & 0 deletions crates/uv-virtualenv/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
# uv-virtualenv

`uv-virtualenv` is a rust library to create Python virtual environments. It also has a CLI.

## Syncing with upstream virtualenv activation scripts

This crate tries to stay in sync with pypa/virtualenv project's activation scrips. However, there
are some deviations that are specific to this crate's implementation.

### License disclaimers added

This crate includes license information at the top of each activation script. This is done in
accordance with the pypa/virtualenv project's MIT License. Do not remove the declarative license
comments from this crate's activation scripts.

### Placeholder names are slightly different

Note, these activation scripts are actually templates that are populated with certain values when a
virtual environment is created.

In upstream, the placeholder names are found in
[`virtualenv.activation.ViaTemplateActivator.replacements()`][upstream-placeholders].

In this crate, the placeholder names are found in
[`uv_virtualenv::virtualenv::create()`][crate-placeholders]

[upstream-placeholders]:
https://github.com/pypa/virtualenv/blob/dad9369e97f5aef7e33777b18dcdb51b1fdac7bd/src/virtualenv/activation/via_template.py#L43
[crate-placeholders]:
https://github.com/astral-sh/uv/blob/d8f3f03198308be53de51a3a297c85566eabb084/crates/uv-virtualenv/src/virtualenv.rs#L462

It is important that the placeholder names (as used in the activation scripts) conform to the
placeholders names used in [this crate's source][crate-placeholders].

### Relocatable virtual environments

This crate uses some additional tweaks in the activation scripts to ensure the virtual environment
is relocatable. Thus, the patch in [astral-sh/uv#5640] shall be retained.

[astral-sh/uv#5640]: https://github.com/astral-sh/uv/pull/5640

### TCL/TK library locations

The patches in upstream virtualenv ([pypa/virtualenv#2928] and [pypa/virtualenv#2940]) implement
dynamically locating the TCL/TK libraries of a base Python distribution (see [upstream
approach][upstream-tcl/tk-approach]).

[pypa/virtualenv#2928]: https://github.com/pypa/virtualenv/pull/2928
[pypa/virtualenv#2940]: https://github.com/pypa/virtualenv/pull/2940
[upstream-tcl/tk-approach]:
https://github.com/pypa/virtualenv/blob/dad9369e97f5aef7e33777b18dcdb51b1fdac7bd/src/virtualenv/discovery/py_info.py#L140

This upstream implementation is considered an undesirable complexity in this project. As such, the
upstream TCL/TK related patches shall be omitted when syncing activation scripts with upstream
sources.
2 changes: 1 addition & 1 deletion crates/uv-virtualenv/src/activator/activate
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,4 @@ pydoc () {
# The hash command must be called to get it to forget past
# commands. Without forgetting past commands the $PATH changes
# we made may not be respected
hash -r 2>/dev/null
hash -r 2>/dev/null || true
6 changes: 3 additions & 3 deletions crates/uv-virtualenv/src/activator/activate.bat
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

@REM This file is UTF-8 encoded, so we need to update the current code page while executing it
@for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do @set _OLD_CODEPAGE=%%a

@if defined _OLD_CODEPAGE (
@"%SystemRoot%\System32\chcp.com" 65001 > nul
"%SystemRoot%\System32\chcp.com" 65001 > nul
)

@for %%i in ("{{ VIRTUAL_ENV_DIR }}") do @set "VIRTUAL_ENV=%%~fi"
Expand Down Expand Up @@ -64,8 +65,7 @@

@set "PATH=%VIRTUAL_ENV%\{{ BIN_NAME }};%PATH%"

:END
@if defined _OLD_CODEPAGE (
@"%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul
"%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul
@set _OLD_CODEPAGE=
)
4 changes: 2 additions & 2 deletions crates/uv-virtualenv/src/activator/activate.fish
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
if test (echo $FISH_VERSION | head -c 1) -lt 3
if test (string sub -s 1 -l 1 $FISH_VERSION) -lt 3
set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH")
else
set -gx PATH $_OLD_VIRTUAL_PATH
Expand Down Expand Up @@ -82,7 +82,7 @@ deactivate nondestructive
set -gx VIRTUAL_ENV '{{ VIRTUAL_ENV_DIR }}'

# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
if test (echo $FISH_VERSION | head -c 1) -lt 3
if test (string sub -s 1 -l 1 $FISH_VERSION) -lt 3
set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH)
else
set -gx _OLD_VIRTUAL_PATH $PATH
Expand Down
119 changes: 52 additions & 67 deletions crates/uv-virtualenv/src/activator/activate.nu
Original file line number Diff line number Diff line change
Expand Up @@ -19,97 +19,82 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# virtualenv activation module
# Activate with `overlay use activate.nu`
# Deactivate with `deactivate`, as usual
# virtualenv activation module:
# - Activate with `overlay use activate.nu`
# - Deactivate with `deactivate`, as usual
#
# To customize the overlay name, you can call `overlay use activate.nu as foo`,
# but then simply `deactivate` won't work because it is just an alias to hide
# the "activate" overlay. You'd need to call `overlay hide foo` manually.
# To customize the overlay name, you can call `overlay use activate.nu as foo`, but then simply `deactivate` won't work
# because it is just an alias to hide the "activate" overlay. You'd need to call `overlay hide foo` manually.

module warning {
export-env {
const file = path self
error make -u {
msg: $"`($file | path basename)` is meant to be used with `overlay use`, not `source`"
}
}

}

use warning

export-env {

let nu_ver = (version | get version | split row '.' | take 2 | each { into int })
if $nu_ver.0 == 0 and $nu_ver.1 < 106 {
error make {
msg: 'virtualenv Nushell activation requires Nushell 0.106 or greater.'
}
}

def is-string [x] {
($x | describe) == 'string'
}

def has-env [...names] {
$names | each {|n|
$n in $env
} | all {|i| $i == true}
$names | each {|n| $n in $env } | all {|i| $i }
}

# Emulates a `test -z`, but better as it handles e.g 'false'
def is-env-true [name: string] {
if (has-env $name) {
# Try to parse 'true', '0', '1', and fail if not convertible
let parsed = (do -i { $env | get $name | into bool })
if ($parsed | describe) == 'bool' {
$parsed
if (has-env $name) {
let val = ($env | get --optional $name)
if ($val | describe) == 'bool' {
$val
} else {
not ($val | is-empty)
}
} else {
not ($env | get -i $name | is-empty)
false
}
} else {
false
}
}

let virtual_env = '{{ VIRTUAL_ENV_DIR }}'
let bin = '{{ BIN_NAME }}'

let is_windows = ($nu.os-info.family) == 'windows'
let path_name = (if (has-env 'Path') {
'Path'
} else {
'PATH'
}
)

let path_name = if (has-env 'Path') { 'Path' } else { 'PATH' }
let venv_path = ([$virtual_env $bin] | path join)
let new_path = ($env | get $path_name | prepend $venv_path)

# If there is no default prompt, then use the env name instead
let virtual_env_prompt = (if ('{{ VIRTUAL_PROMPT }}' | is-empty) {
let virtual_env_prompt = if ('{{ VIRTUAL_PROMPT }}' | is-empty) {
($virtual_env | path basename)
} else {
'{{ VIRTUAL_PROMPT }}'
})

let new_env = {
$path_name : $new_path
VIRTUAL_ENV : $virtual_env
VIRTUAL_ENV_PROMPT : $virtual_env_prompt
}

let new_env = (if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') {
$new_env
let new_env = { $path_name: $new_path VIRTUAL_ENV: $virtual_env VIRTUAL_ENV_PROMPT: $virtual_env_prompt }
let old_prompt_command = if (has-env 'PROMPT_COMMAND') { $env.PROMPT_COMMAND } else { '' }
let new_env = if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') {
$new_env
} else {
# Creating the new prompt for the session
let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) '

# Back up the old prompt builder
let old_prompt_command = (if (has-env 'PROMPT_COMMAND') {
$env.PROMPT_COMMAND
} else {
''
})

let new_prompt = (if (has-env 'PROMPT_COMMAND') {
if 'closure' in ($old_prompt_command | describe) {
{|| $'($virtual_prefix)(do $old_prompt_command)' }
} else {
{|| $'($virtual_prefix)($old_prompt_command)' }
}
} else {
{|| $'($virtual_prefix)' }
})

$new_env | merge {
PROMPT_COMMAND : $new_prompt
VIRTUAL_PREFIX : $virtual_prefix
}
})

# Environment variables that will be loaded as the virtual env
let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) '
let new_prompt = if (has-env 'PROMPT_COMMAND') {
if ('closure' in ($old_prompt_command | describe)) {
{|| $'($virtual_prefix)(do $old_prompt_command)' }
} else {
{|| $'($virtual_prefix)($old_prompt_command)' }
}
} else {
{|| $'($virtual_prefix)' }
}
$new_env | merge { PROMPT_COMMAND: $new_prompt VIRTUAL_PREFIX: $virtual_prefix }
}
load-env $new_env
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uv-virtualenv/src/activator/deactivate.bat
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@
@if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH
@set "PATH=%_OLD_VIRTUAL_PATH%"
@set _OLD_VIRTUAL_PATH=
:ENDIFVPATH
:ENDIFVPATH
2 changes: 1 addition & 1 deletion crates/uv-virtualenv/src/activator/pydoc.bat
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
@REM OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
@REM WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

python.exe -m pydoc %*
python.exe -m pydoc %*
Loading