From 0e37886ab2b3ec2fba9033f7080da452e563c3bd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 02:59:06 +0000 Subject: [PATCH 1/8] Show managed symlinks with `uv python list --managed-python` When `--managed-python` is used, widen discovery to include search path sources so that symlinks pointing to managed installations are found, then post-filter to keep only managed interpreters. Previously, `OnlyManaged` only searched the `Managed` source, missing symlinks in `~/.local/bin/` and similar PATH directories that point to uv-managed Python installations. Fixes #17959 https://claude.ai/code/session_01Q6Uo1r3YhrdCWyKPXB5X5R --- crates/uv/src/commands/python/list.rs | 18 ++++++++- crates/uv/tests/it/python_list.rs | 54 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index cf437d2b8be7a..419c1c847b5cf 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -131,13 +131,22 @@ pub(crate) async fn list( } } + // When `--managed-python` is used (`OnlyManaged`), widen discovery to include search path + // sources so that symlinks pointing to managed installations are found. We then post-filter + // to keep only managed interpreters. + let discovery_preference = if python_preference == PythonPreference::OnlyManaged { + PythonPreference::Managed + } else { + python_preference + }; + let installed = match kinds { PythonListKinds::Installed | PythonListKinds::Default => { Some(find_python_installations( request.as_ref().unwrap_or(&PythonRequest::Any), EnvironmentPreference::OnlySystem, - python_preference, + discovery_preference, cache, preview, ) @@ -151,7 +160,12 @@ pub(crate) async fn list( .collect::>, DiscoveryError>>()? .into_iter() // Drop any "missing" installations - .filter_map(Result::ok)) + .filter_map(Result::ok) + // When `--managed-python` was requested, filter to only managed interpreters + .filter(|installation| { + python_preference != PythonPreference::OnlyManaged + || installation.interpreter().is_managed() + })) } PythonListKinds::Downloads => None, }; diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index 8e045ae78cc1f..e035fbd3325c3 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -483,6 +483,60 @@ 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.19-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + pypy-3.10.16-[PLATFORM] + graalpy-3.10.0-[PLATFORM] + + ----- stderr ----- + "); +} + +/// Test that `--managed-python` shows symlinks on the search path that point to managed +/// installations. +#[test] +fn python_list_managed_symlinks() { + let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]) + .with_filtered_python_symlinks() + .with_filtered_python_keys() + .with_collapsed_whitespace() + .with_versions_as_managed(&["3.12"]); + + // Without any flags, all interpreters are shown + uv_snapshot!(context.filters(), context.python_list(), @" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); + + // With `--managed-python`, only managed interpreters (3.12) are shown + uv_snapshot!(context.filters(), context.python_list().arg("--managed-python"), @" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] + + ----- stderr ----- + "); + + // With `--no-managed-python`, only non-managed interpreters (3.11) are shown + uv_snapshot!(context.filters(), context.python_list().arg("--no-managed-python"), @" + success: true + exit_code: 0 + ----- stdout ----- + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + + ----- stderr ----- + "); } #[tokio::test] From d8137e0caaefaf660dd3e1e0501db9a2446225b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 03:05:40 +0000 Subject: [PATCH 2/8] Also filter managed interpreters from `--no-managed-python` output Extend the post-filter to handle both directions: `--managed-python` keeps only managed interpreters, and `--no-managed-python` excludes managed interpreters (e.g., symlinks on the search path that point to uv-managed installations). https://claude.ai/code/session_01Q6Uo1r3YhrdCWyKPXB5X5R --- crates/uv/src/commands/python/list.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 419c1c847b5cf..60d3e78fb3a36 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -161,10 +161,13 @@ pub(crate) async fn list( .into_iter() // Drop any "missing" installations .filter_map(Result::ok) - // When `--managed-python` was requested, filter to only managed interpreters - .filter(|installation| { - python_preference != PythonPreference::OnlyManaged - || installation.interpreter().is_managed() + // When `--managed-python` was requested, filter to only managed interpreters; + // when `--no-managed-python` was requested, filter out managed interpreters + // (e.g., symlinks on the search path pointing to managed installations). + .filter(|installation| match python_preference { + PythonPreference::OnlyManaged => installation.interpreter().is_managed(), + PythonPreference::OnlySystem => !installation.interpreter().is_managed(), + _ => true, })) } PythonListKinds::Downloads => None, From 812307e1dd3cb07356a02d464b7b49fb6001b815 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 18:46:50 +0000 Subject: [PATCH 3/8] Make match exhaustive and inline discovery_preference https://claude.ai/code/session_01Q6Uo1r3YhrdCWyKPXB5X5R --- crates/uv/src/commands/python/list.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 60d3e78fb3a36..4a152b714ee11 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -131,18 +131,17 @@ pub(crate) async fn list( } } - // When `--managed-python` is used (`OnlyManaged`), widen discovery to include search path - // sources so that symlinks pointing to managed installations are found. We then post-filter - // to keep only managed interpreters. - let discovery_preference = if python_preference == PythonPreference::OnlyManaged { - PythonPreference::Managed - } else { - python_preference - }; - let installed = match kinds { PythonListKinds::Installed | PythonListKinds::Default => { + // When `--managed-python` is used (`OnlyManaged`), widen discovery to include + // search path sources so that symlinks pointing to managed installations are + // found. We then post-filter to keep only managed interpreters. + 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, @@ -167,7 +166,7 @@ pub(crate) async fn list( .filter(|installation| match python_preference { PythonPreference::OnlyManaged => installation.interpreter().is_managed(), PythonPreference::OnlySystem => !installation.interpreter().is_managed(), - _ => true, + PythonPreference::Managed | PythonPreference::System => true, })) } PythonListKinds::Downloads => None, From ad3e7c393d38591412a6d7e85813f3eb72416440 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 18:52:40 +0000 Subject: [PATCH 4/8] Replace managed symlinks test with real python install test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test used `with_versions_as_managed` which changes the discovery source to `Managed` — bypassing the search path symlink scenario. The new test uses `python install` to create real symlinks in `bin_dir`, then adds `bin_dir` to `UV_TEST_PYTHON_PATH` to verify that `--no-managed-python` correctly excludes symlinks pointing to managed installations, and `--managed-python` includes them. https://claude.ai/code/session_01Q6Uo1r3YhrdCWyKPXB5X5R --- crates/uv/tests/it/python_list.rs | 49 ++++++++++++++++++------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index e035fbd3325c3..8884caa6c2f12 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -497,43 +497,50 @@ fn python_list_downloads_installed() { "); } -/// Test that `--managed-python` shows symlinks on the search path that point to managed -/// installations. +/// Test that symlinks installed by `python install` on the search path are correctly +/// filtered by `--managed-python` and `--no-managed-python`. #[test] +#[cfg(feature = "test-python-managed")] fn python_list_managed_symlinks() { - let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]) - .with_filtered_python_symlinks() + use assert_cmd::assert::OutputAssertExt; + + let context = uv_test::test_context_with_versions!(&[]) .with_filtered_python_keys() - .with_collapsed_whitespace() - .with_versions_as_managed(&["3.12"]); + .with_filtered_python_install_bin() + .with_filtered_python_names() + .with_managed_python_dirs(); - // Without any flags, all interpreters are shown - uv_snapshot!(context.filters(), context.python_list(), @" - success: true - exit_code: 0 - ----- stdout ----- - cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] - cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] + // Install a Python version; this creates a symlink in `bin_dir` (on the search path) + context.python_install().arg("3.10").assert().success(); - ----- stderr ----- - "); + // Include `bin_dir` in the test search path so the symlink is discoverable + let bin_dir = context.bin_dir.to_path_buf(); - // With `--managed-python`, only managed interpreters (3.12) are shown - uv_snapshot!(context.filters(), context.python_list().arg("--managed-python"), @" + // 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 ----- - cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12] ----- stderr ----- "); - // With `--no-managed-python`, only non-managed interpreters (3.11) are shown - uv_snapshot!(context.filters(), context.python_list().arg("--no-managed-python"), @" + // 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.11.[X]-[PLATFORM] [PYTHON-3.11] + cpython-3.10.19-[PLATFORM] [BIN]/[PYTHON] -> managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + cpython-3.10.19-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] ----- stderr ----- "); From 67d780c9f3dc7ff9c8eeb9c37269552a86269948 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 13 Mar 2026 14:53:35 -0500 Subject: [PATCH 5/8] Apply suggestion from @zanieb --- crates/uv/src/commands/python/list.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 4a152b714ee11..9358b6a074d16 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -134,9 +134,9 @@ pub(crate) async fn list( let installed = match kinds { PythonListKinds::Installed | PythonListKinds::Default => { - // When `--managed-python` is used (`OnlyManaged`), widen discovery to include - // search path sources so that symlinks pointing to managed installations are - // found. We then post-filter to keep only managed interpreters. + // 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 { From 0c67e6250d7decd9df8fc1be22e27badfef097c8 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 13 Mar 2026 14:53:42 -0500 Subject: [PATCH 6/8] Apply suggestion from @zanieb --- crates/uv/src/commands/python/list.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 9358b6a074d16..fa075afe8f9ca 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -160,9 +160,8 @@ pub(crate) async fn list( .into_iter() // Drop any "missing" installations .filter_map(Result::ok) - // When `--managed-python` was requested, filter to only managed interpreters; - // when `--no-managed-python` was requested, filter out managed interpreters - // (e.g., symlinks on the search path pointing to managed installations). + // 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(), From d2dad35dea58dba1e3bab839d3217a578931a507 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 13 Mar 2026 18:12:39 -0500 Subject: [PATCH 7/8] Fix snapshot tests: use [LATEST] filter for 3.10 patch versions --- crates/uv/tests/it/python_list.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index c6ab66e82f414..3b80f301cfdce 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -493,7 +493,7 @@ fn python_list_downloads_installed() { success: true exit_code: 0 ----- stdout ----- - cpython-3.10.19-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + cpython-3.10.[LATEST]-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] pypy-3.10.16-[PLATFORM] graalpy-3.10.0-[PLATFORM] @@ -512,7 +512,8 @@ fn python_list_managed_symlinks() { .with_filtered_python_keys() .with_filtered_python_install_bin() .with_filtered_python_names() - .with_managed_python_dirs(); + .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(); @@ -543,8 +544,8 @@ fn python_list_managed_symlinks() { success: true exit_code: 0 ----- stdout ----- - cpython-3.10.19-[PLATFORM] [BIN]/[PYTHON] -> managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] - cpython-3.10.19-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + 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 ----- "); From 91a33c296b6f8b0da21ec54e90ba9f1bd6e5f977 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 13 Mar 2026 18:34:22 -0500 Subject: [PATCH 8/8] Restrict python_list_managed_symlinks test to Unix (no symlinks on Windows) --- crates/uv/tests/it/python_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index 3b80f301cfdce..e9a6421987a5d 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -504,7 +504,7 @@ fn python_list_downloads_installed() { /// Test that symlinks installed by `python install` on the search path are correctly /// filtered by `--managed-python` and `--no-managed-python`. #[test] -#[cfg(feature = "test-python-managed")] +#[cfg(all(unix, feature = "test-python-managed"))] fn python_list_managed_symlinks() { use assert_cmd::assert::OutputAssertExt;