diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index fbc6d430e..c8a1017b4 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4577,6 +4577,11 @@ pub enum PythonCommand { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct PythonListArgs { + /// A Python request to filter by. + /// + /// See `uv help python` to view supported request formats. + pub request: Option, + /// List all Python versions, including old patch versions. /// /// By default, only the latest patch version is shown for each minor version. diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index e818af4f3..d71850aca 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -52,6 +52,7 @@ struct PrintData { /// List available Python installations. #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] pub(crate) async fn list( + request: Option, kinds: PythonListKinds, all_versions: bool, all_platforms: bool, @@ -63,23 +64,31 @@ pub(crate) async fn list( cache: &Cache, printer: Printer, ) -> Result { + 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(); - if python_preference != PythonPreference::OnlySystem { + if let Some(base_download_request) = base_download_request { let download_request = match kinds { PythonListKinds::Installed => None, PythonListKinds::Downloads => Some(if all_platforms { - PythonDownloadRequest::default() + base_download_request } else { - PythonDownloadRequest::from_env()? + base_download_request.fill()? }), PythonListKinds::Default => { if python_downloads.is_automatic() { Some(if all_platforms { - PythonDownloadRequest::default() + base_download_request } else if all_arches { - PythonDownloadRequest::from_env()?.with_any_arch() + base_download_request.fill()?.with_any_arch() } else { - PythonDownloadRequest::from_env()? + base_download_request.fill()? }) } else { // 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 { PythonListKinds::Installed | PythonListKinds::Default => { Some(find_python_installations( - &PythonRequest::Any, + request.as_ref().unwrap_or(&PythonRequest::Any), EnvironmentPreference::OnlySystem, python_preference, cache, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 97aca26c9..b82caa222 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1272,6 +1272,7 @@ async fn run(mut cli: Cli) -> Result { let cache = cache.init()?; commands::python_list( + args.request, args.kinds, args.all_versions, args.all_platforms, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 111b54798..4b4e31226 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -826,6 +826,7 @@ pub(crate) enum PythonListKinds { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PythonListSettings { + pub(crate) request: Option, pub(crate) kinds: PythonListKinds, pub(crate) all_platforms: bool, pub(crate) all_arches: bool, @@ -839,6 +840,7 @@ impl PythonListSettings { #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: PythonListArgs, _filesystem: Option) -> Self { let PythonListArgs { + request, all_versions, all_platforms, all_arches, @@ -857,6 +859,7 @@ impl PythonListSettings { }; Self { + request, kinds, all_platforms, all_arches, diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index 63c41abc8..43a1090eb 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -1,3 +1,4 @@ +use uv_python::platform::{Arch, Os}; use uv_static::EnvVars; use crate::common::{uv_snapshot, TestContext}; @@ -27,6 +28,79 @@ fn python_list() { ----- 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 context.python_versions.reverse(); @@ -42,16 +116,12 @@ fn python_list() { // Request Python 3.11 uv_snapshot!(context.filters(), context.python_list().arg("3.11"), @r" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- + cpython-3.11.[X]-[PLATFORM] [PYTHON-3.11] ----- 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 ----- "); } + +#[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. + "); +} diff --git a/docs/concepts/python-versions.md b/docs/concepts/python-versions.md index 73b4510fd..1e2d13a22 100644 --- a/docs/concepts/python-versions.md +++ b/docs/concepts/python-versions.md @@ -173,6 +173,18 @@ To list installed and available Python versions: $ 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. To view all versions: diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 7a7e410e3..ac8a3901d 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4603,9 +4603,17 @@ Use `--only-installed` to omit available downloads.

Usage

``` -uv python list [OPTIONS] +uv python list [OPTIONS] [REQUEST] ``` +

Arguments

+ +
REQUEST

A Python request to filter by.

+ +

See uv python to view supported request formats.

+ +
+

Options

--all-arches

List Python downloads for all architectures.