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
24 changes: 22 additions & 2 deletions docs/lang/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,28 @@ If you have installed `uv` (for example, with `mise use -g uv@latest`), `mise` w

Note that `uv` does not include `pip` by default (as `uv` provides `uv pip` instead). If you need the `pip` package, add the `uv_create_args = ['--seed']` option.

If you are still using the legacy `python.uv_venv_auto = true` value (deprecated in favor of `"source"` or `"create|source"`), mise will also export `UV_PYTHON`,
which forces `uv` to use `mise`'s selected python version.
:::warning
The `true` value for `python.uv_venv_auto` is considered legacy and will be deprecated in a
future release (planned for mise 2026.7). Prefer `"source"` or `"create|source"` instead.
Note: the `python.uv_venv_auto` **setting** itself is not going away — only the `true` value is
being phased out.
:::

One difference between the legacy `true` value and the newer string values is that `true` also
exports `UV_PYTHON` (set to just the Python version number). This tells `uv` which Python version
to use, but does not guarantee that `uv` uses the specific interpreter managed by `mise` — `uv`
may fall back to a system or self-managed Python of the same version.

To strictly ensure `uv` uses `mise`'s managed Python interpreter, set `UV_PYTHON` to the actual
install path instead:

```toml
[tools]
python = "3.12"

[env]
UV_PYTHON = { value = "{{ tools.python.path }}", tools = true }
```

See the [mise + uv Cookbook](/mise-cookbook/python.html#mise-uv) for more examples.

Expand Down
40 changes: 33 additions & 7 deletions e2e/core/test_python_uv_venv
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ set -euo pipefail
export MISE_EXPERIMENTAL=1

# Test uv is used for manually defined venv
cat >.mise.toml <<EOF
cat >mise.toml <<EOF
[env._.python]
venv = {path = "my_venv", create=true}

Expand All @@ -25,7 +25,7 @@ assert_not_contains "ls $PWD/my_venv/" "include" # stdlib virtual venv has an "i
rm -rf .venv
rm -f uv.lock

cat >.mise.toml <<EOF
cat >mise.toml <<EOF
[tools]
python = "3.12.3"
uv = "0.5.4"
Expand All @@ -51,7 +51,7 @@ assert_not_contains "mise env -s bash" "UV_PYTHON"
# Test "create|source" mode - creates venv if missing, sources it
rm -rf .venv

cat >.mise.toml <<EOF
cat >mise.toml <<EOF
[tools]
python = "3.12.3"
uv = "0.5.4"
Expand All @@ -70,7 +70,7 @@ assert_not_contains "mise env -s bash" "UV_PYTHON"
# this will be deprecated in future releases
rm -rf .venv

cat >.mise.toml <<EOF
cat >mise.toml <<EOF
[tools]
python = "3.12.3"
uv = "0.5.4"
Expand All @@ -88,7 +88,7 @@ assert_contains "mise env -s bash" "UV_PYTHON"
# Test boolean parsing from env vars - "true","1", "yes", should all work
rm -rf .venv

cat >.mise.toml <<EOF
cat >mise.toml <<EOF
[tools]
python = "3.12.3"
uv = "0.5.4"
Expand All @@ -105,9 +105,35 @@ MISE_PYTHON_UV_VENV_AUTO=false assert_not_contains "mise env -s bash" "VIRTUAL_E
MISE_PYTHON_UV_VENV_AUTO=no assert_not_contains "mise env -s bash" "VIRTUAL_ENV"
MISE_PYTHON_UV_VENV_AUTO=0 assert_not_contains "mise env -s bash" "VIRTUAL_ENV"

# Test shim exclusion: the mise uv shim must NOT be called internally when creating
# a venv via uv_venv_auto — doing so would trigger `mise exec`, which re-enters
# env resolution and causes infinite subprocess recursion.
rm -rf .venv uv.lock
mkdir -p "$MISE_DATA_DIR/shims"
rm -f "$MISE_DATA_DIR/shims/uv"
cat >"$MISE_DATA_DIR/shims/uv" <<'SHIM'
#!/bin/bash
echo "ERROR: mise uv shim was invoked during venv creation" >&2
exit 99
SHIM
chmod +x "$MISE_DATA_DIR/shims/uv"
ORIG_PATH="$PATH"
export PATH="$MISE_DATA_DIR/shims:$PATH"
cat >mise.toml <<EOF
[settings]
python.uv_venv_auto = "create|source"
EOF
touch uv.lock
# which_no_shims() skips $MISE_DATA_DIR/shims, so the fake shim is never called;
# since no real uv is available either, venv creation is silently skipped.
output=$(mise env -s bash 2>&1 || true)
assert_not_contains "printf '%s' \"$output\"" "ERROR: mise uv shim was invoked during venv creation"
rm -f "$MISE_DATA_DIR/shims/uv" uv.lock
export PATH="$ORIG_PATH"

## Allows opt-out uv's venv
#mkdir -p subdir
#cat >subdir/.mise.toml <<EOF
#cat >subdir/mise.toml <<EOF
#[env._.python]
#venv = {path = "my_subvenv", create=true}
#[tools]
Expand All @@ -125,7 +151,7 @@ MISE_PYTHON_UV_VENV_AUTO=0 assert_not_contains "mise env -s bash" "VIRTUAL_ENV"
#assert_contains "ls $PWD/my_subvenv/" "include" # stdlib virtual venv has an "include" folder while uv doesn't
#
#cd .. || exit 1
#cat >.mise.toml <<EOF
#cat >mise.toml <<EOF
#[tools]
#python = "3.12.3"
#uv = "0.5.4"
Expand Down
4 changes: 2 additions & 2 deletions src/config/env_directive/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::config::config_file::trust_check;
use crate::config::env_directive::EnvResults;
use crate::config::{Config, Settings};
use crate::env_diff::EnvMap;
use crate::file::{display_path, which_non_pristine};
use crate::file::{display_path, which_no_shims};
use crate::lock_file::LockFile;
use crate::toolset::Toolset;
use crate::{backend, plugins};
Expand Down Expand Up @@ -139,7 +139,7 @@ pub(crate) async fn create_python_venv(
let uv_bin = ts
.which_bin(config, "uv")
.await
.or_else(|| which_non_pristine("uv"));
.or_else(|| which_no_shims("uv"));

if require_uv && uv_bin.is_none() {
warn_once!(
Expand Down
13 changes: 13 additions & 0 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,19 @@ pub fn which_non_pristine<P: AsRef<Path>>(name: P) -> Option<PathBuf> {
_which(name, &env::PATH_NON_PRISTINE)
}

/// returns the first executable in PATH, excluding the mise shim directory
/// use this for internal tool lookups to avoid recursive shim invocations
/// (shims call `mise exec`, which would re-enter the same code path)
pub fn which_no_shims<P: AsRef<Path>>(name: P) -> Option<PathBuf> {
let shim_dir = &*dirs::SHIMS;
let paths: Vec<PathBuf> = env::PATH_NON_PRISTINE
.iter()
.filter(|p| p.as_path() != *shim_dir)
.cloned()
.collect();
_which(name, &paths)
}

fn _which<P: AsRef<Path>>(name: P, paths: &[PathBuf]) -> Option<PathBuf> {
let name = name.as_ref();
paths.iter().find_map(|path| {
Expand Down
17 changes: 10 additions & 7 deletions src/uv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub async fn uv_venv(config: &Arc<Config>, ts: &Toolset) -> &'static Option<Venv
let venv_path = uv_root.join(".venv");
if !venv_path.exists() {
if uv_auto.should_create() {
if let Err(err) = create_python_venv(
match create_python_venv(
config,
ts,
&venv_path,
Expand All @@ -38,13 +38,16 @@ pub async fn uv_venv(config: &Arc<Config>, ts: &Toolset) -> &'static Option<Venv
)
.await
{
warn_once!(
"uv venv creation failed at: {p}\n\n{err}",
p = display_path(&venv_path)
);
return None;
Ok(true) => {} // venv created successfully, fall through to load it
Ok(false) => return None, // uv not available, venv not created
Err(err) => {
warn_once!(
"uv venv creation failed at: {p}\n\n{err}",
p = display_path(&venv_path)
);
return None;
}
}
// venv created successfully, fall through to load it
} else {
if !prepare_uv_enabled(config, &uv_root) {
warn_once!(
Expand Down
Loading