diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index 486d034d0..d8b0de47f 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -12,7 +12,7 @@ use uv_warnings::warn_user_once; /// A client to fetch the latest version of a package from an index. /// /// The returned distribution is guaranteed to be compatible with the provided tags and Python -/// requirement. +/// requirement (if specified). #[derive(Debug, Clone)] pub(crate) struct LatestClient<'env> { pub(crate) client: &'env RegistryClient, @@ -20,7 +20,7 @@ pub(crate) struct LatestClient<'env> { pub(crate) prerelease: PrereleaseMode, pub(crate) exclude_newer: &'env ExcludeNewer, pub(crate) tags: Option<&'env Tags>, - pub(crate) requires_python: &'env RequiresPython, + pub(crate) requires_python: Option<&'env RequiresPython>, } impl LatestClient<'_> { @@ -30,7 +30,7 @@ impl LatestClient<'_> { package: &PackageName, index: Option<&IndexUrl>, download_concurrency: &Semaphore, - ) -> anyhow::Result, uv_client::Error> { + ) -> Result, uv_client::Error> { debug!("Fetching latest version of: `{package}`"); let archives = match self @@ -101,14 +101,16 @@ impl LatestClient<'_> { } // Skip distributions that are incompatible with the Python requirement. - if file - .requires_python - .as_ref() - .is_some_and(|requires_python| { - !self.requires_python.is_contained_by(requires_python) - }) - { - continue; + if let Some(requires_python) = self.requires_python { + if file + .requires_python + .as_ref() + .is_some_and(|file_requires_python| { + !requires_python.is_contained_by(file_requires_python) + }) + { + continue; + } } // Skip distributions that are incompatible with the current platform. diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 0c7879d18..63e5a9f51 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -133,7 +133,7 @@ pub(crate) async fn pip_list( prerelease, exclude_newer: &exclude_newer, tags: Some(tags), - requires_python: &requires_python, + requires_python: Some(&requires_python), }; let reporter = LatestVersionReporter::from(printer).with_length(results.len() as u64); diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index a0b172fba..b87615f08 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -115,7 +115,7 @@ pub(crate) async fn pip_tree( prerelease, exclude_newer: &exclude_newer, tags: Some(tags), - requires_python: &requires_python, + requires_python: Some(&requires_python), }; let reporter = LatestVersionReporter::from(printer).with_length(packages.len() as u64); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 8b18e247c..bfbd4d772 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -219,6 +219,9 @@ pub(crate) enum ProjectError { #[error(transparent)] DependencyGroup(#[from] DependencyGroupError), + #[error(transparent)] + Client(#[from] uv_client::Error), + #[error(transparent)] Python(#[from] uv_python::Error), diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index d75c66d84..4ee2e80f8 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -232,7 +232,7 @@ pub(crate) async fn tree( capabilities: &capabilities, prerelease: lock.prerelease_mode(), exclude_newer: &lock.exclude_newer(), - requires_python: lock.requires_python(), + requires_python: Some(lock.requires_python()), tags: None, }; diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 5cba0a4b4..ac8f215d3 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -3,16 +3,17 @@ use std::str::FromStr; use anyhow::{Result, bail}; use owo_colors::OwoColorize; +use tokio::sync::Semaphore; use tracing::{debug, trace}; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; -use uv_client::BaseClientBuilder; +use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_configuration::{Concurrency, Constraints, DryRun, Reinstall, TargetTriple, Upgrade}; use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::{ - ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource, - UnresolvedRequirementSpecification, + ExtraBuildRequires, IndexCapabilities, NameRequirementSpecification, Requirement, + RequirementSource, UnresolvedRequirementSpecification, }; use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages}; use uv_normalize::PackageName; @@ -29,6 +30,7 @@ use uv_warnings::warn_user; use uv_workspace::WorkspaceCache; use crate::commands::ExitStatus; +use crate::commands::pip::latest::LatestClient; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::{self, Modifications}; use crate::commands::pip::{resolution_markers, resolution_tags}; @@ -224,6 +226,62 @@ pub(crate) async fn install( } }; + // For `@latest`, fetch the latest version and create a constraint. + let latest = if let ToolRequest::Package { + target: Target::Latest(_, name, _), + .. + } = &request + { + // Build the registry client to fetch the latest version. + let client = RegistryClientBuilder::new(client_builder.clone(), cache.clone()) + .index_locations(settings.resolver.index_locations.clone()) + .index_strategy(settings.resolver.index_strategy) + .markers(interpreter.markers()) + .platform(interpreter.platform()) + .build(); + + // Initialize the capabilities. + let capabilities = IndexCapabilities::default(); + let download_concurrency = Semaphore::new(concurrency.downloads); + + // Initialize the client to fetch the latest version. + let latest_client = LatestClient { + client: &client, + capabilities: &capabilities, + prerelease: settings.resolver.prerelease, + exclude_newer: &settings.resolver.exclude_newer, + tags: None, + requires_python: None, + }; + + // Fetch the latest version. + if let Some(dist_filename) = latest_client + .find_latest(name, None, &download_concurrency) + .await? + { + let version = dist_filename.version().clone(); + debug!("Resolved `{name}@latest` to `{name}=={version}`"); + + // The constraint pins the version during resolution to prevent backtracking. + Some(Requirement { + name: name.clone(), + extras: vec![].into_boxed_slice(), + groups: Box::new([]), + marker: MarkerTree::default(), + source: RequirementSource::Registry { + specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(version)), + index: None, + conflict: None, + }, + origin: None, + }) + } else { + None + } + } else { + None + }; + let package_name = &requirement.name; // If the user passed, e.g., `ruff@latest`, we need to mark it as upgradable. @@ -284,11 +342,11 @@ pub(crate) async fn install( }; // Resolve the constraints. - let constraints = spec + let constraints: Vec<_> = spec .constraints .into_iter() .map(|constraint| constraint.requirement) - .collect::>(); + .collect(); // Resolve the overrides. let overrides = resolve_names( @@ -412,7 +470,7 @@ pub(crate) async fn install( if matches!( site_packages.satisfies_requirements( requirements.iter(), - constraints.iter(), + constraints.iter().chain(latest.iter()), overrides.iter(), InstallationStrategy::Permissive, &markers, @@ -454,6 +512,7 @@ pub(crate) async fn install( constraints: constraints .iter() .cloned() + .chain(latest.into_iter()) .map(NameRequirementSpecification::from) .collect(), overrides: overrides diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 06522c8c7..f5d3ce883 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -10,20 +10,21 @@ use console::Term; use itertools::Itertools; use owo_colors::OwoColorize; use tokio::process::Command; +use tokio::sync::Semaphore; use tracing::{debug, warn}; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_cli::ExternalCommand; -use uv_client::BaseClientBuilder; +use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_configuration::Concurrency; use uv_configuration::Constraints; use uv_configuration::TargetTriple; use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::InstalledDist; use uv_distribution_types::{ - IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource, - UnresolvedRequirement, UnresolvedRequirementSpecification, + IndexCapabilities, IndexUrl, Name, NameRequirementSpecification, Requirement, + RequirementSource, UnresolvedRequirement, UnresolvedRequirementSpecification, }; use uv_fs::Simplified; use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages}; @@ -47,6 +48,7 @@ use uv_workspace::WorkspaceCache; use crate::child::run_to_completion; use crate::commands::ExitStatus; use crate::commands::pip; +use crate::commands::pip::latest::LatestClient; use crate::commands::pip::loggers::{ DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger, }; @@ -905,6 +907,62 @@ async fn get_or_create_environment( } }; + // For `@latest`, fetch the latest version and create a constraint. + let latest = if let ToolRequest::Package { + target: Target::Latest(_, name, _), + .. + } = &request + { + // Build the registry client to fetch the latest version. + let client = RegistryClientBuilder::new(client_builder.clone(), cache.clone()) + .index_locations(settings.resolver.index_locations.clone()) + .index_strategy(settings.resolver.index_strategy) + .markers(interpreter.markers()) + .platform(interpreter.platform()) + .build(); + + // Initialize the capabilities. + let capabilities = IndexCapabilities::default(); + let download_concurrency = Semaphore::new(concurrency.downloads); + + // Initialize the client to fetch the latest version. + let latest_client = LatestClient { + client: &client, + capabilities: &capabilities, + prerelease: settings.resolver.prerelease, + exclude_newer: &settings.resolver.exclude_newer, + tags: None, + requires_python: None, + }; + + // Fetch the latest version. + if let Some(dist_filename) = latest_client + .find_latest(name, None, &download_concurrency) + .await? + { + let version = dist_filename.version().clone(); + debug!("Resolved `{name}@latest` to `{name}=={version}`"); + + // The constraint pins the version during resolution to prevent backtracking. + Some(Requirement { + name: name.clone(), + extras: vec![].into_boxed_slice(), + groups: Box::new([]), + marker: MarkerTree::default(), + source: RequirementSource::Registry { + specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(version)), + index: None, + conflict: None, + }, + origin: None, + }) + } else { + None + } + } else { + None + }; + // Read the `--with` requirements. let spec = RequirementsSpecification::from_sources( with, @@ -1016,7 +1074,7 @@ async fn get_or_create_environment( if matches!( site_packages.satisfies_requirements( requirements.iter(), - constraints.iter(), + constraints.iter().chain(latest.iter()), overrides.iter(), InstallationStrategy::Permissive, &markers, @@ -1044,6 +1102,7 @@ async fn get_or_create_environment( .collect(), constraints: constraints .into_iter() + .chain(latest.into_iter()) .map(NameRequirementSpecification::from) .collect(), overrides: overrides