Add support for Python version requests in `uv python list` (#12375)

Allows `uv python list <request>` to filter the installed list. I often
want this and it's not hard to add.

I tested the remote download filtering locally (#12381 is needed for
snapshot tests)

```
❯ cargo run -q -- python list --all-versions 3.13
cpython-3.13.2-macos-aarch64-none    <download available>
cpython-3.13.1-macos-aarch64-none    /opt/homebrew/opt/python@3.13/bin/python3.13 -> ../Frameworks/Python.framework/Versions/3.13/bin/python3.13
cpython-3.13.1-macos-aarch64-none    <download available>
cpython-3.13.0-macos-aarch64-none    /Users/zb/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/bin/python3.13
❯ cargo run -q -- python list --all-versions 3.13 --only-installed
cpython-3.13.1-macos-aarch64-none    /opt/homebrew/opt/python@3.13/bin/python3.13 -> ../Frameworks/Python.framework/Versions/3.13/bin/python3.13
cpython-3.13.0-macos-aarch64-none    /Users/zb/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/bin/python3.13
```
This commit is contained in:
Zanie Blue 2025-03-22 22:13:58 -05:00 committed by GitHub
parent cdd6de555b
commit ec499807f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 199 additions and 15 deletions

View File

@ -4577,6 +4577,11 @@ pub enum PythonCommand {
#[derive(Args)] #[derive(Args)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct PythonListArgs { pub struct PythonListArgs {
/// A Python request to filter by.
///
/// See `uv help python` to view supported request formats.
pub request: Option<String>,
/// List all Python versions, including old patch versions. /// List all Python versions, including old patch versions.
/// ///
/// By default, only the latest patch version is shown for each minor version. /// By default, only the latest patch version is shown for each minor version.

View File

@ -52,6 +52,7 @@ struct PrintData {
/// List available Python installations. /// List available Python installations.
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub(crate) async fn list( pub(crate) async fn list(
request: Option<String>,
kinds: PythonListKinds, kinds: PythonListKinds,
all_versions: bool, all_versions: bool,
all_platforms: bool, all_platforms: bool,
@ -63,23 +64,31 @@ pub(crate) async fn list(
cache: &Cache, cache: &Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let request = request.as_deref().map(PythonRequest::parse);
let base_download_request = if python_preference == PythonPreference::OnlySystem {
None
} else {
// If the user request cannot be mapped to a download request, we won't show any downloads
PythonDownloadRequest::from_request(request.as_ref().unwrap_or(&PythonRequest::Any))
};
let mut output = BTreeSet::new(); let mut output = BTreeSet::new();
if python_preference != PythonPreference::OnlySystem { if let Some(base_download_request) = base_download_request {
let download_request = match kinds { let download_request = match kinds {
PythonListKinds::Installed => None, PythonListKinds::Installed => None,
PythonListKinds::Downloads => Some(if all_platforms { PythonListKinds::Downloads => Some(if all_platforms {
PythonDownloadRequest::default() base_download_request
} else { } else {
PythonDownloadRequest::from_env()? base_download_request.fill()?
}), }),
PythonListKinds::Default => { PythonListKinds::Default => {
if python_downloads.is_automatic() { if python_downloads.is_automatic() {
Some(if all_platforms { Some(if all_platforms {
PythonDownloadRequest::default() base_download_request
} else if all_arches { } else if all_arches {
PythonDownloadRequest::from_env()?.with_any_arch() base_download_request.fill()?.with_any_arch()
} else { } else {
PythonDownloadRequest::from_env()? base_download_request.fill()?
}) })
} else { } else {
// If fetching is not automatic, then don't show downloads as available by default // If fetching is not automatic, then don't show downloads as available by default
@ -109,7 +118,7 @@ pub(crate) async fn list(
match kinds { match kinds {
PythonListKinds::Installed | PythonListKinds::Default => { PythonListKinds::Installed | PythonListKinds::Default => {
Some(find_python_installations( Some(find_python_installations(
&PythonRequest::Any, request.as_ref().unwrap_or(&PythonRequest::Any),
EnvironmentPreference::OnlySystem, EnvironmentPreference::OnlySystem,
python_preference, python_preference,
cache, cache,

View File

@ -1272,6 +1272,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
let cache = cache.init()?; let cache = cache.init()?;
commands::python_list( commands::python_list(
args.request,
args.kinds, args.kinds,
args.all_versions, args.all_versions,
args.all_platforms, args.all_platforms,

View File

@ -826,6 +826,7 @@ pub(crate) enum PythonListKinds {
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct PythonListSettings { pub(crate) struct PythonListSettings {
pub(crate) request: Option<String>,
pub(crate) kinds: PythonListKinds, pub(crate) kinds: PythonListKinds,
pub(crate) all_platforms: bool, pub(crate) all_platforms: bool,
pub(crate) all_arches: bool, pub(crate) all_arches: bool,
@ -839,6 +840,7 @@ impl PythonListSettings {
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: PythonListArgs, _filesystem: Option<FilesystemOptions>) -> Self { pub(crate) fn resolve(args: PythonListArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let PythonListArgs { let PythonListArgs {
request,
all_versions, all_versions,
all_platforms, all_platforms,
all_arches, all_arches,
@ -857,6 +859,7 @@ impl PythonListSettings {
}; };
Self { Self {
request,
kinds, kinds,
all_platforms, all_platforms,
all_arches, all_arches,

View File

@ -1,3 +1,4 @@
use uv_python::platform::{Arch, Os};
use uv_static::EnvVars; use uv_static::EnvVars;
use crate::common::{uv_snapshot, TestContext}; use crate::common::{uv_snapshot, TestContext};
@ -27,6 +28,79 @@ fn python_list() {
----- stderr ----- ----- stderr -----
"); ");
// Request Python 3.12
uv_snapshot!(context.filters(), context.python_list().arg("3.12"), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12]
----- stderr -----
");
// Request Python 3.11
uv_snapshot!(context.filters(), context.python_list().arg("3.11"), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11]
----- stderr -----
");
// Request CPython
uv_snapshot!(context.filters(), context.python_list().arg("cpython"), @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 CPython 3.12
uv_snapshot!(context.filters(), context.python_list().arg("cpython@3.12"), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12]
----- stderr -----
");
// Request CPython 3.12 via partial key syntax
uv_snapshot!(context.filters(), context.python_list().arg("cpython-3.12"), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12]
----- stderr -----
");
// Request CPython 3.12 for the current platform
let os = Os::from_env();
let arch = Arch::from_env();
uv_snapshot!(context.filters(), context.python_list().arg(format!("cpython-3.12-{os}-{arch}")), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.12.[X]-[PLATFORM] [PYTHON-3.12]
----- stderr -----
");
// Request PyPy (which should be missing)
uv_snapshot!(context.filters(), context.python_list().arg("pypy"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
// Swap the order of the Python versions // Swap the order of the Python versions
context.python_versions.reverse(); context.python_versions.reverse();
@ -42,16 +116,12 @@ fn python_list() {
// Request Python 3.11 // Request Python 3.11
uv_snapshot!(context.filters(), context.python_list().arg("3.11"), @r" uv_snapshot!(context.filters(), context.python_list().arg("3.11"), @r"
success: false success: true
exit_code: 2 exit_code: 0
----- stdout ----- ----- stdout -----
cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11]
----- stderr ----- ----- stderr -----
error: unexpected argument '3.11' found
Usage: uv python list [OPTIONS]
For more information, try '--help'.
"); ");
} }
@ -134,3 +204,79 @@ fn python_list_venv() {
----- stderr ----- ----- stderr -----
"); ");
} }
#[cfg(unix)]
#[test]
fn python_list_unsupported_version() {
let context: TestContext = TestContext::new_with_versions(&["3.12"])
.with_filtered_python_symlinks()
.with_filtered_python_keys();
// Request a low version
uv_snapshot!(context.filters(), context.python_list().arg("3.6"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid version request: Python <3.7 is not supported but 3.6 was requested.
");
// Request a low version with a patch
uv_snapshot!(context.filters(), context.python_list().arg("3.6.9"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid version request: Python <3.7 is not supported but 3.6.9 was requested.
");
// Request a really low version
uv_snapshot!(context.filters(), context.python_list().arg("2.6"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid version request: Python <3.7 is not supported but 2.6 was requested.
");
// Request a really low version with a patch
uv_snapshot!(context.filters(), context.python_list().arg("2.6.8"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid version request: Python <3.7 is not supported but 2.6.8 was requested.
");
// Request a future version
uv_snapshot!(context.filters(), context.python_list().arg("4.2"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
// Request a low version with a range
uv_snapshot!(context.filters(), context.python_list().arg("<3.0"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
// Request free-threaded Python on unsupported version
uv_snapshot!(context.filters(), context.python_list().arg("3.12t"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid version request: Python <3.13 does not support free-threading but 3.12t was requested.
");
}

View File

@ -173,6 +173,18 @@ To list installed and available Python versions:
$ uv python list $ uv python list
``` ```
To filter the Python versions, provide a request, e.g., to show all Python 3.13 interpreters:
```console
$ uv python list 3.13
```
Or, to show all PyPy interpreters:
```console
$ uv python list pypy
```
By default, downloads for other platforms and old patch versions are hidden. By default, downloads for other platforms and old patch versions are hidden.
To view all versions: To view all versions:

View File

@ -4603,9 +4603,17 @@ Use `--only-installed` to omit available downloads.
<h3 class="cli-reference">Usage</h3> <h3 class="cli-reference">Usage</h3>
``` ```
uv python list [OPTIONS] uv python list [OPTIONS] [REQUEST]
``` ```
<h3 class="cli-reference">Arguments</h3>
<dl class="cli-reference"><dt id="uv-python-list--request"><a href="#uv-python-list--request"<code>REQUEST</code></a></dt><dd><p>A Python request to filter by.</p>
<p>See <a href="#uv-python">uv python</a> to view supported request formats.</p>
</dd></dl>
<h3 class="cli-reference">Options</h3> <h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt id="uv-python-list--all-arches"><a href="#uv-python-list--all-arches"><code>--all-arches</code></a></dt><dd><p>List Python downloads for all architectures.</p> <dl class="cli-reference"><dt id="uv-python-list--all-arches"><a href="#uv-python-list--all-arches"><code>--all-arches</code></a></dt><dd><p>List Python downloads for all architectures.</p>