diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 7407cba7c..d57918bf9 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -224,6 +224,27 @@ impl TestContext { self } + /// Add extra filtering for ` -> ` symlink display for Python versions in the test + /// context, e.g., for use in `uv python list`. + #[must_use] + pub fn with_filtered_python_symlinks(mut self) -> Self { + for (version, executable) in &self.python_versions { + if fs_err::symlink_metadata(executable).unwrap().is_symlink() { + self.filters.extend( + Self::path_patterns(executable.read_link().unwrap()) + .into_iter() + .map(|pattern| (format! {" -> {pattern}"}, String::new())), + ); + } + // Drop links that are byproducts of the test context too + self.filters.push(( + regex::escape(&format!(" -> [PYTHON-{version}]")), + String::new(), + )); + } + self + } + /// Add extra standard filtering for a given path. #[must_use] pub fn with_filtered_path(mut self, path: &Path, name: &str) -> Self { @@ -803,6 +824,18 @@ impl TestContext { command } + /// Create a `uv python list` command with options shared across scenarios. + pub fn python_list(&self) -> Command { + let mut command = self.new_command(); + command + .arg("python") + .arg("list") + .env(EnvVars::UV_PYTHON_INSTALL_DIR, "") + .current_dir(&self.temp_dir); + self.add_shared_options(&mut command, false); + command + } + /// Create a `uv python install` command with options shared across scenarios. pub fn python_install(&self) -> Command { let mut command = self.new_command(); diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 5ffe6349e..9632c9a3b 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -72,6 +72,9 @@ mod python_dir; #[cfg(feature = "python")] mod python_find; +#[cfg(feature = "python")] +mod python_list; + #[cfg(feature = "python-managed")] mod python_install; diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs new file mode 100644 index 000000000..63c41abc8 --- /dev/null +++ b/crates/uv/tests/it/python_list.rs @@ -0,0 +1,136 @@ +use uv_static::EnvVars; + +use crate::common::{uv_snapshot, TestContext}; + +#[test] +fn python_list() { + let mut context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]) + .with_filtered_python_symlinks() + .with_filtered_python_keys(); + + uv_snapshot!(context.filters(), context.python_list().env(EnvVars::UV_TEST_PYTHON_PATH, ""), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + // We show all interpreters + uv_snapshot!(context.filters(), context.python_list(), @r" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); + + // Swap the order of the Python versions + context.python_versions.reverse(); + + uv_snapshot!(context.filters(), context.python_list(), @r" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); + + // Request Python 3.11 + uv_snapshot!(context.filters(), context.python_list().arg("3.11"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: unexpected argument '3.11' found + + Usage: uv python list [OPTIONS] + + For more information, try '--help'. + "); +} + +#[test] +fn python_list_pin() { + let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]) + .with_filtered_python_symlinks() + .with_filtered_python_keys(); + + // Pin to a version + uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Pinned `.python-version` to `3.12` + + ----- stderr ----- + "###); + + // The pin should not affect the listing + uv_snapshot!(context.filters(), context.python_list(), @r" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); + + // So `--no-config` has no effect + uv_snapshot!(context.filters(), context.python_list().arg("--no-config"), @r" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); +} + +#[test] +fn python_list_venv() { + let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]) + .with_filtered_python_symlinks() + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_filtered_python_names() + .with_filtered_virtualenv_bin(); + + // Create a virtual environment + uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.12").arg("-q"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "###); + + // We should not display the virtual environment + uv_snapshot!(context.filters(), context.python_list(), @r" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); + + // Same if the `VIRTUAL_ENV` is not set (the test context includes it by default) + uv_snapshot!(context.filters(), context.python_list().env_remove(EnvVars::VIRTUAL_ENV), @r" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); +}