Skip to content

Commit c58a662

Browse files
committed
Allow apostrophe in venv name
1 parent f65bbfe commit c58a662

File tree

3 files changed

+61
-9
lines changed

3 files changed

+61
-9
lines changed

crates/uv-virtualenv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ uv-platform-tags = { workspace = true }
2525
uv-pypi-types = { workspace = true }
2626
uv-python = { workspace = true }
2727
uv-version = { workspace = true }
28+
uv-shell = { workspace = true }
2829

2930
fs-err = { workspace = true }
3031
itertools = { workspace = true }

crates/uv-virtualenv/src/virtualenv.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use tracing::debug;
1313
use uv_fs::{cachedir, Simplified, CWD};
1414
use uv_pypi_types::Scheme;
1515
use uv_python::{Interpreter, VirtualEnvironment};
16+
use uv_shell::escape_posix_for_single_quotes;
1617
use uv_version::version;
1718

1819
use crate::{Error, Prompt};
@@ -287,23 +288,20 @@ pub(crate) fn create(
287288

288289
let virtual_env_dir = match (relocatable, name.to_owned()) {
289290
(true, "activate") => {
290-
r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#
291+
r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#.to_string()
291292
}
292-
(true, "activate.bat") => r"%~dp0..",
293+
(true, "activate.bat") => r"%~dp0..".to_string(),
293294
(true, "activate.fish") => {
294-
r#"'"$(dirname -- "$(cd "$(dirname -- "$(status -f)")"; and pwd)")"'"#
295+
r#"'"$(dirname -- "$(cd "$(dirname -- "$(status -f)")"; and pwd)")"'"#.to_string()
295296
}
296297
// Note:
297298
// * relocatable activate scripts appear not to be possible in csh and nu shell
298299
// * `activate.ps1` is already relocatable by default.
299-
_ => {
300-
// SAFETY: `unwrap` is guaranteed to succeed because `location` is an `Utf8PathBuf`.
301-
location.simplified().to_str().unwrap()
302-
}
300+
_ => escape_posix_for_single_quotes(location.simplified().to_str().unwrap()),
303301
};
304302

305303
let activator = template
306-
.replace("{{ VIRTUAL_ENV_DIR }}", virtual_env_dir)
304+
.replace("{{ VIRTUAL_ENV_DIR }}", &virtual_env_dir)
307305
.replace("{{ BIN_NAME }}", bin_name)
308306
.replace(
309307
"{{ VIRTUAL_PROMPT }}",

crates/uv/tests/it/venv.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use assert_cmd::prelude::*;
33
use assert_fs::prelude::*;
44
use indoc::indoc;
55
use predicates::prelude::*;
6-
76
use uv_python::{PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME};
87
use uv_static::EnvVars;
98

@@ -1087,3 +1086,57 @@ fn path_with_trailing_space_gives_proper_error() {
10871086
);
10881087
// Note the extra trailing `/` in the snapshot is due to the filters, not the actual output.
10891088
}
1089+
1090+
/// Check that the activate script still works with the path contains an apostrophe.
1091+
#[test]
1092+
#[cfg(target_os = "linux")]
1093+
fn create_venv_apostrophe() {
1094+
use std::env;
1095+
use std::ffi::OsString;
1096+
use std::io::Write;
1097+
use std::process::Command;
1098+
use std::process::Stdio;
1099+
1100+
let context = TestContext::new_with_versions(&["3.12"]);
1101+
1102+
let venv_dir = context.temp_dir.join("Testing's");
1103+
1104+
uv_snapshot!(context.filters(), context.venv()
1105+
.arg(&venv_dir)
1106+
.arg("--python")
1107+
.arg("3.12"), @r###"
1108+
success: true
1109+
exit_code: 0
1110+
----- stdout -----
1111+
1112+
----- stderr -----
1113+
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
1114+
Creating virtual environment at: Testing's
1115+
Activate with: source Testing's/[BIN]/activate
1116+
"###
1117+
);
1118+
1119+
// One of them should be commonly available on a linux developer machine, if not, we have to
1120+
// extend the fallbacks.
1121+
let shell = env::var_os("SHELL").unwrap_or(OsString::from("bash"));
1122+
let mut child = Command::new(shell)
1123+
.stdin(Stdio::piped())
1124+
.stdout(Stdio::piped())
1125+
.current_dir(&venv_dir)
1126+
.spawn()
1127+
.expect("Failed to spawn shell script");
1128+
1129+
let mut stdin = child.stdin.take().expect("Failed to open stdin");
1130+
std::thread::spawn(move || {
1131+
stdin
1132+
.write_all(". bin/activate && python -c 'import sys; print(sys.prefix)'".as_bytes())
1133+
.expect("Failed to write to stdin");
1134+
});
1135+
1136+
let output = child.wait_with_output().expect("Failed to read stdout");
1137+
1138+
assert!(output.status.success(), "{output:?}");
1139+
1140+
let stdout = String::from_utf8_lossy(&output.stdout);
1141+
assert_eq!(stdout.trim(), venv_dir.to_string_lossy());
1142+
}

0 commit comments

Comments
 (0)