This commit is contained in:
Shaan Majid 2025-12-16 10:04:38 +01:00 committed by GitHub
commit 16b124454e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 285 additions and 11 deletions

View File

@ -6038,6 +6038,12 @@ pub struct PythonListArgs {
#[arg(long, alias = "all_architectures")]
pub all_arches: bool,
/// Show all Python variants, including debug and freethreaded builds.
///
/// By default, debug and freethreaded builds are hidden from the list.
#[arg(long)]
pub all_variants: bool,
/// Only show installed Python versions.
///
/// By default, installed distributions and available downloads for the current platform are shown.

View File

@ -1742,6 +1742,21 @@ impl PythonVariant {
}
}
impl PythonRequest {
/// Return the [`PythonVariant`] of the request, if any.
pub fn variant(&self) -> Option<PythonVariant> {
match self {
Self::Version(version) => version.variant(),
Self::ImplementationVersion(_, version) => version.variant(),
Self::Default
| Self::Any
| Self::Directory(_)
| Self::File(_)
| Self::ExecutableName(_)
| Self::Implementation(_)
| Self::Key(_) => None,
}
}
/// Create a request from a string.
///
/// This cannot fail, which means weird inputs will be parsed as [`PythonRequest::File`] or
@ -3453,7 +3468,8 @@ mod tests {
os: None,
libc: None,
build: None,
prereleases: None
prereleases: None,
all_variants: false
})
);
assert_eq!(
@ -3473,7 +3489,8 @@ mod tests {
os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
libc: Some(Libc::None),
build: None,
prereleases: None
prereleases: None,
all_variants: false
})
);
assert_eq!(
@ -3490,7 +3507,8 @@ mod tests {
os: None,
libc: None,
build: None,
prereleases: None
prereleases: None,
all_variants: false
})
);
assert_eq!(
@ -3510,7 +3528,8 @@ mod tests {
os: None,
libc: None,
build: None,
prereleases: None
prereleases: None,
all_variants: false
})
);

View File

@ -164,6 +164,13 @@ pub struct PythonDownloadRequest {
/// Whether to allow pre-releases or not. If not set, defaults to true if [`Self::version`] is
/// not None, and false otherwise.
pub(crate) prereleases: Option<bool>,
/// Whether to include all Python variants (e.g., debug, freethreaded) in the results.
///
/// If `true`, all variants matching the version request are included.
/// If `false`, only the variant specified in the [`Self::version`] request is included,
/// defaulting to the default variant if no variant is specified.
pub(crate) all_variants: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -255,6 +262,7 @@ impl PythonDownloadRequest {
os: Option<Os>,
libc: Option<Libc>,
prereleases: Option<bool>,
all_variants: bool,
) -> Self {
Self {
version,
@ -264,6 +272,7 @@ impl PythonDownloadRequest {
libc,
build: None,
prereleases,
all_variants,
}
}
@ -319,6 +328,12 @@ impl PythonDownloadRequest {
self
}
#[must_use]
pub fn with_all_variants(mut self, all_variants: bool) -> Self {
self.all_variants = all_variants;
self
}
#[must_use]
pub fn with_build(mut self, build: String) -> Self {
self.build = Some(build);
@ -526,10 +541,10 @@ impl PythonDownloadRequest {
) {
return false;
}
if let Some(variant) = version.variant() {
if variant != key.variant {
return false;
}
// When all_variants is false, only match the variant from the version request.
// For example, `3.13+debug` will only match debug builds.
if !self.all_variants && version.variant().is_some_and(|v| v != key.variant) {
return false;
}
}
true
@ -646,6 +661,7 @@ impl TryFrom<&PythonInstallationKey> for PythonDownloadRequest {
Some(*key.os()),
Some(*key.libc()),
Some(key.prerelease().is_some()),
false,
))
}
}
@ -665,6 +681,7 @@ impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
Some(*key.os()),
Some(*key.libc()),
Some(key.prerelease.is_some()),
false,
)
}
}
@ -888,7 +905,15 @@ impl FromStr for PythonDownloadRequest {
}
}
Ok(Self::new(version, implementation, arch, os, libc, None))
Ok(Self::new(
version,
implementation,
arch,
os,
libc,
None,
false,
))
}
}

View File

@ -59,6 +59,7 @@ pub(crate) async fn list(
all_versions: bool,
all_platforms: bool,
all_arches: bool,
all_variants: bool,
show_urls: bool,
output_format: PythonListFormat,
python_downloads_json_url: Option<String>,
@ -70,11 +71,27 @@ pub(crate) async fn list(
preview: Preview,
) -> Result<ExitStatus> {
let request = request.as_deref().map(PythonRequest::parse);
// Reject --all-variants with explicit non-default variant requests
if all_variants
&& request
.as_ref()
.and_then(uv_python::PythonRequest::variant)
.is_some_and(|v| v != uv_python::PythonVariant::Default)
{
return Err(anyhow::anyhow!(
"`--all-variants` cannot be used with a request that specifies a variant\n\n{}{} Use `--all-variants` to show all variants for a Python version, or specify an exact variant like `3.13t` or `3.13+freethreaded`, but not both",
"hint".bold().cyan(),
":".bold()
));
}
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))
.map(|request| request.with_all_variants(all_variants))
};
let client = client_builder.build();
@ -114,8 +131,17 @@ pub(crate) async fn list(
.map(|request| download_list.iter_matching(request))
.into_iter()
.flatten()
// TODO(zanieb): Add a way to show debug downloads, we just hide them for now
.filter(|download| !download.key().variant().is_debug());
.filter(|download| {
// Show all variants when --all-variants is set
all_variants
// Show all variants when a specific non-default variant was requested
|| !matches!(
request.as_ref().and_then(uv_python::PythonRequest::variant),
Some(uv_python::PythonVariant::Default) | None
)
// Otherwise, hide debug builds by default
|| !download.key().variant().is_debug()
});
for download in downloads {
output.insert((

View File

@ -1582,6 +1582,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.all_versions,
args.all_platforms,
args.all_arches,
args.all_variants,
args.show_urls,
args.output_format,
args.python_downloads_json_url,

View File

@ -992,6 +992,7 @@ pub(crate) struct PythonListSettings {
pub(crate) all_platforms: bool,
pub(crate) all_arches: bool,
pub(crate) all_versions: bool,
pub(crate) all_variants: bool,
pub(crate) show_urls: bool,
pub(crate) output_format: PythonListFormat,
pub(crate) python_downloads_json_url: Option<String>,
@ -1012,6 +1013,7 @@ impl PythonListSettings {
all_arches,
only_installed,
only_downloads,
all_variants,
show_urls,
output_format,
python_downloads_json_url: python_downloads_json_url_arg,
@ -1041,6 +1043,7 @@ impl PythonListSettings {
all_platforms,
all_arches,
all_versions,
all_variants,
show_urls,
output_format,
python_downloads_json_url,

View File

@ -483,6 +483,194 @@ fn python_list_downloads_installed() {
----- stderr -----
");
#[cfg(unix)]
{
// Install a debug variant to test variant behavior when installed
context
.python_install()
.arg("3.10+debug")
.assert()
.success();
// Now the debug variant should show as installed while others remain downloads
uv_snapshot!(context.filters(), context.python_list().arg("3.10").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.10.19-[PLATFORM] managed/cpython-3.10.19-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
cpython-3.10.19+debug-[PLATFORM] managed/cpython-3.10.19+debug-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
pypy-3.10.16-[PLATFORM] <download available>
graalpy-3.10.0-[PLATFORM] <download available>
----- stderr -----
");
// Test --only-installed now shows both installed variants
uv_snapshot!(context.filters(), context.python_list().arg("3.10").arg("--only-installed").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.10.19-[PLATFORM] managed/cpython-3.10.19-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
cpython-3.10.19+debug-[PLATFORM] managed/cpython-3.10.19+debug-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
----- stderr -----
");
}
}
#[test]
fn python_list_variants() {
let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys();
// Use cpython@3.9 for stable tests - Python 3.9 is EOL (Oct 2025) so 3.9.25 is the final version
// Default behavior should only show default variants (no debug/freethreaded)
uv_snapshot!(context.filters(), context.python_list().arg("cpython@3.9").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.9.25-[PLATFORM] <download available>
----- stderr -----
");
// With --all-variants, should show all variants including debug
#[cfg(unix)]
uv_snapshot!(context.filters(), context.python_list().arg("cpython@3.9").arg("--all-variants").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.9.25-[PLATFORM] <download available>
cpython-3.9.25+debug-[PLATFORM] <download available>
----- stderr -----
");
// On Windows, debug builds are not available from python-build-standalone
#[cfg(windows)]
uv_snapshot!(context.filters(), context.python_list().arg("cpython@3.9").arg("--all-variants").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.9.25-[PLATFORM] <download available>
----- stderr -----
");
// Explicit debug variant request should work without --all-variants
#[cfg(unix)]
uv_snapshot!(context.filters(), context.python_list().arg("cpython@3.9+debug").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: true
exit_code: 0
----- stdout -----
cpython-3.9.25+debug-[PLATFORM] <download available>
----- stderr -----
");
// On Windows, explicit debug variant request returns empty since no debug builds available
#[cfg(windows)]
uv_snapshot!(context.filters(), context.python_list().arg("cpython@3.9+debug").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"#);
// Explicit freethreaded variant request on 3.9 should fail with error
uv_snapshot!(context.filters(), context.python_list().arg("3.9+freethreaded").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid version request: Python <3.13 does not support free-threading but 3.9+freethreaded was requested.
");
// Explicit freethreaded+debug variant request on 3.9 should fail with error
uv_snapshot!(context.filters(), context.python_list().arg("3.9+freethreaded+debug").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid version request: Python <3.13 does not support free-threading but 3.9+freethreaded+debug was requested.
");
// Using --all-variants with a specific variant request should fail
uv_snapshot!(context.filters(), context.python_list().arg("cpython@3.9+debug").arg("--all-variants").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: `--all-variants` cannot be used with a request that specifies a variant
hint: Use `--all-variants` to show all variants for a Python version, or specify an exact variant like `3.13t` or `3.13+freethreaded`, but not both
");
// Note: --all-variants combined with --all-versions is tested implicitly through the
// individual flag tests above. A dedicated combined test would be fragile due to
// platform-specific version availability in python-build-standalone.
// Test freethreaded variants with stable pinned version 3.13.0
// This ensures test stability since 3.13 is still under active development
#[cfg(unix)]
uv_snapshot!(context.filters(), context.python_list().arg("3.13.0").arg("--all-variants").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r#"
success: true
exit_code: 0
----- stdout -----
cpython-3.13.0-[PLATFORM] <download available>
cpython-3.13.0+debug-[PLATFORM] <download available>
cpython-3.13.0+freethreaded-[PLATFORM] <download available>
cpython-3.13.0+freethreaded+debug-[PLATFORM] <download available>
----- stderr -----
"#);
// On Windows, freethreaded variants without debug builds
#[cfg(windows)]
uv_snapshot!(context.filters(), context.python_list().arg("3.13.0").arg("--all-variants").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r#"
success: true
exit_code: 0
----- stdout -----
cpython-3.13.0-[PLATFORM] <download available>
cpython-3.13.0+freethreaded-[PLATFORM] <download available>
----- stderr -----
"#);
// Test explicit freethreaded variant request with pinned version
uv_snapshot!(context.filters(), context.python_list().arg("3.13.0+freethreaded").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r#"
success: true
exit_code: 0
----- stdout -----
cpython-3.13.0+freethreaded-[PLATFORM] <download available>
----- stderr -----
"#);
// Test explicit freethreaded+debug variant request with pinned version
#[cfg(unix)]
uv_snapshot!(context.filters(), context.python_list().arg("3.13.0+freethreaded+debug").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r#"
success: true
exit_code: 0
----- stdout -----
cpython-3.13.0+freethreaded+debug-[PLATFORM] <download available>
----- stderr -----
"#);
// On Windows, freethreaded+debug variant request returns empty since no debug builds available
#[cfg(windows)]
uv_snapshot!(context.filters(), context.python_list().arg("3.13.0+freethreaded+debug").arg("--only-downloads").env_remove(EnvVars::UV_PYTHON_DOWNLOADS), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"#);
}
#[tokio::test]

View File

@ -259,6 +259,12 @@ To view Python versions for other platforms:
$ uv python list --all-platforms
```
To view all Python variants, including debug and freethreaded builds:
```console
$ uv python list --all-variants
```
To exclude downloads and only show installed Python versions:
```console