diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index d4ca88c7fe4b4..135e272a9fad1 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -142,10 +142,18 @@ pub(crate) async fn list( let installed = match kinds { PythonListKinds::Installed | PythonListKinds::Default => { + // While usually [`PythonPreference::OnlyManaged`] means we can skip searching the `PATH`, + // in `uv python list` we want to enumerate links to managed Python interpreters for inspection. + // Consequently, we widen the preference here and perform post-filtering. + let discovery_preference = if python_preference == PythonPreference::OnlyManaged { + PythonPreference::Managed + } else { + python_preference + }; Some(find_python_installations( request.as_ref().unwrap_or(&PythonRequest::Any), EnvironmentPreference::OnlySystem, - python_preference, + discovery_preference, cache, preview, ) @@ -159,7 +167,14 @@ pub(crate) async fn list( .collect::>, DiscoveryError>>()? .into_iter() // Drop any "missing" installations - .filter_map(Result::ok)) + .filter_map(Result::ok) + // Apply the `PythonPreference` to discovered interpreters, since we may have + // expanded it above + .filter(|installation| match python_preference { + PythonPreference::OnlyManaged => installation.interpreter().is_managed(), + PythonPreference::OnlySystem => !installation.interpreter().is_managed(), + PythonPreference::Managed | PythonPreference::System => true, + })) } PythonListKinds::Downloads => None, }; diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index 328885852543c..e9a6421987a5d 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -487,6 +487,68 @@ fn python_list_downloads_installed() { ----- stderr ----- "); + + // When `--managed-python` is used, managed installations should still be shown + uv_snapshot!(context.filters(), context.python_list().arg("3.10").arg("--managed-python").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.10.[LATEST]-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + pypy-3.10.16-[PLATFORM] + graalpy-3.10.0-[PLATFORM] + + ----- stderr ----- + "); +} + +/// Test that symlinks installed by `python install` on the search path are correctly +/// filtered by `--managed-python` and `--no-managed-python`. +#[test] +#[cfg(all(unix, feature = "test-python-managed"))] +fn python_list_managed_symlinks() { + use assert_cmd::assert::OutputAssertExt; + + let context = uv_test::test_context_with_versions!(&[]) + .with_filtered_python_keys() + .with_filtered_python_install_bin() + .with_filtered_python_names() + .with_managed_python_dirs() + .with_filtered_latest_python_versions(); + + // Install a Python version; this creates a symlink in `bin_dir` (on the search path) + context.python_install().arg("3.10").assert().success(); + + // Include `bin_dir` in the test search path so the symlink is discoverable + let bin_dir = context.bin_dir.to_path_buf(); + + // With `--no-managed-python`, the symlink should be excluded since it points to a + // managed installation + uv_snapshot!(context.filters(), context.python_list() + .arg("3.10") + .arg("--only-installed") + .arg("--no-managed-python") + .env(EnvVars::UV_TEST_PYTHON_PATH, &bin_dir), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + // With `--managed-python`, both the managed installation and the symlink are shown + uv_snapshot!(context.filters(), context.python_list() + .arg("3.10") + .arg("--only-installed") + .arg("--managed-python") + .env(EnvVars::UV_TEST_PYTHON_PATH, &bin_dir), @" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.10.[LATEST]-[PLATFORM] [BIN]/[PYTHON] -> managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + cpython-3.10.[LATEST]-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); } #[tokio::test]