Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// List all Python versions, including old patch versions.
///
/// By default, only the latest patch version is shown for each minor version.
Expand Down
23 changes: 16 additions & 7 deletions crates/uv/src/commands/python/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
kinds: PythonListKinds,
all_versions: bool,
all_platforms: bool,
Expand All @@ -63,23 +64,31 @@ pub(crate) async fn list(
cache: &Cache,
printer: Printer,
) -> 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();
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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
let cache = cache.init()?;

commands::python_list(
args.request,
args.kinds,
args.all_versions,
args.all_platforms,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,7 @@ pub(crate) enum PythonListKinds {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct PythonListSettings {
pub(crate) request: Option<String>,
pub(crate) kinds: PythonListKinds,
pub(crate) all_platforms: bool,
pub(crate) all_arches: bool,
Expand All @@ -839,6 +840,7 @@ impl PythonListSettings {
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: PythonListArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let PythonListArgs {
request,
all_versions,
all_platforms,
all_arches,
Expand All @@ -857,6 +859,7 @@ impl PythonListSettings {
};

Self {
request,
kinds,
all_platforms,
all_arches,
Expand Down
160 changes: 153 additions & 7 deletions crates/uv/tests/it/python_list.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use uv_python::platform::{Arch, Os};
use uv_static::EnvVars;

use crate::common::{uv_snapshot, TestContext};
Expand Down Expand Up @@ -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("[email protected]"), @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();

Expand All @@ -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'.
");
}

Expand Down Expand Up @@ -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.
");
}
12 changes: 12 additions & 0 deletions docs/concepts/python-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 9 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -4603,9 +4603,17 @@ Use `--only-installed` to omit available downloads.
<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>

<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>
Expand Down
Loading