From dec6f5aa022dae01b14d46ab3da4cb820061c591 Mon Sep 17 00:00:00 2001 From: Luca Leonardo Scorcia Date: Mon, 30 Dec 2024 17:49:57 +0100 Subject: [PATCH 001/135] Initial support for ARMv5TE platform via cross compilation (#10234) ## Summary Allows uv to recognize the ARMv5TE platform. This platform is currently supported on Debian distributions. It is an older 32 bit platform mostly used in embedded devices, currently in rust tier 2.5 so it requires cross compilation. Fixes #10157 . ## Test Plan Tested directly on device by applying a slightly different patch to tag 0.5.4 which is used by the current Home Assistant version (2024.12.5). After the patch Home Assistant is able to recognize the Python venv and setup its dependencies. Patched uv was built with ``` $ CARGO_TARGET_ARMV5TE_UNKNOWN_LINUX_GNUEABI_LINKER="/usr/bin/arm-linux-gnueabi-gcc" maturin build --release --target armv5te-unknown-linux-gnueabi --manylinux off ``` The target wheel was then moved on the device and installed via pip install. --- Cargo.toml | 2 +- crates/uv-platform-tags/src/platform.rs | 4 +++- crates/uv-python/src/platform.rs | 6 +++++- crates/uv-python/template-download-metadata.py | 2 ++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 706789b1157e..f1b73bd65c10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ axoupdater = { version = "0.9.0", default-features = false } backoff = { version = "0.4.0" } base64 = { version = "0.22.1" } bitflags = { version = "2.6.0" } -boxcar = { version = "0.2.5" } +boxcar = { version = "0.2.8" } bytecheck = { version = "0.8.0" } cachedir = { version = "0.3.1" } cargo-util = { version = "0.2.14" } diff --git a/crates/uv-platform-tags/src/platform.rs b/crates/uv-platform-tags/src/platform.rs index 12876bb0831a..e3418a17dc45 100644 --- a/crates/uv-platform-tags/src/platform.rs +++ b/crates/uv-platform-tags/src/platform.rs @@ -77,6 +77,7 @@ impl fmt::Display for Os { pub enum Arch { #[serde(alias = "arm64")] Aarch64, + Armv5TEL, Armv6L, #[serde(alias = "armv8l")] Armv7L, @@ -96,6 +97,7 @@ impl fmt::Display for Arch { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Self::Aarch64 => write!(f, "aarch64"), + Self::Armv5TEL => write!(f, "armv5tel"), Self::Armv6L => write!(f, "armv6l"), Self::Armv7L => write!(f, "armv7l"), Self::Powerpc64Le => write!(f, "ppc64le"), @@ -122,7 +124,7 @@ impl Arch { // manylinux_2_31 Self::Riscv64 => Some(31), // unsupported - Self::Armv6L => None, + Self::Armv5TEL | Self::Armv6L => None, } } } diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs index 9f933633d0c5..67342ae1a988 100644 --- a/crates/uv-python/src/platform.rs +++ b/crates/uv-python/src/platform.rs @@ -54,7 +54,7 @@ impl Libc { // Checks if the CPU supports hardware floating-point operations. // Depending on the result, it selects either the `gnueabihf` (hard-float) or `gnueabi` (soft-float) environment. // download-metadata.json only includes armv7. - "arm" | "armv7" => match detect_hardware_floating_point_support() { + "arm" | "armv5te" | "armv7" => match detect_hardware_floating_point_support() { Ok(true) => target_lexicon::Environment::Gnueabihf, Ok(false) => target_lexicon::Environment::Gnueabi, Err(_) => target_lexicon::Environment::Gnu, @@ -240,6 +240,10 @@ impl From<&uv_platform_tags::Arch> for Arch { ), variant: None, }, + uv_platform_tags::Arch::Armv5TEL => Self { + family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv5te), + variant: None, + }, uv_platform_tags::Arch::Armv6L => Self { family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv6), variant: None, diff --git a/crates/uv-python/template-download-metadata.py b/crates/uv-python/template-download-metadata.py index 95313a699777..4be2d0ef98bb 100755 --- a/crates/uv-python/template-download-metadata.py +++ b/crates/uv-python/template-download-metadata.py @@ -69,6 +69,8 @@ def prepare_arch(arch: dict) -> tuple[str, str]: family = "X86_32(target_lexicon::X86_32Architecture::I686)" case "aarch64": family = "Aarch64(target_lexicon::Aarch64Architecture::Aarch64)" + case "armv5tel": + family = "Arm(target_lexicon::ArmArchitecture::Armv5te)" case "armv7": family = "Arm(target_lexicon::ArmArchitecture::Armv7)" case value: From dcd96a83aa5254d6b5068d2c68b074a0376d4100 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 30 Dec 2024 12:47:06 -0500 Subject: [PATCH 002/135] Respect static metadata for already-installed distributions (#10242) ## Summary Closes https://github.com/astral-sh/uv/issues/10239#issuecomment-2565663046 --- .../uv-distribution-types/src/dist_error.rs | 22 ++- crates/uv-distribution-types/src/lib.rs | 2 + crates/uv-distribution-types/src/requested.rs | 71 ++++++++++ .../src/distribution_database.rs | 29 +++- crates/uv-distribution/src/error.rs | 4 +- crates/uv-installer/src/preparer.rs | 2 +- crates/uv-requirements/src/lib.rs | 6 +- crates/uv-resolver/src/error.rs | 8 +- crates/uv-resolver/src/resolver/mod.rs | 37 ++--- crates/uv-resolver/src/resolver/provider.rs | 28 +++- crates/uv/src/commands/diagnostics.rs | 51 ++++++- crates/uv/tests/it/pip_install.rs | 134 ++++++++++++++++++ 12 files changed, 347 insertions(+), 47 deletions(-) create mode 100644 crates/uv-distribution-types/src/requested.rs diff --git a/crates/uv-distribution-types/src/dist_error.rs b/crates/uv-distribution-types/src/dist_error.rs index 9bfcf8e974bb..fcbb13dde83b 100644 --- a/crates/uv-distribution-types/src/dist_error.rs +++ b/crates/uv-distribution-types/src/dist_error.rs @@ -1,12 +1,17 @@ -use crate::{BuiltDist, Dist, DistRef, Edge, Name, Node, Resolution, ResolvedDist, SourceDist}; +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Formatter}; + use petgraph::prelude::EdgeRef; use petgraph::Direction; use rustc_hash::FxHashSet; -use std::collections::VecDeque; -use std::fmt::{Debug, Display, Formatter}; +use version_ranges::Ranges; + use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::Version; -use version_ranges::Ranges; + +use crate::{ + BuiltDist, Dist, DistRef, Edge, Name, Node, RequestedDist, Resolution, ResolvedDist, SourceDist, +}; /// Inspect whether an error type is a build error. pub trait IsBuildBackendError: std::error::Error + Send + Sync + 'static { @@ -25,7 +30,14 @@ pub enum DistErrorKind { } impl DistErrorKind { - pub fn from_dist_and_err(dist: &Dist, err: &impl IsBuildBackendError) -> Self { + pub fn from_requested_dist(dist: &RequestedDist, err: &impl IsBuildBackendError) -> Self { + match dist { + RequestedDist::Installed(_) => DistErrorKind::Read, + RequestedDist::Installable(dist) => Self::from_dist(dist, err), + } + } + + pub fn from_dist(dist: &Dist, err: &impl IsBuildBackendError) -> Self { if err.is_build_backend_error() { DistErrorKind::BuildBackend } else { diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index 9aba9e5c5f14..c9a0c48b50cd 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -67,6 +67,7 @@ pub use crate::installed::*; pub use crate::origin::*; pub use crate::pip_index::*; pub use crate::prioritized_distribution::*; +pub use crate::requested::*; pub use crate::resolution::*; pub use crate::resolved::*; pub use crate::specified_requirement::*; @@ -90,6 +91,7 @@ mod installed; mod origin; mod pip_index; mod prioritized_distribution; +mod requested; mod resolution; mod resolved; mod specified_requirement; diff --git a/crates/uv-distribution-types/src/requested.rs b/crates/uv-distribution-types/src/requested.rs new file mode 100644 index 000000000000..b804a16adb88 --- /dev/null +++ b/crates/uv-distribution-types/src/requested.rs @@ -0,0 +1,71 @@ +use std::fmt::{Display, Formatter}; + +use crate::{ + Dist, DistributionId, DistributionMetadata, Identifier, InstalledDist, Name, ResourceId, + VersionOrUrlRef, +}; +use uv_normalize::PackageName; +use uv_pep440::Version; + +/// A distribution that can be requested during resolution. +/// +/// Either an already-installed distribution or a distribution that can be installed. +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub enum RequestedDist { + Installed(InstalledDist), + Installable(Dist), +} + +impl RequestedDist { + /// Returns the version of the distribution, if it is known. + pub fn version(&self) -> Option<&Version> { + match self { + Self::Installed(dist) => Some(dist.version()), + Self::Installable(dist) => dist.version(), + } + } +} + +impl Name for RequestedDist { + fn name(&self) -> &PackageName { + match self { + Self::Installable(dist) => dist.name(), + Self::Installed(dist) => dist.name(), + } + } +} + +impl DistributionMetadata for RequestedDist { + fn version_or_url(&self) -> VersionOrUrlRef { + match self { + Self::Installed(dist) => dist.version_or_url(), + Self::Installable(dist) => dist.version_or_url(), + } + } +} + +impl Identifier for RequestedDist { + fn distribution_id(&self) -> DistributionId { + match self { + Self::Installed(dist) => dist.distribution_id(), + Self::Installable(dist) => dist.distribution_id(), + } + } + + fn resource_id(&self) -> ResourceId { + match self { + Self::Installed(dist) => dist.resource_id(), + Self::Installable(dist) => dist.resource_id(), + } + } +} + +impl Display for RequestedDist { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Installed(dist) => dist.fmt(f), + Self::Installable(dist) => dist.fmt(f), + } + } +} diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 95b52b516051..d1ab5a5d0519 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -21,7 +21,8 @@ use uv_client::{ }; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{ - BuildableSource, BuiltDist, Dist, FileLocation, HashPolicy, Hashed, Name, SourceDist, + BuildableSource, BuiltDist, Dist, FileLocation, HashPolicy, Hashed, InstalledDist, Name, + SourceDist, }; use uv_extract::hash::Hasher; use uv_fs::write_atomic; @@ -115,6 +116,32 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } } + /// Either fetch the only wheel metadata (directly from the index or with range requests) or + /// fetch and build the source distribution. + /// + /// While hashes will be generated in some cases, hash-checking is only enforced for source + /// distributions, and should be enforced by the caller for wheels. + #[instrument(skip_all, fields(%dist))] + pub async fn get_installed_metadata( + &self, + dist: &InstalledDist, + ) -> Result { + // If the metadata was provided by the user directly, prefer it. + if let Some(metadata) = self + .build_context + .dependency_metadata() + .get(dist.name(), Some(dist.version())) + { + return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); + } + + let metadata = dist + .metadata() + .map_err(|err| Error::ReadInstalled(Box::new(dist.clone()), err))?; + + Ok(ArchiveMetadata::from_metadata23(metadata)) + } + /// Either fetch the only wheel metadata (directly from the index or with range requests) or /// fetch and build the source distribution. /// diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 217682501bd8..84315658e42e 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -8,7 +8,7 @@ use zip::result::ZipError; use crate::metadata::MetadataError; use uv_client::WrappedReqwestError; use uv_distribution_filename::WheelFilenameError; -use uv_distribution_types::IsBuildBackendError; +use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError}; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; @@ -82,6 +82,8 @@ pub enum Error { Metadata(#[from] uv_pypi_types::MetadataError), #[error("Failed to read metadata: `{}`", _0.user_display())] WheelMetadata(PathBuf, #[source] Box), + #[error("Failed to read metadata from installed package `{0}`")] + ReadInstalled(Box, #[source] InstalledDistError), #[error("Failed to read zip archive from built wheel")] Zip(#[from] ZipError), #[error("Source distribution directory contains neither readable `pyproject.toml` nor `setup.py`: `{}`", _0.user_display())] diff --git a/crates/uv-installer/src/preparer.rs b/crates/uv-installer/src/preparer.rs index fac49057025c..541383de10da 100644 --- a/crates/uv-installer/src/preparer.rs +++ b/crates/uv-installer/src/preparer.rs @@ -230,7 +230,7 @@ impl Error { let chain = DerivationChain::from_resolution(resolution, (&dist).into()).unwrap_or_default(); Self::Dist( - DistErrorKind::from_dist_and_err(&dist, &err), + DistErrorKind::from_dist(&dist, &err), Box::new(dist), chain, err, diff --git a/crates/uv-requirements/src/lib.rs b/crates/uv-requirements/src/lib.rs index adb342e133c6..a6bff1998a61 100644 --- a/crates/uv-requirements/src/lib.rs +++ b/crates/uv-requirements/src/lib.rs @@ -35,11 +35,7 @@ pub enum Error { impl Error { /// Create an [`Error`] from a distribution error. pub(crate) fn from_dist(dist: Dist, err: uv_distribution::Error) -> Self { - Self::Dist( - DistErrorKind::from_dist_and_err(&dist, &err), - Box::new(dist), - err, - ) + Self::Dist(DistErrorKind::from_dist(&dist, &err), Box::new(dist), err) } } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 70ef20f60e0d..2cd333c40a82 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -10,8 +10,7 @@ use rustc_hash::FxHashMap; use tracing::trace; use uv_distribution_types::{ - DerivationChain, Dist, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, - InstalledDist, InstalledDistError, + DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist, }; use uv_normalize::{ExtraName, PackageName}; use uv_pep440::{LocalVersionSlice, Version}; @@ -98,14 +97,11 @@ pub enum ResolveError { #[error("{0} `{1}`")] Dist( DistErrorKind, - Box, + Box, DerivationChain, #[source] Arc, ), - #[error("Failed to read metadata from installed package `{0}`")] - ReadInstalled(Box, #[source] InstalledDistError), - #[error(transparent)] NoSolution(#[from] NoSolutionError), diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index d27f0df1b76b..7873a44e0a89 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -21,7 +21,7 @@ use tokio_stream::wrappers::ReceiverStream; use tracing::{debug, info, instrument, trace, warn, Level}; use uv_configuration::{Constraints, Overrides}; -use uv_distribution::{ArchiveMetadata, DistributionDatabase}; +use uv_distribution::DistributionDatabase; use uv_distribution_types::{ BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel, IndexCapabilities, IndexLocations, @@ -33,9 +33,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifiers, MIN_VERSION}; use uv_pep508::MarkerTree; use uv_platform_tags::Tags; -use uv_pypi_types::{ - ConflictItem, ConflictItemRef, Conflicts, Requirement, ResolutionMetadata, VerbatimParsedUrl, -}; +use uv_pypi_types::{ConflictItem, ConflictItemRef, Conflicts, Requirement, VerbatimParsedUrl}; use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider}; use uv_warnings::warn_user_once; @@ -1097,11 +1095,11 @@ impl ResolverState { - // TODO(charlie): Add derivation chain for URL dependencies. In practice, this isn't - // critical since we fetch URL dependencies _prior_ to invoking the resolver. return Err(ResolveError::Dist( - DistErrorKind::from_dist_and_err(dist, &**err), + DistErrorKind::from_requested_dist(dist, &**err), dist.clone(), DerivationChain::default(), err.clone(), @@ -1642,7 +1640,7 @@ impl ResolverState ResolverState { trace!("Received installed distribution metadata for: {dist}"); - self.index.distributions().done( - dist.version_id(), - Arc::new(MetadataResponse::Found(ArchiveMetadata::from_metadata23( - metadata, - ))), - ); + self.index + .distributions() + .done(dist.version_id(), Arc::new(metadata)); } Some(Response::Dist { dist, metadata }) => { let dist_kind = match dist { @@ -2134,10 +2129,8 @@ impl ResolverState { - // TODO(charlie): This should be return a `MetadataResponse`. - let metadata = dist - .metadata() - .map_err(|err| ResolveError::ReadInstalled(Box::new(dist.clone()), err))?; + let metadata = provider.get_installed_metadata(&dist).boxed_local().await?; + Ok(Some(Response::Installed { dist, metadata })) } @@ -2251,9 +2244,9 @@ impl ResolverState { - let metadata = dist.metadata().map_err(|err| { - ResolveError::ReadInstalled(Box::new(dist.clone()), err) - })?; + let metadata = + provider.get_installed_metadata(&dist).boxed_local().await?; + Response::Installed { dist, metadata } } }; @@ -3079,7 +3072,7 @@ enum Response { /// The returned metadata for an already-installed distribution. Installed { dist: InstalledDist, - metadata: ResolutionMetadata, + metadata: MetadataResponse, }, } diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 29e02384699e..03cda439d294 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use uv_configuration::BuildOptions; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; -use uv_distribution_types::{Dist, IndexCapabilities, IndexUrl}; +use uv_distribution_types::{Dist, IndexCapabilities, IndexUrl, InstalledDist, RequestedDist}; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::Tags; @@ -37,7 +37,7 @@ pub enum MetadataResponse { /// A non-fatal error. Unavailable(MetadataUnavailable), /// The distribution could not be built or downloaded, a fatal error. - Error(Box, Arc), + Error(Box, Arc), } /// Non-fatal metadata fetching error. @@ -83,7 +83,7 @@ pub trait ResolverProvider { /// Get the metadata for a distribution. /// - /// For a wheel, this is done by querying it's (remote) metadata, for a source dist we + /// For a wheel, this is done by querying it (remote) metadata. For a source distribution, we /// (fetch and) build the source distribution and return the metadata from the built /// distribution. fn get_or_build_wheel_metadata<'io>( @@ -91,6 +91,12 @@ pub trait ResolverProvider { dist: &'io Dist, ) -> impl Future + 'io; + /// Get the metadata for an installed distribution. + fn get_installed_metadata<'io>( + &'io self, + dist: &'io InstalledDist, + ) -> impl Future + 'io; + /// Set the [`uv_distribution::Reporter`] to use for this installer. #[must_use] fn with_reporter(self, reporter: impl uv_distribution::Reporter + 'static) -> Self; @@ -246,13 +252,27 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, )) } err => Ok(MetadataResponse::Error( - Box::new(dist.clone()), + Box::new(RequestedDist::Installable(dist.clone())), Arc::new(err), )), }, } } + /// Return the metadata for an installed distribution. + async fn get_installed_metadata<'io>( + &'io self, + dist: &'io InstalledDist, + ) -> WheelMetadataResult { + match self.fetcher.get_installed_metadata(dist).await { + Ok(metadata) => Ok(MetadataResponse::Found(metadata)), + Err(err) => Ok(MetadataResponse::Error( + Box::new(RequestedDist::Installed(dist.clone())), + Arc::new(err), + )), + } + } + /// Set the [`uv_distribution::Reporter`] to use for this installer. #[must_use] fn with_reporter(self, reporter: impl uv_distribution::Reporter + 'static) -> Self { diff --git a/crates/uv/src/commands/diagnostics.rs b/crates/uv/src/commands/diagnostics.rs index 4767cff090b3..2d375e6f7201 100644 --- a/crates/uv/src/commands/diagnostics.rs +++ b/crates/uv/src/commands/diagnostics.rs @@ -5,7 +5,9 @@ use owo_colors::OwoColorize; use rustc_hash::FxHashMap; use version_ranges::Ranges; -use uv_distribution_types::{DerivationChain, DerivationStep, Dist, DistErrorKind, Name}; +use uv_distribution_types::{ + DerivationChain, DerivationStep, Dist, DistErrorKind, Name, RequestedDist, +}; use uv_normalize::PackageName; use uv_pep440::Version; use uv_resolver::SentinelRange; @@ -78,7 +80,7 @@ impl OperationDiagnostic { chain, err, )) => { - dist_error(kind, dist, &chain, err); + requested_dist_error(kind, dist, &chain, err); None } pip::operations::Error::Requirements(uv_requirements::Error::Dist(kind, dist, err)) => { @@ -154,6 +156,51 @@ pub(crate) fn dist_error( anstream::eprint!("{report:?}"); } +/// Render a requested distribution failure (read, download or build) with a help message. +pub(crate) fn requested_dist_error( + kind: DistErrorKind, + dist: Box, + chain: &DerivationChain, + cause: Arc, +) { + #[derive(Debug, miette::Diagnostic, thiserror::Error)] + #[error("{kind} `{dist}`")] + #[diagnostic()] + struct Diagnostic { + kind: DistErrorKind, + dist: Box, + #[source] + cause: Arc, + #[help] + help: Option, + } + + let help = SUGGESTIONS + .get(dist.name()) + .map(|suggestion| { + format!( + "`{}` is often confused for `{}` Did you mean to install `{}` instead?", + dist.name().cyan(), + suggestion.cyan(), + suggestion.cyan(), + ) + }) + .or_else(|| { + if chain.is_empty() { + None + } else { + Some(format_chain(dist.name(), dist.version(), chain)) + } + }); + let report = miette::Report::new(Diagnostic { + kind, + dist, + cause, + help, + }); + anstream::eprint!("{report:?}"); +} + /// Render a [`uv_resolver::NoSolutionError`]. pub(crate) fn no_solution(err: &uv_resolver::NoSolutionError) { let report = miette::Report::msg(format!("{err}")).context(err.header()); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 1a8df7a7a68c..c51e3819f3e3 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -7724,3 +7724,137 @@ fn missing_subdirectory_url() -> Result<()> { Ok(()) } + +#[test] +fn static_metadata_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.12"); + + context.temp_dir.child("pyproject.toml").write_str( + r#" + [project] + name = "example" + version = "0.0.0" + dependencies = [ + "anyio==3.7.0" + ] + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["typing-extensions"] + "#, + )?; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("-r") + .arg("pyproject.toml"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + anyio==3.7.0 + + typing-extensions==4.10.0 + "### + ); + + Ok(()) +} + +#[test] +fn static_metadata_source_tree() -> Result<()> { + let context = TestContext::new("3.12"); + + context.temp_dir.child("pyproject.toml").write_str( + r#" + [project] + name = "example" + version = "0.0.0" + dependencies = [ + "anyio==3.7.0" + ] + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["typing-extensions"] + "#, + )?; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("-e") + .arg("."), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + example==0.0.0 (from file://[TEMP_DIR]/) + + typing-extensions==4.10.0 + "### + ); + + Ok(()) +} + +/// Regression test for: +#[test] +fn static_metadata_already_installed() -> Result<()> { + let context = TestContext::new("3.12"); + + context.temp_dir.child("pyproject.toml").write_str( + r#" + [project] + name = "example" + version = "0.0.0" + dependencies = [ + "anyio==3.7.0" + ] + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["typing-extensions"] + "#, + )?; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("-r") + .arg("pyproject.toml"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + anyio==3.7.0 + + typing-extensions==4.10.0 + "### + ); + + uv_snapshot!(context.filters(), context.pip_install() + .arg("-e") + .arg("."), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + example==0.0.0 (from file://[TEMP_DIR]/) + "### + ); + + Ok(()) +} From 7f1ee9c6dd97815e66ac3f57204ae64b7b83656f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 30 Dec 2024 21:05:00 -0500 Subject: [PATCH 003/135] Accept directories with space names in `uv init` (#10246) ## Summary Closes https://github.com/astral-sh/uv/issues/10245. --- crates/uv/src/commands/project/init.rs | 5 ++- crates/uv/tests/it/init.rs | 49 +++++++++++++++++--------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 708a7dc0423a..88e33a41b2bf 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -121,7 +121,10 @@ pub(crate) async fn init( .and_then(|path| path.to_str()) .context("Missing directory name")?; - PackageName::new(name.to_string())? + // Pre-normalize the package name by removing any leading or trailing + // whitespace, and replacing any internal whitespace with hyphens. + let name = name.trim().replace(' ', "-"); + PackageName::new(name)? } }; diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index d43246e8702a..969aa8d3a33e 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -1218,7 +1218,7 @@ fn init_workspace_outside() -> Result<()> { fn init_normalized_names() -> Result<()> { let context = TestContext::new("3.12"); - // `foo-bar` module is normalized to `foo_bar`. + // `foo-bar` module is normalized to `foo-bar`. uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo-bar").arg("--lib"), @r###" success: true exit_code: 0 @@ -1252,17 +1252,17 @@ fn init_normalized_names() -> Result<()> { ); }); - // `foo-bar` module is normalized to `foo_bar`. - uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo-bar").arg("--app"), @r###" - success: false - exit_code: 2 + // `bar_baz` module is normalized to `bar-baz`. + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("bar_baz").arg("--app"), @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Project is already initialized in `[TEMP_DIR]/foo-bar` (`pyproject.toml` file exists) + Initialized project `bar-baz` at `[TEMP_DIR]/bar_baz` "###); - let child = context.temp_dir.child("foo-bar"); + let child = context.temp_dir.child("bar_baz"); let pyproject = fs_err::read_to_string(child.join("pyproject.toml"))?; insta::with_settings!({ @@ -1271,30 +1271,45 @@ fn init_normalized_names() -> Result<()> { assert_snapshot!( pyproject, @r###" [project] - name = "foo-bar" + name = "bar-baz" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); - // "bar baz" is not allowed. - uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("bar baz"), @r###" - success: false - exit_code: 2 + // "baz bop" is normalized to "baz-bop". + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("baz bop"), @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Not a valid package or extra name: "bar baz". Names must start and end with a letter or digit and may only contain -, _, ., and alphanumeric characters. + Initialized project `baz-bop` at `[TEMP_DIR]/baz bop` "###); + let child = context.temp_dir.child("baz bop"); + let pyproject = fs_err::read_to_string(child.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "baz-bop" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + Ok(()) } From a2f436f79bb32595f85aa9e07b59382825d2826f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Tue, 31 Dec 2024 11:00:37 +0800 Subject: [PATCH 004/135] Add loongarch64 to supported Python platform tags (#10223) --- crates/uv-platform-tags/src/platform.rs | 4 +++- crates/uv-python/src/platform.rs | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/uv-platform-tags/src/platform.rs b/crates/uv-platform-tags/src/platform.rs index e3418a17dc45..697891b19d31 100644 --- a/crates/uv-platform-tags/src/platform.rs +++ b/crates/uv-platform-tags/src/platform.rs @@ -90,6 +90,7 @@ pub enum Arch { #[serde(alias = "amd64")] X86_64, S390X, + LoongArch64, Riscv64, } @@ -105,6 +106,7 @@ impl fmt::Display for Arch { Self::X86 => write!(f, "i686"), Self::X86_64 => write!(f, "x86_64"), Self::S390X => write!(f, "s390x"), + Self::LoongArch64 => write!(f, "loongarch64"), Self::Riscv64 => write!(f, "riscv64"), } } @@ -124,7 +126,7 @@ impl Arch { // manylinux_2_31 Self::Riscv64 => Some(31), // unsupported - Self::Armv5TEL | Self::Armv6L => None, + Self::Armv5TEL | Self::Armv6L | Self::LoongArch64 => None, } } } diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs index 67342ae1a988..d34634754378 100644 --- a/crates/uv-python/src/platform.rs +++ b/crates/uv-python/src/platform.rs @@ -274,6 +274,10 @@ impl From<&uv_platform_tags::Arch> for Arch { family: target_lexicon::Architecture::X86_64, variant: None, }, + uv_platform_tags::Arch::LoongArch64 => Self { + family: target_lexicon::Architecture::LoongArch64, + variant: None, + }, uv_platform_tags::Arch::Riscv64 => Self { family: target_lexicon::Architecture::Riscv64( target_lexicon::Riscv64Architecture::Riscv64, From c77aa5820b64ec6855daa83e369330573f6bae53 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 31 Dec 2024 10:37:46 -0500 Subject: [PATCH 005/135] Add a required version setting to uv (#10248) ## Summary This follows Ruff's design exactly: you can provide a version specifier (like `>=0.5`), and we'll enforce it at runtime. Closes https://github.com/astral-sh/uv/issues/8605. --- Cargo.lock | 1 + crates/uv-configuration/Cargo.toml | 1 + crates/uv-configuration/src/lib.rs | 2 + .../uv-configuration/src/required_version.rs | 61 +++++++++++++++++++ crates/uv-settings/src/combine.rs | 8 ++- crates/uv-settings/src/settings.rs | 21 ++++++- crates/uv/src/lib.rs | 11 ++++ crates/uv/src/settings.rs | 7 ++- crates/uv/tests/it/pip_install.rs | 6 +- crates/uv/tests/it/show_settings.rs | 37 ++++++++++- docs/reference/settings.md | 29 +++++++++ uv.schema.json | 15 +++++ 12 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 crates/uv-configuration/src/required_version.rs diff --git a/Cargo.lock b/Cargo.lock index c4f8c7857852..8e45d60e9e9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4772,6 +4772,7 @@ dependencies = [ "uv-cache-info", "uv-cache-key", "uv-normalize", + "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-pypi-types", diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 83de06f5cc35..cc8b00757a4e 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -21,6 +21,7 @@ uv-cache = { workspace = true } uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-normalize = { workspace = true } +uv-pep440 = { workspace = true } uv-pep508 = { workspace = true, features = ["schemars"] } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index 6eb47a89a82a..62dfcb4a1e78 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -16,6 +16,7 @@ pub use package_options::*; pub use preview::*; pub use project_build_backend::*; pub use rayon::*; +pub use required_version::*; pub use sources::*; pub use target_triple::*; pub use trusted_host::*; @@ -40,6 +41,7 @@ mod package_options; mod preview; mod project_build_backend; mod rayon; +mod required_version; mod sources; mod target_triple; mod trusted_host; diff --git a/crates/uv-configuration/src/required_version.rs b/crates/uv-configuration/src/required_version.rs new file mode 100644 index 000000000000..339abb1cfdb4 --- /dev/null +++ b/crates/uv-configuration/src/required_version.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers, VersionSpecifiersParseError}; + +/// A required version of uv, represented as a version specifier (e.g. `>=0.5.0`). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RequiredVersion(VersionSpecifiers); + +impl RequiredVersion { + /// Return `true` if the given version is required. + pub fn contains(&self, version: &Version) -> bool { + self.0.contains(version) + } +} + +impl FromStr for RequiredVersion { + type Err = VersionSpecifiersParseError; + + fn from_str(s: &str) -> Result { + // Treat `0.5.0` as `==0.5.0`, for backwards compatibility. + if let Ok(version) = Version::from_str(s) { + Ok(Self(VersionSpecifiers::from( + VersionSpecifier::equals_version(version), + ))) + } else { + Ok(Self(VersionSpecifiers::from_str(s)?)) + } + } +} + +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for RequiredVersion { + fn schema_name() -> String { + String::from("RequiredVersion") + } + + fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::String.into()), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some("A version specifier, e.g. `>=0.5.0` or `==0.5.0`.".to_string()), + ..schemars::schema::Metadata::default() + })), + ..schemars::schema::SchemaObject::default() + } + .into() + } +} + +impl<'de> serde::Deserialize<'de> for RequiredVersion { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl std::fmt::Display for RequiredVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index d333144740d2..14c37db8d433 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -4,7 +4,8 @@ use std::path::PathBuf; use url::Url; use uv_configuration::{ - ConfigSettings, IndexStrategy, KeyringProviderType, TargetTriple, TrustedPublishing, + ConfigSettings, IndexStrategy, KeyringProviderType, RequiredVersion, TargetTriple, + TrustedPublishing, }; use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex}; use uv_install_wheel::linker::LinkMode; @@ -73,12 +74,12 @@ macro_rules! impl_combine_or { impl_combine_or!(AnnotationStyle); impl_combine_or!(ExcludeNewer); +impl_combine_or!(ForkStrategy); impl_combine_or!(Index); impl_combine_or!(IndexStrategy); impl_combine_or!(IndexUrl); impl_combine_or!(KeyringProviderType); impl_combine_or!(LinkMode); -impl_combine_or!(ForkStrategy); impl_combine_or!(NonZeroUsize); impl_combine_or!(PathBuf); impl_combine_or!(PipExtraIndex); @@ -88,10 +89,11 @@ impl_combine_or!(PrereleaseMode); impl_combine_or!(PythonDownloads); impl_combine_or!(PythonPreference); impl_combine_or!(PythonVersion); +impl_combine_or!(RequiredVersion); impl_combine_or!(ResolutionMode); +impl_combine_or!(SchemaConflicts); impl_combine_or!(String); impl_combine_or!(SupportedEnvironments); -impl_combine_or!(SchemaConflicts); impl_combine_or!(TargetTriple); impl_combine_or!(TrustedPublishing); impl_combine_or!(Url); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 1285f0d6d1b5..bf9815e7f070 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -3,8 +3,8 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf}; use url::Url; use uv_cache_info::CacheKey; use uv_configuration::{ - ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple, - TrustedHost, TrustedPublishing, + ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, RequiredVersion, + TargetTriple, TrustedHost, TrustedPublishing, }; use uv_distribution_types::{ Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata, @@ -149,6 +149,20 @@ impl Options { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct GlobalOptions { + /// Enforce a requirement on the version of uv. + /// + /// If the version of uv does not meet the requirement at runtime, uv will exit + /// with an error. + /// + /// Accepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`. + #[option( + default = "null", + value_type = "str", + example = r#" + required-version = ">=0.5.0" + "# + )] + pub required_version: Option, /// Whether to load TLS certificates from the platform's native certificate store. /// /// By default, uv loads certificates from the bundled `webpki-roots` crate. The @@ -1623,6 +1637,7 @@ impl From for ResolverInstallerOptions { pub struct OptionsWire { // #[serde(flatten)] // globals: GlobalOptions + required_version: Option, native_tls: Option, offline: Option, no_cache: Option, @@ -1704,6 +1719,7 @@ pub struct OptionsWire { impl From for Options { fn from(value: OptionsWire) -> Self { let OptionsWire { + required_version, native_tls, offline, no_cache, @@ -1764,6 +1780,7 @@ impl From for Options { Self { globals: GlobalOptions { + required_version, native_tls, offline, no_cache, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e30baf2df113..9e7be8ce3796 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -4,6 +4,7 @@ use std::fmt::Write; use std::io::stdout; use std::path::Path; use std::process::ExitCode; +use std::str::FromStr; use std::sync::atomic::Ordering; use anstream::eprintln; @@ -208,6 +209,16 @@ async fn run(mut cli: Cli) -> Result { // Resolve the cache settings. let cache_settings = CacheSettings::resolve(*cli.top_level.cache_args, filesystem.as_ref()); + // Enforce the required version. + if let Some(required_version) = globals.required_version.as_ref() { + let package_version = uv_pep440::Version::from_str(uv_version::version())?; + if !required_version.contains(&package_version) { + return Err(anyhow::anyhow!( + "Required version `{required_version}` does not match the running version `{package_version}`", + )); + } + } + // Configure the `tracing` crate, which controls internal logging. #[cfg(feature = "tracing-durations-export")] let (duration_layer, _duration_guard) = logging::setup_duration()?; diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 3acbc107a5e9..fff0dabb6e1b 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -23,8 +23,8 @@ use uv_client::Connectivity; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, DevGroupsSpecification, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, - NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, SourceStrategy, TargetTriple, - TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, + NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, RequiredVersion, + SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl}; use uv_install_wheel::linker::LinkMode; @@ -53,6 +53,7 @@ const PYPI_PUBLISH_URL: &str = "https://upload.pypi.org/legacy/"; #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct GlobalSettings { + pub(crate) required_version: Option, pub(crate) quiet: bool, pub(crate) verbose: u8, pub(crate) color: ColorChoice, @@ -72,6 +73,8 @@ impl GlobalSettings { /// Resolve the [`GlobalSettings`] from the CLI and filesystem configuration. pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { Self { + required_version: workspace + .and_then(|workspace| workspace.globals.required_version.clone()), quiet: args.quiet, verbose: args.verbose, color: if let Some(color_choice) = args.color { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index c51e3819f3e3..4742a85bfbcb 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -218,8 +218,8 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { let mut filters = context.filters(); filters.push(( - "expected one of `native-tls`, `offline`, .*", - "expected one of `native-tls`, `offline`, [...]", + "expected one of `required-version`, `native-tls`, .*", + "expected one of `required-version`, `native-tls`, [...]", )); uv_snapshot!(filters, context.pip_install() @@ -235,7 +235,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { | 2 | unknown = "field" | ^^^^^^^ - unknown field `unknown`, expected one of `native-tls`, `offline`, [...] + unknown field `unknown`, expected one of `required-version`, `native-tls`, [...] Resolved in [TIME] Audited in [TIME] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index fb8ea039f9ef..3cf5d5ddc4ea 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -63,6 +63,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -215,6 +216,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -368,6 +370,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -553,6 +556,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -707,6 +711,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -840,6 +845,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1017,6 +1023,7 @@ fn resolve_index_url() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1200,6 +1207,7 @@ fn resolve_index_url() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1437,6 +1445,7 @@ fn resolve_find_links() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1613,6 +1622,7 @@ fn resolve_top_level() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1752,6 +1762,7 @@ fn resolve_top_level() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1933,6 +1944,7 @@ fn resolve_top_level() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2138,6 +2150,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2267,6 +2280,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2396,6 +2410,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2527,6 +2542,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2677,6 +2693,7 @@ fn resolve_tool() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2835,6 +2852,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2992,6 +3010,7 @@ fn resolve_both() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3267,6 +3286,7 @@ fn resolve_config_file() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3438,7 +3458,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` "### ); @@ -3520,6 +3540,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3652,6 +3673,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3792,6 +3814,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3946,6 +3969,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4129,6 +4153,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4318,6 +4343,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4502,6 +4528,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4693,6 +4720,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4877,6 +4905,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5074,6 +5103,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5197,6 +5227,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5318,6 +5349,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5441,6 +5473,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5562,6 +5595,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5684,6 +5718,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, diff --git a/docs/reference/settings.md b/docs/reference/settings.md index e0e2fc738dd3..e8a142900a40 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1522,6 +1522,35 @@ Reinstall a specific package, regardless of whether it's already installed. Impl --- +### [`required-version`](#required-version) {: #required-version } + +Enforce a requirement on the version of uv. + +If the version of uv does not meet the requirement at runtime, uv will exit +with an error. + +Accepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`. + +**Default value**: `null` + +**Type**: `str` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + required-version = ">=0.5.0" + ``` +=== "uv.toml" + + ```toml + required-version = ">=0.5.0" + ``` + +--- + ### [`resolution`](#resolution) {: #resolution } The strategy to use when selecting between the different compatible versions for a given diff --git a/uv.schema.json b/uv.schema.json index de5b6eaae721..fcba78967b0d 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -443,6 +443,17 @@ "$ref": "#/definitions/PackageName" } }, + "required-version": { + "description": "Enforce a requirement on the version of uv.\n\nIf the version of uv does not meet the requirement at runtime, uv will exit with an error.\n\nAccepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`.", + "anyOf": [ + { + "$ref": "#/definitions/RequiredVersion" + }, + { + "type": "null" + } + ] + }, "resolution": { "description": "The strategy to use when selecting between the different compatible versions for a given package requirement.\n\nBy default, uv will use the latest compatible version of each package (`highest`).", "anyOf": [ @@ -1409,6 +1420,10 @@ "type": "string", "pattern": "^3\\.\\d+(\\.\\d+)?$" }, + "RequiredVersion": { + "description": "A version specifier, e.g. `>=0.5.0` or `==0.5.0`.", + "type": "string" + }, "Requirement": { "description": "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`", "type": "string" From cf88828e551c5cda0bbc29b873ebad95ccb8daa6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 31 Dec 2024 12:59:26 -0500 Subject: [PATCH 006/135] Avoid stripping query parameters from URLs (#10253) ## Summary Closes https://github.com/astral-sh/uv/issues/10251. --- crates/uv-distribution-types/src/file.rs | 15 +++++++++++---- crates/uv-resolver/src/lock/mod.rs | 8 +++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index bfbbbf7e49b5..5336a1b7fde2 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -157,15 +157,22 @@ impl UrlString { /// Return the [`UrlString`] with any query parameters and fragments removed. pub fn base_str(&self) -> &str { self.as_ref() - .split_once(['#', '?']) + .split_once('?') + .or_else(|| self.as_ref().split_once('#')) .map(|(path, _)| path) .unwrap_or(self.as_ref()) } - /// Return the [`UrlString`] with any query parameters and fragments removed. + /// Return the [`UrlString`] with any fragments removed. #[must_use] - pub fn as_base_url(&self) -> Self { - Self(self.base_str().to_string()) + pub fn without_fragment(&self) -> Self { + Self( + self.as_ref() + .split_once('#') + .map(|(path, _)| path) + .unwrap_or(self.as_ref()) + .to_string(), + ) } } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 2e523458e75e..7d85bcd6cd7b 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -3862,15 +3862,14 @@ impl<'de> serde::Deserialize<'de> for Hash { /// Convert a [`FileLocation`] into a normalized [`UrlString`]. fn normalize_file_location(location: &FileLocation) -> Result { match location { - FileLocation::AbsoluteUrl(ref absolute) => Ok(absolute.as_base_url()), + FileLocation::AbsoluteUrl(ref absolute) => Ok(absolute.without_fragment()), FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)), } } -/// Convert a [`Url`] into a normalized [`UrlString`]. +/// Convert a [`Url`] into a normalized [`UrlString`] by removing the fragment. fn normalize_url(mut url: Url) -> UrlString { url.set_fragment(None); - url.set_query(None); UrlString::from(url) } @@ -3995,9 +3994,8 @@ fn normalize_requirement(requirement: Requirement, root: &Path) -> Result Date: Tue, 31 Dec 2024 14:16:08 -0500 Subject: [PATCH 007/135] Add tests for `UrlString` methods (#10256) --- crates/uv-distribution-types/src/file.rs | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index 5336a1b7fde2..10de4a9c6345 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -275,3 +275,35 @@ pub enum ToUrlError { path: String, }, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn base_str() { + let url = UrlString("https://example.com/path?query#fragment".to_string()); + assert_eq!(url.base_str(), "https://example.com/path"); + + let url = UrlString("https://example.com/path#fragment".to_string()); + assert_eq!(url.base_str(), "https://example.com/path"); + + let url = UrlString("https://example.com/path".to_string()); + assert_eq!(url.base_str(), "https://example.com/path"); + } + + #[test] + fn without_fragment() { + let url = UrlString("https://example.com/path?query#fragment".to_string()); + assert_eq!( + url.without_fragment(), + UrlString("https://example.com/path?query".to_string()) + ); + + let url = UrlString("https://example.com/path#fragment".to_string()); + assert_eq!(url.base_str(), "https://example.com/path"); + + let url = UrlString("https://example.com/path".to_string()); + assert_eq!(url.base_str(), "https://example.com/path"); + } +} From 9e0b35ad8280924fc9e347f668d6153d925673de Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 31 Dec 2024 22:22:42 -0500 Subject: [PATCH 008/135] Detect cyclic dependencies during builds (#10258) ## Summary Closes https://github.com/astral-sh/uv/issues/10255#issuecomment-2566782671. --- crates/uv-build-frontend/src/error.rs | 7 ++- crates/uv-build-frontend/src/lib.rs | 24 ++++++--- crates/uv-dispatch/src/lib.rs | 36 ++++++++++--- .../src/distribution_database.rs | 13 ++++- crates/uv-distribution/src/source/mod.rs | 17 +++++- crates/uv-installer/src/preparer.rs | 2 + crates/uv-types/src/traits.rs | 30 +++++++++-- crates/uv/src/commands/build_frontend.rs | 4 +- crates/uv/src/commands/venv.rs | 8 +-- crates/uv/tests/it/lock.rs | 2 +- crates/uv/tests/it/pip_install.rs | 52 +++++++++++++++++++ 11 files changed, 165 insertions(+), 30 deletions(-) diff --git a/crates/uv-build-frontend/src/error.rs b/crates/uv-build-frontend/src/error.rs index 81fda9d877b5..e8d10d65f26f 100644 --- a/crates/uv-build-frontend/src/error.rs +++ b/crates/uv-build-frontend/src/error.rs @@ -84,10 +84,12 @@ pub enum Error { #[error("Failed to build PATH for build script")] BuildScriptPath(#[source] env::JoinPathsError), // For the convenience of typing `setup_build` properly. - #[error("Building source distributions for {0} is disabled")] + #[error("Building source distributions for `{0}` is disabled")] NoSourceDistBuild(PackageName), #[error("Building source distributions is disabled")] NoSourceDistBuilds, + #[error("Cyclic build dependency detected for `{0}`")] + CyclicBuildDependency(PackageName), } impl IsBuildBackendError for Error { @@ -103,7 +105,8 @@ impl IsBuildBackendError for Error { | Self::RequirementsInstall(_, _) | Self::Virtualenv(_) | Self::NoSourceDistBuild(_) - | Self::NoSourceDistBuilds => false, + | Self::NoSourceDistBuilds + | Self::CyclicBuildDependency(_) => false, Self::CommandFailed(_, _) | Self::BuildBackend(_) | Self::MissingHeader(_) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 75ea2f6382f2..d8952b55340f 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -35,7 +35,7 @@ use uv_pep508::PackageName; use uv_pypi_types::{Requirement, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonEnvironment}; use uv_static::EnvVars; -use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, SourceBuildTrait}; +use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait}; pub use crate::error::{Error, MissingHeaderCause}; @@ -255,6 +255,7 @@ impl SourceBuild { source_strategy: SourceStrategy, config_settings: ConfigSettings, build_isolation: BuildIsolation<'_>, + build_stack: &BuildStack, build_kind: BuildKind, mut environment_variables: FxHashMap, level: BuildOutput, @@ -318,11 +319,12 @@ impl SourceBuild { source_build_context, &default_backend, &pep517_backend, + build_stack, ) .await?; build_context - .install(&resolved_requirements, &venv) + .install(&resolved_requirements, &venv, build_stack) .await .map_err(|err| Error::RequirementsInstall("`build-system.requires`", err.into()))?; } else { @@ -378,6 +380,7 @@ impl SourceBuild { version_id, locations, source_strategy, + build_stack, build_kind, level, &config_settings, @@ -412,6 +415,7 @@ impl SourceBuild { source_build_context: SourceBuildContext, default_backend: &Pep517Backend, pep517_backend: &Pep517Backend, + build_stack: &BuildStack, ) -> Result { Ok( if pep517_backend.requirements == default_backend.requirements { @@ -420,7 +424,7 @@ impl SourceBuild { resolved_requirements.clone() } else { let resolved_requirements = build_context - .resolve(&default_backend.requirements) + .resolve(&default_backend.requirements, build_stack) .await .map_err(|err| { Error::RequirementsResolve("`setup.py` build", err.into()) @@ -430,7 +434,7 @@ impl SourceBuild { } } else { build_context - .resolve(&pep517_backend.requirements) + .resolve(&pep517_backend.requirements, build_stack) .await .map_err(|err| { Error::RequirementsResolve("`build-system.requires`", err.into()) @@ -806,6 +810,7 @@ async fn create_pep517_build_environment( version_id: Option<&str>, locations: &IndexLocations, source_strategy: SourceStrategy, + build_stack: &BuildStack, build_kind: BuildKind, level: BuildOutput, config_settings: &ConfigSettings, @@ -929,12 +934,15 @@ async fn create_pep517_build_environment( .cloned() .chain(extra_requires) .collect(); - let resolution = build_context.resolve(&requirements).await.map_err(|err| { - Error::RequirementsResolve("`build-system.requires`", AnyErrorBuild::from(err)) - })?; + let resolution = build_context + .resolve(&requirements, build_stack) + .await + .map_err(|err| { + Error::RequirementsResolve("`build-system.requires`", AnyErrorBuild::from(err)) + })?; build_context - .install(&resolution, venv) + .install(&resolution, venv, build_stack) .await .map_err(|err| { Error::RequirementsInstall("`build-system.requires`", AnyErrorBuild::from(err)) diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 77e8c237d9f6..c84e81271c0a 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -23,8 +23,8 @@ use uv_configuration::{BuildOutput, Concurrency}; use uv_distribution::DistributionDatabase; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ - CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, IsBuildBackendError, Name, - Resolution, SourceDist, VersionOrUrlRef, + CachedDist, DependencyMetadata, Identifier, IndexCapabilities, IndexLocations, + IsBuildBackendError, Name, Resolution, SourceDist, VersionOrUrlRef, }; use uv_git::GitResolver; use uv_installer::{Installer, Plan, Planner, Preparer, SitePackages}; @@ -35,7 +35,8 @@ use uv_resolver::{ PythonRequirement, Resolver, ResolverEnvironment, }; use uv_types::{ - AnyErrorBuild, BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight, + AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages, HashStrategy, + InFlight, }; #[derive(Debug, Error)] @@ -208,6 +209,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { async fn resolve<'data>( &'data self, requirements: &'data [Requirement], + build_stack: &'data BuildStack, ) -> Result { let python_requirement = PythonRequirement::from_interpreter(self.interpreter); let marker_env = self.interpreter.resolver_marker_environment(); @@ -223,8 +225,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { .build(), &python_requirement, ResolverEnvironment::specific(marker_env), - // Conflicting groups only make sense when doing - // universal resolution. + // Conflicting groups only make sense when doing universal resolution. Conflicts::empty(), Some(tags), self.flat_index, @@ -232,7 +233,8 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.hasher, self, EmptyInstalledPackages, - DistributionDatabase::new(self.client, self, self.concurrency.downloads), + DistributionDatabase::new(self.client, self, self.concurrency.downloads) + .with_build_stack(build_stack), )?; let resolution = Resolution::from(resolver.resolve().await.with_context(|| { format!( @@ -257,6 +259,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { &'data self, resolution: &'data Resolution, venv: &'data PythonEnvironment, + build_stack: &'data BuildStack, ) -> Result, BuildDispatchError> { debug!( "Installing in {} in {}", @@ -296,17 +299,27 @@ impl<'a> BuildContext for BuildDispatch<'a> { return Ok(vec![]); } + // Verify that none of the missing distributions are already in the build stack. + for dist in &remote { + let id = dist.distribution_id(); + if build_stack.contains(&id) { + return Err(BuildDispatchError::BuildFrontend( + uv_build_frontend::Error::CyclicBuildDependency(dist.name().clone()).into(), + )); + } + } + // Download any missing distributions. let wheels = if remote.is_empty() { vec![] } else { - // TODO(konstin): Check that there is no endless recursion. let preparer = Preparer::new( self.cache, tags, self.hasher, self.build_options, - DistributionDatabase::new(self.client, self, self.concurrency.downloads), + DistributionDatabase::new(self.client, self, self.concurrency.downloads) + .with_build_stack(build_stack), ); debug!( @@ -367,6 +380,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { sources: SourceStrategy, build_kind: BuildKind, build_output: BuildOutput, + mut build_stack: BuildStack, ) -> Result { let dist_name = dist.map(uv_distribution_types::Name::name); let dist_version = dist @@ -392,6 +406,11 @@ impl<'a> BuildContext for BuildDispatch<'a> { return Err(err); } + // Push the current distribution onto the build stack, to prevent cyclic dependencies. + if let Some(dist) = dist { + build_stack.insert(dist.distribution_id()); + } + let builder = SourceBuild::setup( source, subdirectory, @@ -406,6 +425,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { sources, self.config_settings.clone(), self.build_isolation, + &build_stack, build_kind, self.build_extra_env_vars.clone(), build_output, diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index d1ab5a5d0519..0904269199b6 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -28,7 +28,7 @@ use uv_extract::hash::Hasher; use uv_fs::write_atomic; use uv_platform_tags::Tags; use uv_pypi_types::HashDigest; -use uv_types::BuildContext; +use uv_types::{BuildContext, BuildStack}; use crate::archive::Archive; use crate::locks::Locks; @@ -71,7 +71,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } } - /// Set the [`Reporter`] to use for this source distribution fetcher. + /// Set the build stack to use for the [`DistributionDatabase`]. + #[must_use] + pub fn with_build_stack(self, build_stack: &'a BuildStack) -> Self { + Self { + builder: self.builder.with_build_stack(build_stack), + ..self + } + } + + /// Set the [`Reporter`] to use for the [`DistributionDatabase`]. #[must_use] pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self { let reporter = Arc::new(reporter); diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 2beae3ca46ef..62164aabb5d8 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -38,7 +38,7 @@ use uv_normalize::PackageName; use uv_pep440::{release_specifiers_to_ranges, Version}; use uv_platform_tags::Tags; use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata12, RequiresTxt, ResolutionMetadata}; -use uv_types::{BuildContext, SourceBuildTrait}; +use uv_types::{BuildContext, BuildStack, SourceBuildTrait}; use zip::ZipArchive; mod built_wheel_metadata; @@ -47,6 +47,7 @@ mod revision; /// Fetch and build a source distribution from a remote source, or from a local cache. pub(crate) struct SourceDistributionBuilder<'a, T: BuildContext> { build_context: &'a T, + build_stack: Option<&'a BuildStack>, reporter: Option>, } @@ -67,11 +68,21 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { pub(crate) fn new(build_context: &'a T) -> Self { Self { build_context, + build_stack: None, reporter: None, } } - /// Set the [`Reporter`] to use for this source distribution fetcher. + /// Set the [`BuildStack`] to use for the [`SourceDistributionBuilder`]. + #[must_use] + pub(crate) fn with_build_stack(self, build_stack: &'a BuildStack) -> Self { + Self { + build_stack: Some(build_stack), + ..self + } + } + + /// Set the [`Reporter`] to use for the [`SourceDistributionBuilder`]. #[must_use] pub(crate) fn with_reporter(self, reporter: Arc) -> Self { Self { @@ -1898,6 +1909,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { BuildKind::Wheel }, BuildOutput::Debug, + self.build_stack.cloned().unwrap_or_default(), ) .await .map_err(|err| Error::Build(err.into()))? @@ -1974,6 +1986,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { BuildKind::Wheel }, BuildOutput::Debug, + self.build_stack.cloned().unwrap_or_default(), ) .await .map_err(|err| Error::Build(err.into()))?; diff --git a/crates/uv-installer/src/preparer.rs b/crates/uv-installer/src/preparer.rs index 541383de10da..4f3e3e46e1ad 100644 --- a/crates/uv-installer/src/preparer.rs +++ b/crates/uv-installer/src/preparer.rs @@ -220,6 +220,8 @@ pub enum Error { DerivationChain, #[source] uv_distribution::Error, ), + #[error("Cyclic build dependency detected for `{0}`")] + CyclicBuildDependency(PackageName), #[error("Unzip failed in another thread: {0}")] Thread(String), } diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index fc88cab097fd..bfc2bf8a0e23 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -2,17 +2,18 @@ use std::fmt::{Debug, Display, Formatter}; use std::future::Future; use std::ops::Deref; use std::path::{Path, PathBuf}; -use uv_distribution_filename::DistFilename; use anyhow::Result; +use rustc_hash::FxHashSet; use uv_cache::Cache; use uv_configuration::{ BuildKind, BuildOptions, BuildOutput, ConfigSettings, LowerBound, SourceStrategy, }; +use uv_distribution_filename::DistFilename; use uv_distribution_types::{ - CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, InstalledDist, - IsBuildBackendError, Resolution, SourceDist, + CachedDist, DependencyMetadata, DistributionId, IndexCapabilities, IndexLocations, + InstalledDist, IsBuildBackendError, Resolution, SourceDist, }; use uv_git::GitResolver; use uv_pep508::PackageName; @@ -96,6 +97,7 @@ pub trait BuildContext { fn resolve<'a>( &'a self, requirements: &'a [Requirement], + build_stack: &'a BuildStack, ) -> impl Future> + 'a; /// Install the given set of package versions into the virtual environment. The environment must @@ -104,6 +106,7 @@ pub trait BuildContext { &'a self, resolution: &'a Resolution, venv: &'a PythonEnvironment, + build_stack: &'a BuildStack, ) -> impl Future, impl IsBuildBackendError>> + 'a; /// Set up a source distribution build by installing the required dependencies. A wrapper for @@ -123,6 +126,7 @@ pub trait BuildContext { sources: SourceStrategy, build_kind: BuildKind, build_output: BuildOutput, + build_stack: BuildStack, ) -> impl Future> + 'a; /// Build by calling directly into the uv build backend without PEP 517, if possible. @@ -244,3 +248,23 @@ impl Deref for AnyErrorBuild { &*self.0 } } + +/// The stack of packages being built. +#[derive(Debug, Clone, Default)] +pub struct BuildStack(FxHashSet); + +impl BuildStack { + /// Return an empty stack. + pub fn empty() -> Self { + Self(FxHashSet::default()) + } + + pub fn contains(&self, id: &DistributionId) -> bool { + self.0.contains(id) + } + + /// Push a package onto the stack. + pub fn insert(&mut self, id: DistributionId) -> bool { + self.0.insert(id) + } +} diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 0ec665e386c7..0569b72f17a6 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -42,7 +42,7 @@ use uv_python::{ use uv_requirements::RequirementsSource; use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython}; use uv_settings::PythonInstallMirrors; -use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, HashStrategy}; +use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError}; #[derive(Debug, Error)] @@ -921,6 +921,7 @@ async fn build_sdist( sources, BuildKind::Sdist, build_output, + BuildStack::default(), ) .await .map_err(|err| Error::BuildDispatch(err.into()))?; @@ -1018,6 +1019,7 @@ async fn build_wheel( sources, BuildKind::Wheel, build_output, + BuildStack::default(), ) .await .map_err(|err| Error::BuildDispatch(err.into()))?; diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 71ff32b6260f..6b7c10f95f34 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -26,7 +26,7 @@ use uv_python::{ use uv_resolver::{ExcludeNewer, FlatIndex}; use uv_settings::PythonInstallMirrors; use uv_shell::{shlex_posix, shlex_windows, Shell}; -use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, HashStrategy}; +use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError}; @@ -353,16 +353,18 @@ async fn venv_impl( )] }; + let build_stack = BuildStack::default(); + // Resolve and install the requirements. // // Since the virtual environment is empty, and the set of requirements is trivial (no // constraints, no editables, etc.), we can use the build dispatch APIs directly. let resolution = build_dispatch - .resolve(&requirements) + .resolve(&requirements, &build_stack) .await .map_err(|err| VenvError::Seed(err.into()))?; let installed = build_dispatch - .install(&resolution, &venv) + .install(&resolution, &venv, &build_stack) .await .map_err(|err| VenvError::Seed(err.into()))?; diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 962eca2de435..b8533a09fa2f 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -20592,7 +20592,7 @@ fn lock_no_build_dynamic_metadata() -> Result<()> { ----- stderr ----- × Failed to build `dummy @ file://[TEMP_DIR]/` - ╰─▶ Building source distributions for dummy is disabled + ╰─▶ Building source distributions for `dummy` is disabled "###); Ok(()) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 4742a85bfbcb..ba7134271147 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -7858,3 +7858,55 @@ fn static_metadata_already_installed() -> Result<()> { Ok(()) } + +/// `circular-one` depends on `circular-two` as a build dependency, but `circular-two` depends on +/// `circular-one` was a runtime dependency. +#[test] +fn cyclic_build_dependency() { + static EXCLUDE_NEWER: &str = "2025-01-02T00:00:00Z"; + + let context = TestContext::new("3.13"); + + // Installing with `--no-binary circular-one` should fail, since we'll end up in a recursive + // build. + uv_snapshot!(context.filters(), context.pip_install() + .env(EnvVars::UV_EXCLUDE_NEWER, EXCLUDE_NEWER) + .arg("circular-one") + .arg("--extra-index-url") + .arg("https://test.pypi.org/simple") + .arg("--index-strategy") + .arg("unsafe-best-match") + .arg("--no-binary") + .arg("circular-one"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + × Failed to download and build `circular-one==0.2.0` + ├─▶ Failed to install requirements from `build-system.requires` + ╰─▶ Cyclic build dependency detected for `circular-one` + "### + ); + + // Installing without `--no-binary circular-one` should succeed, since we can use the wheel. + uv_snapshot!(context.filters(), context.pip_install() + .env(EnvVars::UV_EXCLUDE_NEWER, EXCLUDE_NEWER) + .arg("circular-one") + .arg("--extra-index-url") + .arg("https://test.pypi.org/simple") + .arg("--index-strategy") + .arg("unsafe-best-match"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + circular-one==0.2.0 + "### + ); +} From a65aebb4f1847dbb13e078f365edbb9afc79bd37 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Tue, 31 Dec 2024 19:23:07 -0800 Subject: [PATCH 009/135] Fix small typo in editable packages docs (#10257) ## Summary Was reading through some of the docs and noticed this small typo in https://docs.astral.sh/uv/pip/packages/#editable-packages ## Test Plan Not applicable --- docs/pip/packages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pip/packages.md b/docs/pip/packages.md index 3643f4c0cdfd..ab363105e081 100644 --- a/docs/pip/packages.md +++ b/docs/pip/packages.md @@ -62,7 +62,7 @@ for installation from a private repository. ## Editable packages -Editable packages do not need to be reinstalled for change to their source code to be active. +Editable packages do not need to be reinstalled for changes to their source code to be active. To install the current project as an editable package From 7bbec6b2e454912f2835140e41438fa9d4ca8d6e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 1 Jan 2025 12:35:30 -0500 Subject: [PATCH 010/135] Parse URLs lazily in resolver (#10259) ## Summary The idea here is to avoid parsing all registry URLs upfront, and instead parse them when we need them. Closes #6133. --- crates/uv-client/src/error.rs | 2 +- crates/uv-client/src/registry_client.rs | 2 +- crates/uv-distribution-types/src/file.rs | 80 ++++++------------- .../src/distribution_database.rs | 2 +- crates/uv-distribution/src/error.rs | 4 +- crates/uv-distribution/src/source/mod.rs | 4 +- crates/uv-requirements/src/upgrade.rs | 2 +- crates/uv-resolver/src/lock/mod.rs | 47 ++++++----- .../uv-resolver/src/lock/requirements_txt.rs | 16 +++- 9 files changed, 70 insertions(+), 89 deletions(-) diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index 30df2fe0d769..b9b5a81c761f 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -139,7 +139,7 @@ impl From for Error { #[derive(Debug, thiserror::Error)] pub enum ErrorKind { #[error(transparent)] - UrlParse(#[from] url::ParseError), + InvalidUrl(#[from] uv_distribution_types::ToUrlError), #[error(transparent)] JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError), diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 0510b42c0386..c792a9882a84 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -474,7 +474,7 @@ impl RegistryClient { } } FileLocation::AbsoluteUrl(url) => { - let url = url.to_url(); + let url = url.to_url().map_err(ErrorKind::InvalidUrl)?; if url.scheme() == "file" { let path = url .to_file_path() diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index 10de4a9c6345..081cab1107d6 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -1,6 +1,4 @@ -use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; -use std::path::PathBuf; use std::str::FromStr; use jiff::Timestamp; @@ -8,7 +6,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use uv_pep440::{VersionSpecifiers, VersionSpecifiersParseError}; -use uv_pep508::VerbatimUrl; +use uv_pep508::split_scheme; use uv_pypi_types::{CoreMetadata, HashDigest, Yanked}; /// Error converting [`uv_pypi_types::File`] to [`distribution_type::File`]. @@ -56,9 +54,9 @@ impl File { .map_err(|err| FileConversionError::RequiresPython(err.line().clone(), err))?, size: file.size, upload_time_utc_ms: file.upload_time.map(Timestamp::as_millisecond), - url: match Url::parse(&file.url) { - Ok(url) => FileLocation::AbsoluteUrl(url.into()), - Err(_) => FileLocation::RelativeUrl(base.to_string(), file.url), + url: match split_scheme(&file.url) { + Some(..) => FileLocation::AbsoluteUrl(UrlString::new(file.url)), + None => FileLocation::RelativeUrl(base.to_string(), file.url), }, yanked: file.yanked, }) @@ -101,7 +99,7 @@ impl FileLocation { })?; Ok(joined) } - FileLocation::AbsoluteUrl(ref absolute) => Ok(absolute.to_url()), + FileLocation::AbsoluteUrl(ref absolute) => absolute.to_url(), } } @@ -128,7 +126,7 @@ impl Display for FileLocation { /// A [`Url`] represented as a `String`. /// -/// This type is guaranteed to be a valid URL but avoids being parsed into the [`Url`] type. +/// This type is not guaranteed to be a valid URL, and may error on conversion. #[derive( Debug, Clone, @@ -148,10 +146,17 @@ impl Display for FileLocation { pub struct UrlString(String); impl UrlString { + /// Create a new [`UrlString`] from a [`String`]. + pub fn new(url: String) -> Self { + Self(url) + } + /// Converts a [`UrlString`] to a [`Url`]. - pub fn to_url(&self) -> Url { - // This conversion can never fail as the only way to construct a `UrlString` is from a `Url`. - Url::from_str(&self.0).unwrap() + pub fn to_url(&self) -> Result { + Url::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute { + absolute: self.0.clone(), + err, + }) } /// Return the [`UrlString`] with any query parameters and fragments removed. @@ -194,42 +199,18 @@ impl From<&Url> for UrlString { } } -impl From> for UrlString { - fn from(value: Cow<'_, Url>) -> Self { - UrlString(value.to_string()) - } -} - -impl From for UrlString { - fn from(value: VerbatimUrl) -> Self { - UrlString(value.raw().to_string()) - } -} - -impl From<&VerbatimUrl> for UrlString { - fn from(value: &VerbatimUrl) -> Self { - UrlString(value.raw().to_string()) - } -} - -impl From for String { - fn from(value: UrlString) -> Self { - value.0 - } -} - impl Display for UrlString { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } -/// An error that occurs when a `FileLocation` is not a valid URL. +/// An error that occurs when a [`FileLocation`] is not a valid URL. #[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] pub enum ToUrlError { - /// An error that occurs when the base URL in `FileLocation::Relative` + /// An error that occurs when the base URL in [`FileLocation::Relative`] /// could not be parsed as a valid URL. - #[error("could not parse base URL `{base}` as a valid URL")] + #[error("Could not parse base URL `{base}` as a valid URL")] InvalidBase { /// The base URL that could not be parsed as a valid URL. base: String, @@ -238,8 +219,8 @@ pub enum ToUrlError { err: url::ParseError, }, /// An error that occurs when the base URL could not be joined with - /// the relative path in a `FileLocation::Relative`. - #[error("could not join base URL `{base}` to relative path `{path}`")] + /// the relative path in a [`FileLocation::Relative`]. + #[error("Could not join base URL `{base}` to relative path `{path}`")] InvalidJoin { /// The base URL that could not be parsed as a valid URL. base: String, @@ -249,9 +230,9 @@ pub enum ToUrlError { #[source] err: url::ParseError, }, - /// An error that occurs when the absolute URL in `FileLocation::Absolute` + /// An error that occurs when the absolute URL in [`FileLocation::Absolute`] /// could not be parsed as a valid URL. - #[error("could not parse absolute URL `{absolute}` as a valid URL")] + #[error("Could not parse absolute URL `{absolute}` as a valid URL")] InvalidAbsolute { /// The absolute URL that could not be parsed as a valid URL. absolute: String, @@ -259,21 +240,6 @@ pub enum ToUrlError { #[source] err: url::ParseError, }, - /// An error that occurs when the file path in `FileLocation::Path` is - /// not valid UTF-8. We need paths to be valid UTF-8 to be transformed - /// into URLs, which must also be UTF-8. - #[error("could not build URL from file path `{path}` because it is not valid UTF-8")] - PathNotUtf8 { - /// The original path that was not valid UTF-8. - path: PathBuf, - }, - /// An error that occurs when the file URL created from a file path is not - /// a valid URL. - #[error("could not parse file path `{path}` as a valid URL")] - InvalidPath { - /// The file path URL that could not be parsed as a valid URL. - path: String, - }, } #[cfg(test)] diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 0904269199b6..3b0ab11a4cc2 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -187,7 +187,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { FileLocation::RelativeUrl(base, url) => { uv_pypi_types::base_url_join_relative(base, url)? } - FileLocation::AbsoluteUrl(url) => url.to_url(), + FileLocation::AbsoluteUrl(url) => url.to_url()?, }; // Create a cache entry for the wheel. diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 84315658e42e..0727b8896927 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -21,11 +21,11 @@ pub enum Error { NoBuild, // Network error - #[error("Failed to parse URL: {0}")] - Url(String, #[source] url::ParseError), #[error("Expected an absolute path, but received: {}", _0.user_display())] RelativePath(PathBuf), #[error(transparent)] + InvalidUrl(#[from] uv_distribution_types::ToUrlError), + #[error(transparent)] ParsedUrl(#[from] ParsedUrlError), #[error(transparent)] JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError), diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 62164aabb5d8..772c3d384442 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -114,7 +114,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { FileLocation::RelativeUrl(base, url) => { uv_pypi_types::base_url_join_relative(base, url)? } - FileLocation::AbsoluteUrl(url) => url.to_url(), + FileLocation::AbsoluteUrl(url) => url.to_url()?, }; // If the URL is a file URL, use the local path directly. @@ -263,7 +263,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { FileLocation::RelativeUrl(base, url) => { uv_pypi_types::base_url_join_relative(base, url)? } - FileLocation::AbsoluteUrl(url) => url.to_url(), + FileLocation::AbsoluteUrl(url) => url.to_url()?, }; // If the URL is a file URL, use the local path directly. diff --git a/crates/uv-requirements/src/upgrade.rs b/crates/uv-requirements/src/upgrade.rs index 8a472b373b94..7f7556e67d74 100644 --- a/crates/uv-requirements/src/upgrade.rs +++ b/crates/uv-requirements/src/upgrade.rs @@ -81,7 +81,7 @@ pub fn read_lock_requirements( preferences.push(Preference::from_lock(package, install_path)?); // Map each entry in the lockfile to a Git SHA. - if let Some(git_ref) = package.as_git_ref() { + if let Some(git_ref) = package.as_git_ref()? { git.push(git_ref); } } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 7d85bcd6cd7b..49f95bb7909e 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1098,7 +1098,7 @@ impl Lock { .into_iter() .filter_map(|index| match index.url() { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { - Some(UrlString::from(index.url().redacted())) + Some(UrlString::from(index.url().redacted().as_ref())) } IndexUrl::Path(_) => None, }) @@ -1814,7 +1814,7 @@ impl Package { let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone(); let url = Url::from(ParsedArchiveUrl { - url: url.to_url(), + url: url.to_url().map_err(LockErrorKind::InvalidUrl)?, subdirectory: direct.subdirectory.clone(), ext: DistExtension::Wheel, }); @@ -1946,7 +1946,7 @@ impl Package { Source::Git(url, git) => { // Remove the fragment and query from the URL; they're already present in the // `GitSource`. - let mut url = url.to_url(); + let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?; url.set_fragment(None); url.set_query(None); @@ -1976,7 +1976,7 @@ impl Package { let DistExtension::Source(ext) = DistExtension::from_path(url.as_ref())? else { return Ok(None); }; - let location = url.to_url(); + let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?; let subdirectory = direct.subdirectory.as_ref().map(PathBuf::from); let url = Url::from(ParsedArchiveUrl { url: location.clone(), @@ -2020,7 +2020,10 @@ impl Package { url: FileLocation::AbsoluteUrl(file_url.clone()), yanked: None, }); - let index = IndexUrl::from(VerbatimUrl::from_url(url.to_url())); + + let index = IndexUrl::from(VerbatimUrl::from_url( + url.to_url().map_err(LockErrorKind::InvalidUrl)?, + )); let reg_dist = RegistrySourceDist { name: self.id.name.clone(), @@ -2262,7 +2265,9 @@ impl Package { pub fn index(&self, root: &Path) -> Result, LockError> { match &self.id.source { Source::Registry(RegistrySource::Url(url)) => { - let index = IndexUrl::from(VerbatimUrl::from_url(url.to_url())); + let index = IndexUrl::from(VerbatimUrl::from_url( + url.to_url().map_err(LockErrorKind::InvalidUrl)?, + )); Ok(Some(index)) } Source::Registry(RegistrySource::Path(path)) => { @@ -2291,16 +2296,16 @@ impl Package { } /// Returns the [`ResolvedRepositoryReference`] for the package, if it is a Git source. - pub fn as_git_ref(&self) -> Option { + pub fn as_git_ref(&self) -> Result, LockError> { match &self.id.source { - Source::Git(url, git) => Some(ResolvedRepositoryReference { + Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference { reference: RepositoryReference { - url: RepositoryUrl::new(&url.to_url()), + url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?), reference: GitReference::from(git.kind.clone()), }, sha: git.precise, - }), - _ => None, + })), + _ => Ok(None), } } } @@ -3138,7 +3143,7 @@ impl SourceDist { match ®_dist.index { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { let url = normalize_file_location(®_dist.file.url) - .map_err(LockErrorKind::InvalidFileUrl) + .map_err(LockErrorKind::InvalidUrl) .map_err(LockError::from)?; let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from); let size = reg_dist.file.size; @@ -3153,7 +3158,7 @@ impl SourceDist { .file .url .to_url() - .map_err(LockErrorKind::InvalidFileUrl)? + .map_err(LockErrorKind::InvalidUrl)? .to_file_path() .map_err(|()| LockErrorKind::UrlToPath)?; let path = relative_to(®_dist_path, index_path) @@ -3444,7 +3449,7 @@ impl Wheel { match &wheel.index { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { let url = normalize_file_location(&wheel.file.url) - .map_err(LockErrorKind::InvalidFileUrl) + .map_err(LockErrorKind::InvalidUrl) .map_err(LockError::from)?; let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from); let size = wheel.file.size; @@ -3461,7 +3466,7 @@ impl Wheel { .file .url .to_url() - .map_err(LockErrorKind::InvalidFileUrl)? + .map_err(LockErrorKind::InvalidUrl)? .to_file_path() .map_err(|()| LockErrorKind::UrlToPath)?; let path = relative_to(&wheel_path, index_path) @@ -3507,7 +3512,7 @@ impl Wheel { let filename: WheelFilename = self.filename.clone(); match source { - RegistrySource::Url(index_url) => { + RegistrySource::Url(url) => { let file_url = match &self.url { WheelWireSource::Url { url } => url, WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => { @@ -3528,7 +3533,9 @@ impl Wheel { url: FileLocation::AbsoluteUrl(file_url.clone()), yanked: None, }); - let index = IndexUrl::from(VerbatimUrl::from_url(index_url.to_url())); + let index = IndexUrl::from(VerbatimUrl::from_url( + url.to_url().map_err(LockErrorKind::InvalidUrl)?, + )); Ok(RegistryBuiltWheel { filename, file, @@ -4094,11 +4101,11 @@ enum LockErrorKind { }, /// An error that occurs when the URL to a file for a wheel or /// source dist could not be converted to a structured `url::Url`. - #[error("Failed to parse wheel or source distribution URL")] - InvalidFileUrl( + #[error(transparent)] + InvalidUrl( /// The underlying error that occurred. This includes the /// errant URL in its error message. - #[source] + #[from] ToUrlError, ), /// An error that occurs when the extension can't be determined diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index 7290f61ba20b..694064727e34 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -303,7 +303,7 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { Source::Git(url, git) => { // Remove the fragment and query from the URL; they're already present in the // `GitSource`. - let mut url = url.to_url(); + let mut url = url.to_url().map_err(|_| std::fmt::Error)?; url.set_fragment(None); url.set_query(None); @@ -325,7 +325,7 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { Source::Direct(url, direct) => { let subdirectory = direct.subdirectory.as_ref().map(PathBuf::from); let url = Url::from(ParsedArchiveUrl { - url: url.to_url(), + url: url.to_url().map_err(|_| std::fmt::Error)?, subdirectory: subdirectory.clone(), ext: DistExtension::Source(SourceDistExtension::TarGz), }); @@ -333,7 +333,11 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { } Source::Path(path) | Source::Directory(path) => { if path.is_absolute() { - write!(f, "{}", Url::from_file_path(path).unwrap())?; + write!( + f, + "{}", + Url::from_file_path(path).map_err(|()| std::fmt::Error)? + )?; } else { write!(f, "{}", anchor(path).portable_display())?; } @@ -344,7 +348,11 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { } EditableMode::NonEditable => { if path.is_absolute() { - write!(f, "{}", Url::from_file_path(path).unwrap())?; + write!( + f, + "{}", + Url::from_file_path(path).map_err(|()| std::fmt::Error)? + )?; } else { write!(f, "{}", anchor(path).portable_display())?; } From ed4e4ff4b5683222b5baa8f100671ad4a159743c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 1 Jan 2025 20:21:36 -0500 Subject: [PATCH 011/135] Use `BTreeMap::range` to avoid iterating over unnecessary versions (#10266) ## Summary Closes https://github.com/astral-sh/uv/issues/6174. --- crates/uv-resolver/src/version_map.rs | 63 ++++++++++++++++++++------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index fc4a409d22c7..bf9a87983242 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -1,6 +1,9 @@ -use pubgrub::Range; use std::collections::btree_map::{BTreeMap, Entry}; +use std::collections::Bound; +use std::ops::RangeBounds; use std::sync::OnceLock; + +use pubgrub::Ranges; use tracing::instrument; use uv_client::{OwnedArchive, SimpleMetadata, VersionFiles}; @@ -156,8 +159,8 @@ impl VersionMap { /// for each version. pub(crate) fn iter( &self, - range: &Range, - ) -> impl DoubleEndedIterator + ExactSizeIterator { + range: &Ranges, + ) -> impl DoubleEndedIterator { // Performance optimization: If we only have a single version, return that version directly. if let Some(version) = range.as_singleton() { either::Either::Left(match self.inner { @@ -185,20 +188,24 @@ impl VersionMap { } else { either::Either::Right(match self.inner { VersionMapInner::Eager(ref eager) => { - either::Either::Left(eager.map.iter().map(|(version, dist)| { - let version_map_dist = VersionMapDistHandle { - inner: VersionMapDistHandleInner::Eager(dist), - }; - (version, version_map_dist) - })) + either::Either::Left(eager.map.range(BoundingRange::from(range)).map( + |(version, dist)| { + let version_map_dist = VersionMapDistHandle { + inner: VersionMapDistHandleInner::Eager(dist), + }; + (version, version_map_dist) + }, + )) } VersionMapInner::Lazy(ref lazy) => { - either::Either::Right(lazy.map.iter().map(|(version, dist)| { - let version_map_dist = VersionMapDistHandle { - inner: VersionMapDistHandleInner::Lazy { lazy, dist }, - }; - (version, version_map_dist) - })) + either::Either::Right(lazy.map.range(BoundingRange::from(range)).map( + |(version, dist)| { + let version_map_dist = VersionMapDistHandle { + inner: VersionMapDistHandleInner::Lazy { lazy, dist }, + }; + (version, version_map_dist) + }, + )) } }) } @@ -609,3 +616,29 @@ struct SimplePrioritizedDist { /// of writing, is to use `--exclude-newer 1900-01-01`.) dist: OnceLock>, } + +/// A range that can be used to iterate over a subset of a [`BTreeMap`]. +#[derive(Debug)] +struct BoundingRange<'a> { + min: Bound<&'a Version>, + max: Bound<&'a Version>, +} + +impl<'a> From<&'a Ranges> for BoundingRange<'a> { + fn from(value: &'a Ranges) -> Self { + let (min, max) = value + .bounding_range() + .unwrap_or((Bound::Unbounded, Bound::Unbounded)); + Self { min, max } + } +} + +impl<'a> RangeBounds for BoundingRange<'a> { + fn start_bound(&self) -> Bound<&'a Version> { + self.min + } + + fn end_bound(&self) -> Bound<&'a Version> { + self.max + } +} From 475e33af7e517d27cf3becb7c852a8290249e6be Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 2 Jan 2025 15:40:23 +0100 Subject: [PATCH 012/135] Fix `lock_pytorch_cpu` (#10271) A new release of pillow broke the test. By pinning the upper versions, we make the test stable. --- crates/uv/tests/it/lock.rs | 82 ++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index b8533a09fa2f..cf27c05d1f0b 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -21390,6 +21390,32 @@ fn lock_pytorch_cpu() -> Result<()> { { extra = "cu124" }, ], ] + constraint-dependencies = [ + "filelock<=3.16.1", + "fsspec<=2024.12.0", + "jinja2<=3.1.4", + "markupsafe<=3.0.2", + "mpmath<=1.3.0", + "networkx<=3.4.2", + "numpy<=2.2.0", + "nvidia-cublas-cu12<=12.4.5.8", + "nvidia-cuda-cupti-cu12<=12.4.127", + "nvidia-cuda-nvrtc-cu12<=12.4.127", + "nvidia-cuda-runtime-cu12<=12.4.127", + "nvidia-cudnn-cu12<=9.1.0.70", + "nvidia-cufft-cu12<=11.2.1.3", + "nvidia-curand-cu12<=10.3.5.147", + "nvidia-cusolver-cu12<=11.6.1.9", + "nvidia-cusparse-cu12<=12.3.1.170", + "nvidia-nccl-cu12<=2.21.5", + "nvidia-nvjitlink-cu12<=12.4.127", + "nvidia-nvtx-cu12<=12.4.127", + "pillow<=11.0.0", + "setuptools<=75.6.0", + "sympy<=1.13.1", + "triton<=3.1.0", + "typing-extensions<=4.12.2", + ] [tool.uv.sources] torch = [ @@ -21410,7 +21436,6 @@ fn lock_pytorch_cpu() -> Result<()> { name = "pytorch-cu124" url = "https://download.pytorch.org/whl/cu124" explicit = true - "#, )?; @@ -21433,6 +21458,9 @@ fn lock_pytorch_cpu() -> Result<()> { version = 1 requires-python = ">=3.12.[X]" resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine != 'x86_64' or sys_platform != 'linux'", "sys_platform != 'darwin'", "sys_platform == 'darwin'", ] @@ -21441,6 +21469,34 @@ fn lock_pytorch_cpu() -> Result<()> { { package = "project", extra = "cu124" }, ]] + [manifest] + constraints = [ + { name = "filelock", specifier = "<=3.16.1" }, + { name = "fsspec", specifier = "<=2024.12.0" }, + { name = "jinja2", specifier = "<=3.1.4" }, + { name = "markupsafe", specifier = "<=3.0.2" }, + { name = "mpmath", specifier = "<=1.3.0" }, + { name = "networkx", specifier = "<=3.4.2" }, + { name = "numpy", specifier = "<=2.2.0" }, + { name = "nvidia-cublas-cu12", specifier = "<=12.4.5.8" }, + { name = "nvidia-cuda-cupti-cu12", specifier = "<=12.4.127" }, + { name = "nvidia-cuda-nvrtc-cu12", specifier = "<=12.4.127" }, + { name = "nvidia-cuda-runtime-cu12", specifier = "<=12.4.127" }, + { name = "nvidia-cudnn-cu12", specifier = "<=9.1.0.70" }, + { name = "nvidia-cufft-cu12", specifier = "<=11.2.1.3" }, + { name = "nvidia-curand-cu12", specifier = "<=10.3.5.147" }, + { name = "nvidia-cusolver-cu12", specifier = "<=11.6.1.9" }, + { name = "nvidia-cusparse-cu12", specifier = "<=12.3.1.170" }, + { name = "nvidia-nccl-cu12", specifier = "<=2.21.5" }, + { name = "nvidia-nvjitlink-cu12", specifier = "<=12.4.127" }, + { name = "nvidia-nvtx-cu12", specifier = "<=12.4.127" }, + { name = "pillow", specifier = "<=11.0.0" }, + { name = "setuptools", specifier = "<=75.6.0" }, + { name = "sympy", specifier = "<=1.13.1" }, + { name = "triton", specifier = "<=3.1.0" }, + { name = "typing-extensions", specifier = "<=4.12.2" }, + ] + [[package]] name = "filelock" version = "3.16.1" @@ -21610,7 +21666,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "9.1.0.70" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 }, @@ -21622,7 +21678,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "11.2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 }, @@ -21645,9 +21701,9 @@ fn lock_pytorch_cpu() -> Result<()> { version = "11.6.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 }, @@ -21660,7 +21716,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "12.3.1.170" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 }, @@ -21833,6 +21889,11 @@ fn lock_pytorch_cpu() -> Result<()> { name = "torch" version = "2.5.1+cu124" source = { registry = "https://download.pytorch.org/whl/cu124" } + resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine != 'x86_64' or sys_platform != 'linux'", + ] dependencies = [ { name = "filelock" }, { name = "fsspec" }, @@ -21899,6 +21960,11 @@ fn lock_pytorch_cpu() -> Result<()> { name = "torchvision" version = "0.20.1+cu124" source = { registry = "https://download.pytorch.org/whl/cu124" } + resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine != 'x86_64' or sys_platform != 'linux'", + ] dependencies = [ { name = "numpy" }, { name = "pillow" }, @@ -21914,7 +21980,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, + { name = "filelock", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444 }, From f5a60d1a180a085acb43a1b05256687bc41de9fe Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 2 Jan 2025 15:40:38 +0100 Subject: [PATCH 013/135] Fix musl cdylib warning (#10268) The `cdylib` was used for the pyo3 bindings to uv-pep508, which don't exist anymore. It was now creating warnings on musl due to musl (statically linked) no supporting shared libraries. --- crates/uv-pep508/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index 15e0e0095b2c..912a226f7c0d 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -13,8 +13,6 @@ repository = { workspace = true } authors = { workspace = true } [lib] -name = "uv_pep508" -crate-type = ["cdylib", "rlib"] doctest = false [lints] From a3307d91a355efc56df8560e6f7f243b310ec947 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 2 Jan 2025 16:03:14 +0100 Subject: [PATCH 014/135] Actually use jemalloc (#10269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The uv-performance-memory-allocator is currently optimized out at least on musl due to the crate being otherwise unused (https://github.com/rust-lang/rust/issues/64402), causing musl to not use jemalloc and being slow. Command: ``` cargo build --target x86_64-unknown-linux-musl --profile profiling hyperfine --warmup 1 --runs 10 --prepare "uv venv -p 3.12" "target/x86_64-unknown-linux-musl/profiling/uv pip compile scripts/requirements/airflow.in" ``` Before: ``` Time (mean ± σ): 1.149 s ± 0.013 s [User: 1.498 s, System: 0.433 s] Range (min … max): 1.131 s … 1.173 s 10 runs ``` After: ``` Time (mean ± σ): 552.6 ms ± 4.7 ms [User: 771.7 ms, System: 197.5 ms] Range (min … max): 546.4 ms … 561.6 ms 10 runs --- crates/uv/src/bin/uv.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/uv/src/bin/uv.rs b/crates/uv/src/bin/uv.rs index dd3a216e3394..5405d75c3d11 100644 --- a/crates/uv/src/bin/uv.rs +++ b/crates/uv/src/bin/uv.rs @@ -1,3 +1,7 @@ +// Don't optimize the alloc crate away due to it being otherwise unused. +// https://github.com/rust-lang/rust/issues/64402 +extern crate uv_performance_memory_allocator; + use std::process::ExitCode; use uv::main as uv_main; From 2eba52f6749d87bf6041964fdf7550eadbd8bdee Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 2 Jan 2025 11:40:48 -0500 Subject: [PATCH 015/135] Avoid forking on version in non-universal resolutions (#10274) ## Summary Closes https://github.com/astral-sh/uv/issues/10275. --- crates/uv-resolver/src/resolver/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 7873a44e0a89..e57bc54e1791 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1328,6 +1328,11 @@ impl ResolverState, ) -> Result, ResolveError> { + // This only applies to universal resolutions. + if env.marker_environment().is_some() { + return Ok(None); + } + // For now, we only apply this to local versions. if !candidate.version().is_local() { return Ok(None); From d1a5a27da95998152c5c1a826c0a9404c98949e2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 2 Jan 2025 12:31:19 -0500 Subject: [PATCH 016/135] Gate performance feature on cfg (#10277) --- crates/uv/src/bin/uv.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv/src/bin/uv.rs b/crates/uv/src/bin/uv.rs index 5405d75c3d11..261618ca826e 100644 --- a/crates/uv/src/bin/uv.rs +++ b/crates/uv/src/bin/uv.rs @@ -1,5 +1,6 @@ // Don't optimize the alloc crate away due to it being otherwise unused. // https://github.com/rust-lang/rust/issues/64402 +#[cfg(feature = "performance-memory-allocator")] extern crate uv_performance_memory_allocator; use std::process::ExitCode; From 906511fa2365e0343a9dd3058117bbd6832b4be6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 2 Jan 2025 12:43:15 -0500 Subject: [PATCH 017/135] Ignore empty or missing hrefs in Simple HTML (#10276) ## Summary Closes https://github.com/astral-sh/uv/issues/7735. ## Test Plan `cargo run pip install -f https://whl.smartgic.io/ ggwave --python-platform linux` (fails prior to this PR; passes after) --- crates/uv-client/src/html.rs | 73 ++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/crates/uv-client/src/html.rs b/crates/uv-client/src/html.rs index 648c5165a7bb..9ea6580134c2 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use tl::{HTMLTag, Parser}; +use tl::HTMLTag; use tracing::{instrument, warn}; use url::Url; @@ -44,7 +44,12 @@ impl SimpleHtml { .iter() .filter_map(|node| node.as_tag()) .filter(|link| link.name().as_bytes() == b"a") - .map(|link| Self::parse_anchor(link, dom.parser())) + .map(|link| Self::parse_anchor(link)) + .filter_map(|result| match result { + Ok(None) => None, + Ok(Some(file)) => Some(Ok(file)), + Err(err) => Some(Err(err)), + }) .collect::, _>>()?; // While it has not been positively observed, we sort the files // to ensure we have a defined ordering. Otherwise, if we rely on @@ -70,14 +75,18 @@ impl SimpleHtml { } /// Parse a [`File`] from an `` tag. - fn parse_anchor(link: &HTMLTag, parser: &Parser) -> Result { + /// + /// Returns `None` if the `` don't doesn't have an `href` attribute. + fn parse_anchor(link: &HTMLTag) -> Result, Error> { // Extract the href. - let href = link + let Some(href) = link .attributes() .get("href") .flatten() .filter(|bytes| !bytes.as_bytes().is_empty()) - .ok_or(Error::MissingHref(link.inner_text(parser).to_string()))?; + else { + return Ok(None); + }; let href = std::str::from_utf8(href.as_bytes())?; // Extract the hash, which should be in the fragment. @@ -158,7 +167,7 @@ impl SimpleHtml { None }; - Ok(File { + Ok(Some(File { core_metadata, dist_info_metadata: None, data_dist_info_metadata: None, @@ -169,7 +178,7 @@ impl SimpleHtml { url: decoded.to_string(), size: None, upload_time: None, - }) + })) } } @@ -628,8 +637,29 @@ mod tests { "; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap_err(); - insta::assert_snapshot!(result, @"Missing href attribute on anchor link: `Jinja2-3.1.2-py3-none-any.whl`"); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [], + } + "###); } #[test] @@ -645,8 +675,29 @@ mod tests { "#; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap_err(); - insta::assert_snapshot!(result, @"Missing href attribute on anchor link: `Jinja2-3.1.2-py3-none-any.whl`"); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [], + } + "###); } #[test] From 9f1ba2b967ef36d649218ea03cbb23e708bc7d03 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 2 Jan 2025 14:31:04 -0500 Subject: [PATCH 018/135] Bump version to v0.5.14 (#10279) --- CHANGELOG.md | 30 +++++++++++++++++++++++++++ Cargo.lock | 4 ++-- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/getting-started/installation.md | 4 ++-- docs/guides/integration/docker.md | 8 +++---- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 6 +++--- pyproject.toml | 2 +- 9 files changed, 45 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23b3cfbac2ba..81289b3a7f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 0.5.14 + +### Enhancements + +- Add `--exact` flag to `uv run` ([#10198](https://github.com/astral-sh/uv/pull/10198)) +- Add `--outdated` support to `uv pip tree` ([#10199](https://github.com/astral-sh/uv/pull/10199)) +- Add a required version setting to uv ([#10248](https://github.com/astral-sh/uv/pull/10248)) +- Add loongarch64 to supported Python platform tags ([#10223](https://github.com/astral-sh/uv/pull/10223)) +- Add manylinux2014 aliases for `--python-platform` ([#10217](https://github.com/astral-sh/uv/pull/10217)) +- Add support for Python interpreters on ARMv5TE platforms ([#10234](https://github.com/astral-sh/uv/pull/10234)) +- Add support for optional `--description` in `uv init` ([#10209](https://github.com/astral-sh/uv/pull/10209)) +- Ignore empty or missing hrefs in Simple HTML ([#10276](https://github.com/astral-sh/uv/pull/10276)) +- Patch pkgconfig files after Python install ([#10189](https://github.com/astral-sh/uv/pull/10189)) + +### Performance + +- Actually use jemalloc as alternative allocator ([#10269](https://github.com/astral-sh/uv/pull/10269)) +- Parse URLs lazily in resolver ([#10259](https://github.com/astral-sh/uv/pull/10259)) +- Use `BTreeMap::range` to avoid iterating over unnecessary versions ([#10266](https://github.com/astral-sh/uv/pull/10266)) + +### Bug fixes + +- Accept directories with space names in `uv init` ([#10246](https://github.com/astral-sh/uv/pull/10246)) +- Avoid forking on version in non-universal resolutions ([#10274](https://github.com/astral-sh/uv/pull/10274)) +- Avoid stripping query parameters from URLs ([#10253](https://github.com/astral-sh/uv/pull/10253)) +- Consider workspace dependencies to be 'direct' ([#10197](https://github.com/astral-sh/uv/pull/10197)) +- Detect cyclic dependencies during builds ([#10258](https://github.com/astral-sh/uv/pull/10258)) +- Guard against self-deletion in `uv venv` and `uv tool` ([#10206](https://github.com/astral-sh/uv/pull/10206)) +- Respect static metadata for already-installed distributions ([#10242](https://github.com/astral-sh/uv/pull/10242)) + ## 0.5.13 ### Bug fixes diff --git a/Cargo.lock b/Cargo.lock index 8e45d60e9e9a..a0fc17c8a64a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4410,7 +4410,7 @@ checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "uv" -version = "0.5.13" +version = "0.5.14" dependencies = [ "anstream", "anyhow", @@ -5617,7 +5617,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.5.13" +version = "0.5.14" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 401e0a20c5eb..e12b273d3561 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.5.13" +version = "0.5.14" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 32adeb45bb3e..2ba13f820f4e 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.5.13" +version = "0.5.14" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 3e44291555a6..47a7e0fd3170 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.5.13/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.5.14/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.5.13/install.ps1 | iex" + $ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.5.14/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 55abe014c272..1b6e92be361e 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -21,7 +21,7 @@ $ docker run ghcr.io/astral-sh/uv --help uv provides a distroless Docker image including the `uv` binary. The following tags are published: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.5.13` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.5.14` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.5` (the latest patch version) @@ -62,7 +62,7 @@ In addition, uv publishes the following images: As with the distroless image, each image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.5.13-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.5.14-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -100,13 +100,13 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.5.13 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.5.14 /uv /uvx /bin/ ``` Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.5.13/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.5.14/install.sh /uv-installer.sh ``` ### Installing a project diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 7ed7b11d9526..022b71315941 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -40,7 +40,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.5.13" + version: "0.5.14" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index c41be00df52c..3eb5257e95c6 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -29,7 +29,7 @@ To compile requirements via pre-commit, add the following to the `.pre-commit-co ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.13 + rev: 0.5.14 hooks: # Compile requirements - id: pip-compile @@ -41,7 +41,7 @@ To compile alternative files, modify `args` and `files`: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.13 + rev: 0.5.14 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To run the hook over multiple files at the same time: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.13 + rev: 0.5.14 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 8dadfc5b90af..3b34adc0d525 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.5.13" +version = "0.5.14" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From 5b334313e5b11866ecf7b79265559332cea7b170 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 3 Jan 2025 12:07:29 -0500 Subject: [PATCH 019/135] Visit source distributions before wheels (#10291) ## Summary This should address the comment here: https://github.com/astral-sh/uv/pull/10179#issuecomment-2569189265. We don't compute implied markers if the marker is already `TRUE`, and we set it to `TRUE` as soon as we see a source distribution. So if we visit the source distribution before the wheels, we'll avoid computing these for any irrelevant distributions. --- crates/uv-client/src/registry_client.rs | 10 ++++------ .../src/prioritized_distribution.rs | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index c792a9882a84..da8c3b18c427 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -786,15 +786,13 @@ impl VersionFiles { } pub fn all(self) -> impl Iterator { - self.wheels + self.source_dists .into_iter() - .map(|VersionWheel { name, file }| (DistFilename::WheelFilename(name), file)) + .map(|VersionSourceDist { name, file }| (DistFilename::SourceDistFilename(name), file)) .chain( - self.source_dists + self.wheels .into_iter() - .map(|VersionSourceDist { name, file }| { - (DistFilename::SourceDistFilename(name), file) - }), + .map(|VersionWheel { name, file }| (DistFilename::WheelFilename(name), file)), ) } } diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index 029514cff651..ae02e7735b67 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -638,7 +638,9 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut marker = MarkerTree::FALSE; for platform_tag in &filename.platform_tag { match platform_tag.as_str() { - "any" => marker.or(MarkerTree::TRUE), + "any" => { + return MarkerTree::TRUE; + } tag if tag.starts_with("win") => { marker.or(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, From fbe6f1edf4c987c301d42b2d1cb39c43492d1e20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:18:43 -0500 Subject: [PATCH 020/135] Update Rust crate reqwest to v0.12.12 (#10227) --- Cargo.lock | 36 +++++++++++++++++++++++++++++------- Cargo.toml | 2 +- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0fc17c8a64a..886fb05f3e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1026,7 +1026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1875,7 +1875,7 @@ checksum = "24a46169c7a10358cdccfb179910e8a5a392fc291bdb409da9aeece5b19786d8" dependencies = [ "jiff-tzdb-platform", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2744,7 +2744,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2931,9 +2931,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "async-compression", "base64 0.22.1", @@ -2969,6 +2969,7 @@ dependencies = [ "tokio-rustls", "tokio-socks", "tokio-util", + "tower", "tower-service", "url", "wasm-bindgen", @@ -3180,7 +3181,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3748,7 +3749,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4088,6 +4089,27 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index f1b73bd65c10..706789b1157e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ axoupdater = { version = "0.9.0", default-features = false } backoff = { version = "0.4.0" } base64 = { version = "0.22.1" } bitflags = { version = "2.6.0" } -boxcar = { version = "0.2.8" } +boxcar = { version = "0.2.5" } bytecheck = { version = "0.8.0" } cachedir = { version = "0.3.1" } cargo-util = { version = "0.2.14" } From 833519d5d8eae36d8e563ebf60c3b475a5d99768 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 3 Jan 2025 21:42:17 -0500 Subject: [PATCH 021/135] Extract supported architectures from wheel tags (#10179) ## Summary This PR extends #10046 to also handle architectures, which allows us to correctly include `2.5.1` on the `cu124` index for ARM Linux. Closes https://github.com/astral-sh/uv/issues/9655. --- .../src/prioritized_distribution.rs | 278 +++++++++++++++++- crates/uv-resolver/src/resolver/mod.rs | 25 +- crates/uv/tests/it/lock.rs | 131 ++++++--- crates/uv/tests/it/pip_compile.rs | 83 +++--- 4 files changed, 409 insertions(+), 108 deletions(-) diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index ae02e7735b67..def371bc247c 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -1,6 +1,8 @@ use std::fmt::{Display, Formatter}; -use uv_distribution_filename::{BuildTag, WheelFilename}; +use tracing::debug; + +use uv_distribution_filename::{BuildTag, WheelFilename}; use uv_pep440::VersionSpecifiers; use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString}; use uv_platform_tags::{IncompatibleTag, TagPriority}; @@ -634,6 +636,9 @@ impl IncompatibleWheel { } /// Given a wheel filename, determine the set of supported platforms, in terms of their markers. +/// +/// This is roughly the inverse of platform tag generation: given a tag, we want to infer the +/// supported platforms (rather than generating the supported tags from a given platform). pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut marker = MarkerTree::FALSE; for platform_tag in &filename.platform_tag { @@ -641,32 +646,281 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { "any" => { return MarkerTree::TRUE; } - tag if tag.starts_with("win") => { - marker.or(MarkerTree::expression(MarkerExpression::String { + + // Windows + "win32" => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: "win32".to_string(), + }); + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: "x86".to_string(), })); + marker.or(tag_marker); } - tag if tag.starts_with("macosx") => { - marker.or(MarkerTree::expression(MarkerExpression::String { + "win_amd64" => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "darwin".to_string(), + value: "win32".to_string(), + }); + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: "x86_64".to_string(), + })); + marker.or(tag_marker); + } + "win_arm64" => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::SysPlatform, + operator: MarkerOperator::Equal, + value: "win32".to_string(), + }); + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: "arm64".to_string(), })); + marker.or(tag_marker); + } + + // macOS + tag if tag.starts_with("macosx_") => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::SysPlatform, + operator: MarkerOperator::Equal, + value: "darwin".to_string(), + }); + + // Parse the macOS version from the tag. + // + // For example, given `macosx_10_9_x86_64`, infer `10.9`, followed by `x86_64`. + // + // If at any point we fail to parse, we assume the tag is invalid and skip it. + let mut parts = tag.splitn(4, '_'); + + // Skip the "macosx_" prefix. + if parts.next().is_none_or(|part| part != "macosx") { + debug!("Failed to parse macOS prefix from tag: {tag}"); + continue; + } + + // Skip the major and minor version numbers. + if parts + .next() + .and_then(|part| part.parse::().ok()) + .is_none() + { + debug!("Failed to parse macOS major version from tag: {tag}"); + continue; + }; + if parts + .next() + .and_then(|part| part.parse::().ok()) + .is_none() + { + debug!("Failed to parse macOS minor version from tag: {tag}"); + continue; + }; + + // Extract the architecture from the end of the tag. + let Some(arch) = parts.next() else { + debug!("Failed to parse macOS architecture from tag: {tag}"); + continue; + }; + + // Extract the architecture from the end of the tag. + let mut arch_marker = MarkerTree::FALSE; + let supported_architectures = match arch { + "universal" => { + // Allow any of: "x86_64", "i386", "ppc64", "ppc", "intel" + ["x86_64", "i386", "ppc64", "ppc", "intel"].iter() + } + "universal2" => { + // Allow any of: "x86_64", "arm64" + ["x86_64", "arm64"].iter() + } + "intel" => { + // Allow any of: "x86_64", "i386" + ["x86_64", "i386"].iter() + } + "x86_64" => { + // Allow only "x86_64" + ["x86_64"].iter() + } + "arm64" => { + // Allow only "arm64" + ["arm64"].iter() + } + "ppc64" => { + // Allow only "ppc64" + ["ppc64"].iter() + } + "ppc" => { + // Allow only "ppc" + ["ppc"].iter() + } + "i386" => { + // Allow only "i386" + ["i386"].iter() + } + _ => { + debug!("Unknown macOS architecture in wheel tag: {tag}"); + continue; + } + }; + for arch in supported_architectures { + arch_marker.or(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: (*arch).to_string(), + })); + } + tag_marker.and(arch_marker); + + marker.or(tag_marker); } - tag if tag.starts_with("manylinux") - || tag.starts_with("musllinux") - || tag.starts_with("linux") => - { - marker.or(MarkerTree::expression(MarkerExpression::String { + + // Linux + tag => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: "linux".to_string(), + }); + + // Parse the architecture from the tag. + let arch = if let Some(arch) = tag.strip_prefix("linux_") { + arch + } else if let Some(arch) = tag.strip_prefix("manylinux1_") { + arch + } else if let Some(arch) = tag.strip_prefix("manylinux2010_") { + arch + } else if let Some(arch) = tag.strip_prefix("manylinux2014_") { + arch + } else if let Some(arch) = tag.strip_prefix("musllinux_") { + // Skip over the version tags (e.g., given `musllinux_1_2`, skip over `1` and `2`). + let mut parts = arch.splitn(3, '_'); + if parts + .next() + .and_then(|part| part.parse::().ok()) + .is_none() + { + debug!("Failed to parse musllinux major version from tag: {tag}"); + continue; + }; + if parts + .next() + .and_then(|part| part.parse::().ok()) + .is_none() + { + debug!("Failed to parse musllinux minor version from tag: {tag}"); + continue; + }; + let Some(arch) = parts.next() else { + debug!("Failed to parse musllinux architecture from tag: {tag}"); + continue; + }; + arch + } else if let Some(arch) = tag.strip_prefix("manylinux_") { + // Skip over the version tags (e.g., given `manylinux_2_17`, skip over `2` and `17`). + let mut parts = arch.splitn(3, '_'); + if parts + .next() + .and_then(|part| part.parse::().ok()) + .is_none() + { + debug!("Failed to parse manylinux major version from tag: {tag}"); + continue; + }; + if parts + .next() + .and_then(|part| part.parse::().ok()) + .is_none() + { + debug!("Failed to parse manylinux minor version from tag: {tag}"); + continue; + }; + let Some(arch) = parts.next() else { + debug!("Failed to parse manylinux architecture from tag: {tag}"); + continue; + }; + arch + } else { + continue; + }; + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: arch.to_string(), })); + + marker.or(tag_marker); } - _ => {} } } marker } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[track_caller] + fn assert_markers(filename: &str, expected: &str) { + let filename = WheelFilename::from_str(filename).unwrap(); + assert_eq!( + implied_markers(&filename), + expected.parse::().unwrap() + ); + } + + #[test] + fn test_implied_markers() { + let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap(); + assert_eq!(implied_markers(&filename), MarkerTree::TRUE); + + assert_markers( + "example-1.0-cp310-cp310-win32.whl", + "sys_platform == 'win32' and platform_machine == 'x86'", + ); + assert_markers( + "numpy-2.2.1-cp313-cp313t-win_amd64.whl", + "sys_platform == 'win32' and platform_machine == 'x86_64'", + ); + assert_markers( + "numpy-2.2.1-cp313-cp313t-win_arm64.whl", + "sys_platform == 'win32' and platform_machine == 'arm64'", + ); + assert_markers( + "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "sys_platform == 'linux' and platform_machine == 'aarch64'", + ); + assert_markers( + "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "sys_platform == 'linux' and platform_machine == 'x86_64'", + ); + assert_markers( + "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", + "sys_platform == 'linux' and platform_machine == 'aarch64'", + ); + assert_markers( + "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", + "sys_platform == 'darwin' and platform_machine == 'x86_64'", + ); + assert_markers( + "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", + "sys_platform == 'darwin' and platform_machine == 'x86_64'", + ); + assert_markers( + "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", + "sys_platform == 'darwin' and platform_machine == 'arm64'", + ); + } +} diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index e57bc54e1791..e6d36cbdb0bc 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -31,7 +31,7 @@ use uv_distribution_types::{ use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifiers, MIN_VERSION}; -use uv_pep508::MarkerTree; +use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString}; use uv_platform_tags::Tags; use uv_pypi_types::{ConflictItem, ConflictItemRef, Conflicts, Requirement, VerbatimParsedUrl}; use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider}; @@ -1372,7 +1372,7 @@ impl ResolverState ResolverState Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 31 packages in [TIME] + Resolved 33 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -21460,9 +21460,10 @@ fn lock_pytorch_cpu() -> Result<()> { resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "platform_machine != 'x86_64' or sys_platform != 'linux'", - "sys_platform != 'darwin'", - "sys_platform == 'darwin'", + "(platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", + "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')", + "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'", ] conflicts = [[ { package = "project", extra = "cpu" }, @@ -21801,14 +21802,16 @@ fn lock_pytorch_cpu() -> Result<()> { [package.optional-dependencies] cpu = [ - { name = "torch", version = "2.5.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, - { name = "torch", version = "2.5.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, - { name = "torchvision", version = "0.20.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, - { name = "torchvision", version = "0.20.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, + { name = "torch", version = "2.5.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "torch", version = "2.5.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "torchvision", version = "0.20.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "torchvision", version = "0.20.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] cu124 = [ - { name = "torch", version = "2.5.1+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" } }, - { name = "torchvision", version = "0.20.1+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" } }, + { name = "torch", version = "2.5.1", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "torch", version = "2.5.1+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "torchvision", version = "0.20.1", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "torchvision", version = "0.20.1+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] [package.metadata] @@ -21847,37 +21850,57 @@ fn lock_pytorch_cpu() -> Result<()> { version = "2.5.1" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ - "sys_platform == 'darwin'", + "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'", ] dependencies = [ - { name = "filelock", marker = "sys_platform == 'darwin'" }, - { name = "fsspec", marker = "sys_platform == 'darwin'" }, - { name = "jinja2", marker = "sys_platform == 'darwin'" }, - { name = "networkx", marker = "sys_platform == 'darwin'" }, - { name = "setuptools", marker = "sys_platform == 'darwin'" }, - { name = "sympy", marker = "sys_platform == 'darwin'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, + { name = "filelock", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "fsspec", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "jinja2", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "networkx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "setuptools", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "sympy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, ] wheels = [ { url = "https://download.pytorch.org/whl/cpu/torch-2.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36d1be99281b6f602d9639bd0af3ee0006e7aab16f6718d86f709d395b6f262c" }, { url = "https://download.pytorch.org/whl/cpu/torch-2.5.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8c712df61101964eb11910a846514011f0b6f5920c55dbf567bff8a34163d5b1" }, ] + [[package]] + name = "torch" + version = "2.5.1" + source = { registry = "https://download.pytorch.org/whl/cu124" } + resolution-markers = [ + "platform_machine == 'aarch64' and sys_platform == 'linux'", + ] + dependencies = [ + { name = "filelock", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "fsspec", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "jinja2", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "networkx", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "sympy", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/cu124/torch-2.5.1-cp312-cp312-linux_aarch64.whl", hash = "sha256:302041d457ee169fd925b53da283c13365c6de75c6bb3e84130774b10e2fbb39" }, + ] + [[package]] name = "torch" version = "2.5.1+cpu" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ - "sys_platform != 'darwin'", + "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "filelock", marker = "sys_platform != 'darwin'" }, - { name = "fsspec", marker = "sys_platform != 'darwin'" }, - { name = "jinja2", marker = "sys_platform != 'darwin'" }, - { name = "networkx", marker = "sys_platform != 'darwin'" }, - { name = "setuptools", marker = "sys_platform != 'darwin'" }, - { name = "sympy", marker = "sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, + { name = "filelock", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "fsspec", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "jinja2", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "networkx", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "setuptools", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "sympy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://download.pytorch.org/whl/cpu/torch-2.5.1%2Bcpu-cp312-cp312-linux_x86_64.whl", hash = "sha256:4856f9d6925121d13c2df07aa7580b767f449dfe71ae5acde9c27535d5da4840" }, @@ -21892,13 +21915,13 @@ fn lock_pytorch_cpu() -> Result<()> { resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "platform_machine != 'x86_64' or sys_platform != 'linux'", + "(platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", ] dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, + { name = "filelock", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "fsspec", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "jinja2", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "networkx", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -21911,10 +21934,10 @@ fn lock_pytorch_cpu() -> Result<()> { { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools" }, - { name = "sympy" }, + { name = "setuptools", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "sympy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, { name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://download.pytorch.org/whl/cu124/torch-2.5.1%2Bcu124-cp312-cp312-linux_x86_64.whl", hash = "sha256:bf6484bfe5bc4f92a4a1a1bf553041505e19a911f717065330eb061afe0e14d7" }, @@ -21927,29 +21950,45 @@ fn lock_pytorch_cpu() -> Result<()> { version = "0.20.1" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ - "sys_platform == 'darwin'", + "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", marker = "sys_platform == 'darwin'" }, - { name = "pillow", marker = "sys_platform == 'darwin'" }, - { name = "torch", version = "2.5.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "numpy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "pillow", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "torch", version = "2.5.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, ] wheels = [ { url = "https://download.pytorch.org/whl/cpu/torchvision-0.20.1-cp312-cp312-linux_aarch64.whl", hash = "sha256:9f853ba4497ac4691815ad41b523ee23cf5ba4f87b1ce869d704052e233ca8b7" }, { url = "https://download.pytorch.org/whl/cpu/torchvision-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1a31256ff945d64f006bb306813a7c95a531fe16bfb2535c837dd4c104533d7a" }, ] + [[package]] + name = "torchvision" + version = "0.20.1" + source = { registry = "https://download.pytorch.org/whl/cu124" } + resolution-markers = [ + "platform_machine == 'aarch64' and sys_platform == 'linux'", + ] + dependencies = [ + { name = "numpy", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "pillow", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "torch", version = "2.5.1", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/cu124/torchvision-0.20.1-cp312-cp312-linux_aarch64.whl", hash = "sha256:3e3289e53d0cb5d1b7f55b3f5912f46a08293c6791585ba2fc32c12cded9f9af" }, + ] + [[package]] name = "torchvision" version = "0.20.1+cpu" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ - "sys_platform != 'darwin'", + "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "numpy", marker = "sys_platform != 'darwin'" }, - { name = "pillow", marker = "sys_platform != 'darwin'" }, - { name = "torch", version = "2.5.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, + { name = "numpy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pillow", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "torch", version = "2.5.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://download.pytorch.org/whl/cpu/torchvision-0.20.1%2Bcpu-cp312-cp312-linux_x86_64.whl", hash = "sha256:5f46c7ac7f00a065cb40bfb1e1bfc4ba16a35f5d46b3fe70cca6b3cea7f822f7" }, @@ -21963,12 +22002,12 @@ fn lock_pytorch_cpu() -> Result<()> { resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "platform_machine != 'x86_64' or sys_platform != 'linux'", + "(platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", ] dependencies = [ - { name = "numpy" }, - { name = "pillow" }, - { name = "torch", version = "2.5.1+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" } }, + { name = "numpy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "pillow", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "torch", version = "2.5.1+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://download.pytorch.org/whl/cu124/torchvision-0.20.1%2Bcu124-cp312-cp312-linux_x86_64.whl", hash = "sha256:d1053ec5054549e7dac2613b151bffe323f3c924939d296df4d7d34925aaf3ad" }, diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 1a6de326ad07..f4ae05acd51c 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -7494,9 +7494,9 @@ fn universal_platform_fork() -> Result<()> { # via torch sympy==1.13.1 # via torch - torch==2.5.1 ; sys_platform == 'darwin' + torch==2.5.1 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' # via -r requirements.in - torch==2.5.1+cpu ; sys_platform != 'darwin' + torch==2.5.1+cpu ; (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux') # via -r requirements.in typing-extensions==4.9.0 # via torch @@ -7629,9 +7629,9 @@ fn universal_transitive_disjoint_locals() -> Result<()> { # -r requirements.in # torchvision # triton - torchvision==0.15.1 ; sys_platform == 'darwin' or sys_platform == 'win32' + torchvision==0.15.1 ; platform_machine != 'x86_64' or sys_platform == 'darwin' or sys_platform == 'win32' # via -r requirements.in - torchvision==0.15.1+rocm5.4.2 ; sys_platform != 'darwin' and sys_platform != 'win32' + torchvision==0.15.1+rocm5.4.2 ; platform_machine == 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' # via -r requirements.in triton==2.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' # via torch @@ -7924,11 +7924,11 @@ fn universal_disjoint_base_or_local_requirement() -> Result<()> { # via torch sympy==1.12 # via torch - torch==2.0.0 ; python_full_version < '3.11' and sys_platform == 'darwin' + torch==2.0.0 ; (python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'darwin') # via # -r requirements.in # example - torch==2.0.0+cpu ; python_full_version >= '3.13' or (python_full_version < '3.11' and sys_platform != 'darwin') + torch==2.0.0+cpu ; python_full_version >= '3.13' or (python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux') # via # -r requirements.in # example @@ -7963,15 +7963,15 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { name = "example" version = "0.0.0" dependencies = [ - "torch==2.0.0+cu118 ; platform_machine == 'x86_64' and os_name == 'Linux'" + "torch==2.0.0+cu118 ; implementation_name == 'cpython' and os_name == 'Linux'" ] requires-python = ">=3.11" "#})?; let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(indoc! {" - torch==2.0.0 ; platform_machine == 'x86_64' - torch==2.3.0 ; platform_machine != 'x86_64' + torch==2.0.0 ; implementation_name == 'cpython' + torch==2.3.0 ; implementation_name != 'cpython' . "})?; @@ -7985,7 +7985,7 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal - cmake==3.28.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' + cmake==3.28.4 ; implementation_name == 'cpython' and platform_machine == 'x86_64' and sys_platform == 'linux' # via triton . # via -r requirements.in @@ -7994,38 +7994,38 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { # pytorch-triton-rocm # torch # triton - fsspec==2024.3.1 ; platform_machine != 'x86_64' + fsspec==2024.3.1 ; implementation_name != 'cpython' # via torch - intel-openmp==2021.4.0 ; platform_machine != 'x86_64' and sys_platform == 'win32' + intel-openmp==2021.4.0 ; implementation_name != 'cpython' and sys_platform == 'win32' # via mkl jinja2==3.1.3 # via torch - lit==18.1.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' + lit==18.1.2 ; implementation_name == 'cpython' and platform_machine == 'x86_64' and sys_platform == 'linux' # via triton markupsafe==2.1.5 # via jinja2 - mkl==2021.4.0 ; platform_machine != 'x86_64' and sys_platform == 'win32' + mkl==2021.4.0 ; implementation_name != 'cpython' and sys_platform == 'win32' # via torch mpmath==1.3.0 # via sympy networkx==3.2.1 # via torch - pytorch-triton-rocm==2.3.0 ; platform_machine != 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' + pytorch-triton-rocm==2.3.0 ; (implementation_name != 'cpython' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') # via torch sympy==1.12 # via torch - tbb==2021.11.0 ; platform_machine != 'x86_64' and sys_platform == 'win32' + tbb==2021.11.0 ; implementation_name != 'cpython' and sys_platform == 'win32' # via mkl - torch==2.0.0+cu118 ; platform_machine == 'x86_64' + torch==2.0.0+cu118 ; implementation_name == 'cpython' # via # -r requirements.in # example # triton - torch==2.3.0 ; (platform_machine != 'x86_64' and sys_platform == 'darwin') or (platform_machine != 'x86_64' and sys_platform == 'win32') + torch==2.3.0 ; (implementation_name != 'cpython' and platform_machine == 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform == 'darwin') or (implementation_name != 'cpython' and sys_platform == 'win32') # via -r requirements.in - torch==2.3.0+rocm6.0 ; platform_machine != 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' + torch==2.3.0+rocm6.0 ; (implementation_name != 'cpython' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') # via -r requirements.in - triton==2.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' + triton==2.0.0 ; implementation_name == 'cpython' and platform_machine == 'x86_64' and sys_platform == 'linux' # via torch typing-extensions==4.10.0 # via torch @@ -8070,7 +8070,6 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { # via -r requirements.in filelock==3.13.1 # via - # pytorch-triton-rocm # torch # triton fsspec==2024.3.1 ; platform_machine != 'x86_64' @@ -8089,8 +8088,6 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { # via sympy networkx==3.2.1 # via torch - pytorch-triton-rocm==2.3.0 ; platform_machine != 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' - # via torch sympy==1.12 # via torch tbb==2021.11.0 ; platform_machine != 'x86_64' and sys_platform == 'win32' @@ -8100,9 +8097,7 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { # -r requirements.in # example # triton - torch==2.3.0 ; (platform_machine != 'x86_64' and sys_platform == 'darwin') or (platform_machine != 'x86_64' and sys_platform == 'win32') - # via -r requirements.in - torch==2.3.0+rocm6.0 ; platform_machine != 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' + torch==2.3.0 ; platform_machine != 'x86_64' # via -r requirements.in triton==2.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' # via torch @@ -8110,7 +8105,7 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { # via torch ----- stderr ----- - Resolved 19 packages in [TIME] + Resolved 17 packages in [TIME] "### ); @@ -8129,8 +8124,8 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> { name = "example" version = "0.0.0" dependencies = [ - "torch==2.0.0+cu118 ; platform_machine == 'x86_64'", - "torch==2.0.0+cpu ; platform_machine != 'x86_64'" + "torch==2.0.0+cu118 ; implementation_name == 'cpython'", + "torch==2.0.0+cpu ; implementation_name != 'cpython'" ] requires-python = ">=3.11" "#})?; @@ -8154,7 +8149,7 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal - cmake==3.28.4 ; os_name == 'Linux' and platform_machine == 'x86_64' and sys_platform == 'linux' + cmake==3.28.4 ; implementation_name == 'cpython' and os_name == 'Linux' and platform_machine == 'x86_64' and sys_platform == 'linux' # via triton . ; os_name == 'Linux' # via -r requirements.in @@ -8169,7 +8164,7 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> { # via mkl jinja2==3.1.3 # via torch - lit==18.1.2 ; os_name == 'Linux' and platform_machine == 'x86_64' and sys_platform == 'linux' + lit==18.1.2 ; implementation_name == 'cpython' and os_name == 'Linux' and platform_machine == 'x86_64' and sys_platform == 'linux' # via triton markupsafe==2.1.5 # via jinja2 @@ -8179,26 +8174,26 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> { # via sympy networkx==3.2.1 # via torch - pytorch-triton-rocm==2.3.0 ; os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'win32' + pytorch-triton-rocm==2.3.0 ; (os_name != 'Linux' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') # via torch sympy==1.12 # via torch tbb==2021.11.0 ; os_name != 'Linux' and sys_platform == 'win32' # via mkl - torch==2.0.0+cpu ; os_name == 'Linux' and platform_machine != 'x86_64' + torch==2.0.0+cpu ; implementation_name != 'cpython' and os_name == 'Linux' # via # -r requirements.in # example - torch==2.0.0+cu118 ; os_name == 'Linux' and platform_machine == 'x86_64' + torch==2.0.0+cu118 ; implementation_name == 'cpython' and os_name == 'Linux' # via # -r requirements.in # example # triton - torch==2.3.0 ; (os_name != 'Linux' and sys_platform == 'darwin') or (os_name != 'Linux' and sys_platform == 'win32') + torch==2.3.0 ; (os_name != 'Linux' and platform_machine == 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform == 'darwin') or (os_name != 'Linux' and sys_platform == 'win32') # via -r requirements.in - torch==2.3.0+rocm6.0 ; os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'win32' + torch==2.3.0+rocm6.0 ; (os_name != 'Linux' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') # via -r requirements.in - triton==2.0.0 ; os_name == 'Linux' and platform_machine == 'x86_64' and sys_platform == 'linux' + triton==2.0.0 ; implementation_name == 'cpython' and os_name == 'Linux' and platform_machine == 'x86_64' and sys_platform == 'linux' # via torch typing-extensions==4.10.0 # via torch @@ -8993,8 +8988,6 @@ fn universal_marker_propagation() -> Result<()> { # via torchvision pytorch-triton-rocm==2.0.2 ; platform_machine == 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' # via torch - pytorch-triton-rocm==2.2.0 ; platform_machine != 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' - # via torch requests==2.31.0 # via torchvision sympy==1.12 @@ -9008,11 +9001,7 @@ fn universal_marker_propagation() -> Result<()> { # -r requirements.in # pytorch-triton-rocm # torchvision - torch==2.2.0 ; (platform_machine != 'x86_64' and sys_platform == 'darwin') or (platform_machine != 'x86_64' and sys_platform == 'win32') - # via - # -r requirements.in - # torchvision - torch==2.2.0+rocm5.7 ; platform_machine != 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' + torch==2.2.0 ; platform_machine != 'x86_64' # via # -r requirements.in # torchvision @@ -9020,9 +9009,7 @@ fn universal_marker_propagation() -> Result<()> { # via -r requirements.in torchvision==0.15.1+rocm5.4.2 ; platform_machine == 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' # via -r requirements.in - torchvision==0.17.0 ; (platform_machine != 'x86_64' and sys_platform == 'darwin') or (platform_machine != 'x86_64' and sys_platform == 'win32') - # via -r requirements.in - torchvision==0.17.0+rocm5.7 ; platform_machine != 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' + torchvision==0.17.0 ; platform_machine != 'x86_64' # via -r requirements.in typing-extensions==4.10.0 # via torch @@ -9031,7 +9018,7 @@ fn universal_marker_propagation() -> Result<()> { ----- stderr ----- warning: The requested Python version 3.8 is not available; 3.12.[X] will be used to build dependencies instead. - Resolved 28 packages in [TIME] + Resolved 25 packages in [TIME] "### ); From a056cb292a501ade27ed194a3cc5bbcb2021126d Mon Sep 17 00:00:00 2001 From: Perchun Pak Date: Sat, 4 Jan 2025 19:31:36 +0100 Subject: [PATCH 022/135] Update copyright year (#10297) ## Summary Just found this accidentally --- LICENSE-APACHE | 2 +- LICENSE-MIT | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE-APACHE b/LICENSE-APACHE index f49a4e16e68b..261eeb9e9f8b 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index ec2236bb8994..014835144877 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Astral Software Inc. +Copyright (c) 2025 Astral Software Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From bbf9558b1620dc6edf3597f1bdc970bff4216ce4 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sun, 5 Jan 2025 14:29:37 -0600 Subject: [PATCH 023/135] Rename CI runners and use Windows 2025 preview on large runners (#10298) I'm renaming our runners to be more explicit about their size, architecture, and version. Switching to Windows 2025 over 2022 in some of our jobs in the hope that it's faster. --- .github/workflows/build-binaries.yml | 2 +- .github/workflows/ci.yml | 40 +++++++++++----------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 2560c5dde29f..054c5e3cfacd 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -156,7 +156,7 @@ jobs: windows: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: windows-latest-large + runs-on: github-windows-2022-x86_64-8 strategy: matrix: platform: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f2248890df4..fce3bceaa495 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,8 +116,7 @@ jobs: timeout-minutes: 15 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: "windows-latest-xlarge" + runs-on: github-windows-2025-x86_64-16 name: "cargo clippy | windows" steps: - uses: actions/checkout@v4 @@ -173,8 +172,7 @@ jobs: timeout-minutes: 10 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: "depot-ubuntu-22.04-16" + runs-on: depot-ubuntu-22.04-16 name: "cargo test | ubuntu" steps: - uses: actions/checkout@v4 @@ -219,8 +217,7 @@ jobs: timeout-minutes: 10 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: "macos-latest-xlarge" + runs-on: macos-latest-xlarge # github-macos-14-aarch64-6 name: "cargo test | macos" steps: - uses: actions/checkout@v4 @@ -258,8 +255,7 @@ jobs: timeout-minutes: 15 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: "windows-latest-xlarge" + runs-on: github-windows-2025-x86_64-16 name: "cargo test | windows" steps: - uses: actions/checkout@v4 @@ -334,7 +330,7 @@ jobs: timeout-minutes: 15 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: windows-latest-xlarge + runs-on: github-windows-2025-x86_64-16 name: "check windows trampoline | ${{ matrix.target-arch }}" strategy: fail-fast: false @@ -456,8 +452,7 @@ jobs: timeout-minutes: 10 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: ubuntu-latest-large + runs-on: github-ubuntu-24.04-x86_64-8 name: "build binary | linux" steps: - uses: actions/checkout@v4 @@ -484,8 +479,7 @@ jobs: timeout-minutes: 10 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: macos-14 + runs-on: macos-14 # github-macos-14-aarch64-3 name: "build binary | macos aarch64" steps: - uses: actions/checkout@v4 @@ -507,8 +501,7 @@ jobs: timeout-minutes: 10 needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: macos-latest-large # Intel runner on GitHub + runs-on: macos-latest-large # github-macos-14-x86_64-12 name: "build binary | macos x86_64" steps: - uses: actions/checkout@v4 @@ -530,8 +523,7 @@ jobs: needs: determine_changes timeout-minutes: 10 if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: windows-latest-large + runs-on: github-windows-2025-x86_64-8 name: "build binary | windows" steps: - uses: actions/checkout@v4 @@ -563,8 +555,7 @@ jobs: name: "cargo build (msrv)" needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: ubuntu-latest-large + runs-on: github-ubuntu-24.04-x86_64-8 timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -585,8 +576,7 @@ jobs: needs: determine_changes timeout-minutes: 10 if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} - runs-on: - labels: ubuntu-latest + runs-on: ubuntu-latest name: "build binary | freebsd" steps: @@ -1220,7 +1210,7 @@ jobs: timeout-minutes: 10 needs: build-binary-macos-aarch64 name: "check cache | macos aarch64" - runs-on: macos-14 + runs-on: macos-14 # github-macos-14-aarch64-3 steps: - uses: actions/checkout@v4 @@ -1468,7 +1458,7 @@ jobs: timeout-minutes: 10 needs: build-binary-macos-aarch64 name: "check system | python on macos aarch64" - runs-on: macos-14 + runs-on: macos-14 # github-macos-14-aarch64-3 steps: - uses: actions/checkout@v4 @@ -1492,7 +1482,7 @@ jobs: timeout-minutes: 10 needs: build-binary-macos-aarch64 name: "check system | homebrew python on macos aarch64" - runs-on: macos-14 + runs-on: macos-14 # github-macos-14-aarch64-3 steps: - uses: actions/checkout@v4 @@ -1517,7 +1507,7 @@ jobs: timeout-minutes: 10 needs: build-binary-macos-x86_64 name: "check system | python on macos x86_64" - runs-on: macos-13 + runs-on: macos-13 # github-macos-13-x86_64-4 steps: - uses: actions/checkout@v4 From 7182a34aa414f788d1a76a15690f2cd14e398e77 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 5 Jan 2025 19:58:07 -0500 Subject: [PATCH 024/135] Avoid generating unused hashes during `uv lock` (#10307) ## Summary We don't even use these! See the comment inline. Closes https://github.com/astral-sh/uv/issues/9651. --- crates/uv-distribution-types/src/hash.rs | 37 ++++++++++++++----- .../src/distribution_database.rs | 28 ++++++++------ crates/uv-requirements/src/source_tree.rs | 6 +-- crates/uv-types/src/hash.rs | 15 ++++---- crates/uv/src/commands/pip/compile.rs | 6 +-- crates/uv/src/commands/project/lock.rs | 4 +- crates/uv/tests/it/lock.rs | 6 +-- crates/uv/tests/it/sync.rs | 1 + 8 files changed, 64 insertions(+), 39 deletions(-) diff --git a/crates/uv-distribution-types/src/hash.rs b/crates/uv-distribution-types/src/hash.rs index ff668b6a1e7a..e3bb4aa8445f 100644 --- a/crates/uv-distribution-types/src/hash.rs +++ b/crates/uv-distribution-types/src/hash.rs @@ -5,7 +5,7 @@ pub enum HashPolicy<'a> { /// No hash policy is specified. None, /// Hashes should be generated (specifically, a SHA-256 hash), but not validated. - Generate, + Generate(HashGeneration), /// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should /// be generated so as to ensure that the archive is valid. Validate(&'a [HashDigest]), @@ -17,21 +17,28 @@ impl HashPolicy<'_> { matches!(self, Self::None) } - /// Returns `true` if the hash policy is `Generate`. - pub fn is_generate(&self) -> bool { - matches!(self, Self::Generate) - } - /// Returns `true` if the hash policy is `Validate`. pub fn is_validate(&self) -> bool { matches!(self, Self::Validate(_)) } + /// Returns `true` if the hash policy indicates that hashes should be generated. + pub fn is_generate(&self, dist: &crate::BuiltDist) -> bool { + match self { + HashPolicy::Generate(HashGeneration::Url) => dist.file().is_none(), + HashPolicy::Generate(HashGeneration::All) => { + dist.file().map_or(true, |file| file.hashes.is_empty()) + } + HashPolicy::Validate(_) => false, + HashPolicy::None => false, + } + } + /// Return the algorithms used in the hash policy. pub fn algorithms(&self) -> Vec { match self { Self::None => vec![], - Self::Generate => vec![HashAlgorithm::Sha256], + Self::Generate(_) => vec![HashAlgorithm::Sha256], Self::Validate(hashes) => { let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::>(); algorithms.sort(); @@ -45,12 +52,22 @@ impl HashPolicy<'_> { pub fn digests(&self) -> &[HashDigest] { match self { Self::None => &[], - Self::Generate => &[], + Self::Generate(_) => &[], Self::Validate(hashes) => hashes, } } } +/// The context in which hashes should be generated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HashGeneration { + /// Generate hashes for direct URL distributions. + Url, + /// Generate hashes for direct URL distributions, along with any distributions that are hosted + /// on a registry that does _not_ provide hashes. + All, +} + pub trait Hashed { /// Return the [`HashDigest`]s for the archive. fn hashes(&self) -> &[HashDigest]; @@ -59,7 +76,7 @@ pub trait Hashed { fn satisfies(&self, hashes: HashPolicy) -> bool { match hashes { HashPolicy::None => true, - HashPolicy::Generate => self + HashPolicy::Generate(_) => self .hashes() .iter() .any(|hash| hash.algorithm == HashAlgorithm::Sha256), @@ -71,7 +88,7 @@ pub trait Hashed { fn has_digests(&self, hashes: HashPolicy) -> bool { match hashes { HashPolicy::None => true, - HashPolicy::Generate => self + HashPolicy::Generate(_) => self .hashes() .iter() .any(|hash| hash.algorithm == HashAlgorithm::Sha256), diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 3b0ab11a4cc2..06c21d007974 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -405,22 +405,28 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); } - // If hash generation is enabled, and the distribution isn't hosted on an index, get the + // If hash generation is enabled, and the distribution isn't hosted on a registry, get the // entire wheel to ensure that the hashes are included in the response. If the distribution // is hosted on an index, the hashes will be included in the simple metadata response. // For hash _validation_, callers are expected to enforce the policy when retrieving the // wheel. + // + // Historically, for `uv pip compile --universal`, we also generate hashes for + // registry-based distributions when the relevant registry doesn't provide them. This was + // motivated by `--find-links`. We continue that behavior (under `HashGeneration::All`) for + // backwards compatibility, but it's a little dubious, since we're only hashing _one_ + // distribution here (as opposed to hashing all distributions for the version), and it may + // not even be a compatible distribution! + // // TODO(charlie): Request the hashes via a separate method, to reduce the coupling in this API. - if hashes.is_generate() { - if dist.file().map_or(true, |file| file.hashes.is_empty()) { - let wheel = self.get_wheel(dist, hashes).await?; - let metadata = wheel.metadata()?; - let hashes = wheel.hashes; - return Ok(ArchiveMetadata { - metadata: Metadata::from_metadata23(metadata), - hashes, - }); - } + if hashes.is_generate(dist) { + let wheel = self.get_wheel(dist, hashes).await?; + let metadata = wheel.metadata()?; + let hashes = wheel.hashes; + return Ok(ArchiveMetadata { + metadata: Metadata::from_metadata23(metadata), + hashes, + }); } let result = self diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index 569c658b6200..5c55c9dff392 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -13,7 +13,7 @@ use url::Url; use uv_configuration::ExtrasSpecification; use uv_distribution::{DistributionDatabase, Reporter, RequiresDist}; use uv_distribution_types::{ - BuildableSource, DirectorySourceUrl, HashPolicy, SourceUrl, VersionId, + BuildableSource, DirectorySourceUrl, HashGeneration, HashPolicy, SourceUrl, VersionId, }; use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; @@ -213,8 +213,8 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { // manual match. let hashes = match self.hasher { HashStrategy::None => HashPolicy::None, - HashStrategy::Generate => HashPolicy::Generate, - HashStrategy::Verify(_) => HashPolicy::Generate, + HashStrategy::Generate(mode) => HashPolicy::Generate(*mode), + HashStrategy::Verify(_) => HashPolicy::Generate(HashGeneration::All), HashStrategy::Require(_) => { return Err(anyhow::anyhow!( "Hash-checking is not supported for local directories: {}", diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index 2ac08effa3b5..e1420cdddcc2 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -5,7 +5,8 @@ use url::Url; use uv_configuration::HashCheckingMode; use uv_distribution_types::{ - DistributionMetadata, HashPolicy, Name, Resolution, UnresolvedRequirement, VersionId, + DistributionMetadata, HashGeneration, HashPolicy, Name, Resolution, UnresolvedRequirement, + VersionId, }; use uv_normalize::PackageName; use uv_pep440::Version; @@ -19,7 +20,7 @@ pub enum HashStrategy { #[default] None, /// Hashes should be generated (specifically, a SHA-256 hash), but not validated. - Generate, + Generate(HashGeneration), /// Hashes should be validated, if present, but ignored if absent. /// /// If necessary, hashes should be generated to ensure that the archive is valid. @@ -35,7 +36,7 @@ impl HashStrategy { pub fn get(&self, distribution: &T) -> HashPolicy { match self { Self::None => HashPolicy::None, - Self::Generate => HashPolicy::Generate, + Self::Generate(mode) => HashPolicy::Generate(*mode), Self::Verify(hashes) => { if let Some(hashes) = hashes.get(&distribution.version_id()) { HashPolicy::Validate(hashes.as_slice()) @@ -56,7 +57,7 @@ impl HashStrategy { pub fn get_package(&self, name: &PackageName, version: &Version) -> HashPolicy { match self { Self::None => HashPolicy::None, - Self::Generate => HashPolicy::Generate, + Self::Generate(mode) => HashPolicy::Generate(*mode), Self::Verify(hashes) => { if let Some(hashes) = hashes.get(&VersionId::from_registry(name.clone(), version.clone())) @@ -79,7 +80,7 @@ impl HashStrategy { pub fn get_url(&self, url: &Url) -> HashPolicy { match self { Self::None => HashPolicy::None, - Self::Generate => HashPolicy::Generate, + Self::Generate(mode) => HashPolicy::Generate(*mode), Self::Verify(hashes) => { if let Some(hashes) = hashes.get(&VersionId::from_url(url)) { HashPolicy::Validate(hashes.as_slice()) @@ -100,7 +101,7 @@ impl HashStrategy { pub fn allows_package(&self, name: &PackageName, version: &Version) -> bool { match self { Self::None => true, - Self::Generate => true, + Self::Generate(_) => true, Self::Verify(_) => true, Self::Require(hashes) => { hashes.contains_key(&VersionId::from_registry(name.clone(), version.clone())) @@ -112,7 +113,7 @@ impl HashStrategy { pub fn allows_url(&self, url: &Url) -> bool { match self { Self::None => true, - Self::Generate => true, + Self::Generate(_) => true, Self::Verify(_) => true, Self::Require(hashes) => hashes.contains_key(&VersionId::from_url(url)), } diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index f9d9eefeec9b..e2398c44d20d 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -17,8 +17,8 @@ use uv_configuration::{ use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_types::{ - DependencyMetadata, Index, IndexLocations, NameRequirementSpecification, Origin, - UnresolvedRequirementSpecification, Verbatim, + DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification, + Origin, UnresolvedRequirementSpecification, Verbatim, }; use uv_fs::Simplified; use uv_install_wheel::linker::LinkMode; @@ -266,7 +266,7 @@ pub(crate) async fn pip_compile( // Generate, but don't enforce hashes for the requirements. let hasher = if generate_hashes { - HashStrategy::Generate + HashStrategy::Generate(HashGeneration::All) } else { HashStrategy::None }; diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index b77ab955f0f9..ec1b4c866a70 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -17,7 +17,7 @@ use uv_configuration::{ use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::DistributionDatabase; use uv_distribution_types::{ - DependencyMetadata, Index, IndexLocations, NameRequirementSpecification, + DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification, UnresolvedRequirementSpecification, }; use uv_git::ResolvedRepositoryReference; @@ -472,7 +472,7 @@ async fn do_lock( .index_strategy(index_strategy) .build_options(build_options.clone()) .build(); - let hasher = HashStrategy::Generate; + let hasher = HashStrategy::Generate(HashGeneration::Url); // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 0a586e99fdf0..f783a696581d 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -9307,7 +9307,7 @@ fn lock_find_links_local_wheel() -> Result<()> { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv - Prepared 1 package in [TIME] + Prepared 2 packages in [TIME] Installed 2 packages in [TIME] + project==0.1.0 (from file://[TEMP_DIR]/workspace) + tqdm==1000.0.0 @@ -9518,7 +9518,7 @@ fn lock_find_links_http_wheel() -> Result<()> { ----- stdout ----- ----- stderr ----- - Prepared 1 package in [TIME] + Prepared 2 packages in [TIME] Installed 2 packages in [TIME] + packaging==23.2 + project==0.1.0 (from file://[TEMP_DIR]/) @@ -9744,7 +9744,7 @@ fn lock_local_index() -> Result<()> { ----- stdout ----- ----- stderr ----- - Prepared 1 package in [TIME] + Prepared 2 packages in [TIME] Installed 2 packages in [TIME] + project==0.1.0 (from file://[TEMP_DIR]/) + tqdm==1000.0.0 diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 9c362d78b096..e4a663362d9b 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -5870,6 +5870,7 @@ fn sync_build_tag() -> Result<()> { ----- stdout ----- ----- stderr ----- + Prepared 1 package in [TIME] Installed 1 package in [TIME] + build-tag==1.0.0 "###); From f4cca71c0b7e12c46914759cee1c8fc27e847951 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 01:11:44 +0000 Subject: [PATCH 025/135] Update Rust crate async-trait to v0.1.84 (#10309) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 886fb05f3e53..331d66472b07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" dependencies = [ "proc-macro2", "quote", From af23311d1517123be711e885484b969519144d3a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 01:12:57 +0000 Subject: [PATCH 026/135] Update Rust crate jiff to v0.1.21 (#10310) --- Cargo.lock | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 331d66472b07..4efc9546af3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1026,7 +1026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1869,13 +1869,16 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.16" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a46169c7a10358cdccfb179910e8a5a392fc291bdb409da9aeece5b19786d8" +checksum = "ed0ce60560149333a8e41ca7dc78799c47c5fd435e2bc18faf6a054382eec037" dependencies = [ "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2570,6 +2573,15 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2744,7 +2756,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3181,7 +3193,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3749,7 +3761,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] From f93ada48e9a2413e134ac52ffe8508cd17c4ecae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 01:16:15 +0000 Subject: [PATCH 027/135] Update Rust crate spdx to v0.10.8 (#10311) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4efc9546af3e..9b9fdc1d18eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3581,9 +3581,9 @@ dependencies = [ [[package]] name = "spdx" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae30cc7bfe3656d60ee99bf6836f472b0c53dddcbf335e253329abb16e535a2" +checksum = "58b69356da67e2fc1f542c71ea7e654a361a79c938e4424392ecf4fa065d2193" dependencies = [ "smallvec", ] From 0b9519b1fc3867d1b1798ce17e694aa7dec91bac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 01:19:11 +0000 Subject: [PATCH 028/135] Update Rust crate syn to v2.0.95 (#10312) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b9fdc1d18eb..8af23a32f969 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3680,9 +3680,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.93" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", From 88b63b16d561aa9d1e8ef9efeaa00950b26643da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 01:27:52 +0000 Subject: [PATCH 029/135] Update pre-commit dependencies (#10313) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e649da9743b3..0c4cff397a42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: validate-pyproject - repo: https://github.com/crate-ci/typos - rev: v1.28.4 + rev: v1.29.4 hooks: - id: typos @@ -42,7 +42,7 @@ repos: types_or: [yaml, json5] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.8.6 hooks: - id: ruff-format - id: ruff From f7fc85f4e95369b6919293d1df9b7b6531cde55b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 5 Jan 2025 21:18:16 -0500 Subject: [PATCH 030/135] Respect `FORCE_COLOR` environment variable (#10315) ## Summary Closes https://github.com/astral-sh/uv/issues/10303. --- crates/uv-cli/src/lib.rs | 9 +- crates/uv/tests/it/help.rs | 22 ++--- docs/reference/cli.md | 195 ++++++++++++++++++++++--------------- 3 files changed, 133 insertions(+), 93 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index bddc6a5f7a37..ade2d43ac685 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -171,12 +171,13 @@ pub struct GlobalArgs { #[arg(global = true, long, hide = true, conflicts_with = "color")] pub no_color: bool, - /// Control colors in output. + /// Control the use of color in output. + /// + /// By default, uv will automatically detect support for colors when writing to a terminal. #[arg( global = true, long, value_enum, - default_value = "auto", conflicts_with = "no_color", value_name = "COLOR_CHOICE" )] @@ -4486,8 +4487,8 @@ pub struct GenerateShellCompletionArgs { pub quiet: bool, #[arg(long, short, action = clap::ArgAction::Count, conflicts_with = "quiet", hide = true)] pub verbose: u8, - #[arg(long, default_value = "auto", conflicts_with = "no_color", hide = true)] - pub color: ColorChoice, + #[arg(long, conflicts_with = "no_color", hide = true)] + pub color: Option, #[arg(long, hide = true)] pub native_tls: bool, #[arg(long, hide = true)] diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 31e3e02d40cc..93b6ece0251f 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -52,7 +52,7 @@ fn help() { -v, --verbose... Use verbose output --color - Control colors in output [default: auto] [possible values: auto, always, never] + Control the use of color in output [possible values: auto, always, never] --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] @@ -132,7 +132,7 @@ fn help_flag() { -v, --verbose... Use verbose output --color - Control colors in output [default: auto] [possible values: auto, always, never] + Control the use of color in output [possible values: auto, always, never] --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] @@ -211,7 +211,7 @@ fn help_short_flag() { -v, --verbose... Use verbose output --color - Control colors in output [default: auto] [possible values: auto, always, never] + Control the use of color in output [possible values: auto, always, never] --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] @@ -347,9 +347,9 @@ fn help_subcommand() { () --color - Control colors in output + Control the use of color in output. - [default: auto] + By default, uv will automatically detect support for colors when writing to a terminal. Possible values: - auto: Enables colored output only when the output is going to a terminal or TTY with @@ -591,9 +591,9 @@ fn help_subsubcommand() { () --color - Control colors in output + Control the use of color in output. - [default: auto] + By default, uv will automatically detect support for colors when writing to a terminal. Possible values: - auto: Enables colored output only when the output is going to a terminal or TTY with @@ -728,7 +728,7 @@ fn help_flag_subcommand() { -v, --verbose... Use verbose output --color - Control colors in output [default: auto] [possible values: auto, always, never] + Control the use of color in output [possible values: auto, always, never] --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] @@ -801,7 +801,7 @@ fn help_flag_subsubcommand() { -v, --verbose... Use verbose output --color - Control colors in output [default: auto] [possible values: auto, always, never] + Control the use of color in output [possible values: auto, always, never] --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] @@ -958,7 +958,7 @@ fn help_with_global_option() { -v, --verbose... Use verbose output --color - Control colors in output [default: auto] [possible values: auto, always, never] + Control the use of color in output [possible values: auto, always, never] --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] @@ -1074,7 +1074,7 @@ fn help_with_no_pager() { -v, --verbose... Use verbose output --color - Control colors in output [default: auto] [possible values: auto, always, never] + Control the use of color in output [possible values: auto, always, never] --native-tls Whether to load TLS certificates from the platform's native certificate store [env: UV_NATIVE_TLS=] diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 602c77c9224f..ae723000047c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -104,9 +104,10 @@ uv run [OPTIONS] [COMMAND]

To view the location of the cache directory, run uv cache dir.

May also be set with the UV_CACHE_DIR environment variable.

-
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -572,9 +573,10 @@ uv init [OPTIONS] [PATH]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -766,9 +768,10 @@ uv add [OPTIONS] >

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -1132,9 +1135,10 @@ uv remove [OPTIONS] ...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -1484,9 +1488,10 @@ uv sync [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -1873,9 +1878,10 @@ uv lock [OPTIONS]

    Equivalent to --frozen.

    May also be set with the UV_FROZEN environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -2189,9 +2195,10 @@ uv export [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -2565,9 +2572,10 @@ uv tree [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -3033,9 +3041,10 @@ uv tool run [OPTIONS] [COMMAND]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -3354,9 +3363,10 @@ uv tool install [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -3687,9 +3697,10 @@ uv tool upgrade [OPTIONS] ...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -3976,9 +3987,10 @@ uv tool list [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -4086,9 +4098,10 @@ uv tool uninstall [OPTIONS] ...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -4208,9 +4221,10 @@ uv tool update-shell [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -4348,9 +4362,10 @@ uv tool dir [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -4548,9 +4563,10 @@ uv python list [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -4696,9 +4712,10 @@ uv python install [OPTIONS] [TARGETS]...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -4863,9 +4880,10 @@ uv python find [OPTIONS] [REQUEST]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -5008,9 +5026,10 @@ uv python pin [OPTIONS] [REQUEST]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -5156,9 +5175,10 @@ uv python dir [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -5282,9 +5302,10 @@ uv python uninstall [OPTIONS] ...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -5465,9 +5486,10 @@ uv pip compile [OPTIONS] ...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -5934,9 +5956,10 @@ uv pip sync [OPTIONS] ...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -6331,9 +6354,10 @@ uv pip install [OPTIONS] |--editable To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -6801,9 +6825,10 @@ uv pip uninstall [OPTIONS] >

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -6953,9 +6978,10 @@ uv pip freeze [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -7087,9 +7113,10 @@ uv pip list [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -7317,9 +7344,10 @@ uv pip show [OPTIONS] [PACKAGE]...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -7451,9 +7479,10 @@ uv pip tree [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -7668,9 +7697,10 @@ uv pip check [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -7822,9 +7852,10 @@ uv venv [OPTIONS] [PATH]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -8086,9 +8117,10 @@ uv build [OPTIONS] [SRC]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -8433,9 +8465,10 @@ uv publish [OPTIONS] [FILES]...

    The index must provide one of the supported hashes (SHA-256, SHA-384, or SHA-512).

    May also be set with the UV_PUBLISH_CHECK_URL environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -8629,9 +8662,10 @@ uv cache clean [OPTIONS] [PACKAGE]...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -8751,9 +8785,10 @@ uv cache prune [OPTIONS]

    In --ci mode, uv will prune any pre-built wheels from the cache, but retain any wheels that were built from source.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -8875,9 +8910,10 @@ uv cache dir [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -9013,9 +9049,10 @@ uv self update [OPTIONS] [TARGET_VERSION]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -9132,9 +9169,10 @@ uv version [OPTIONS]

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

    @@ -9297,9 +9335,10 @@ uv help [OPTIONS] [COMMAND]...

    To view the location of the cache directory, run uv cache dir.

    May also be set with the UV_CACHE_DIR environment variable.

    -
--color color-choice

Control colors in output

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

-

[default: auto]

Possible values:

+
--script script

Lock the specified Python script, rather than the current project.

+ +

If provided, uv will lock the script (based on its inline metadata table, in adherence with PEP 723) to a .lock file adjacent to the script itself.

+
--upgrade, -U

Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

--upgrade-package, -P upgrade-package

Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package

From ad09070dd7ae9cfa7c609a42f07af1df8e6d8b18 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:39:26 +0000 Subject: [PATCH 070/135] Update Rust crate petgraph to 0.7.1 (#10317) --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2bd7f5ae5032..3a8e8184e2df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1099,9 +1099,9 @@ dependencies = [ [[package]] name = "fixedbitset" -version = "0.4.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" @@ -2500,9 +2500,9 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", "indexmap", diff --git a/Cargo.toml b/Cargo.toml index a4df23b68568..bae813d11bda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,7 @@ nix = { version = "0.29.0" } owo-colors = { version = "4.1.0" } path-slash = { version = "0.2.1" } pathdiff = { version = "0.2.1" } -petgraph = { version = "0.6.5" } +petgraph = { version = "0.7.1" } platform-info = { version = "2.0.3" } proc-macro2 = { version = "1.0.86" } procfs = { version = "0.17.0", default-features = false, features = ["flate2"] } From 68bfa5b5b8d32257fabcf0de9897437ba6088d64 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 8 Jan 2025 12:46:24 -0600 Subject: [PATCH 071/135] Avoid Docker rate limits by logging into DockerHub (#10400) The latest release flaked failing to fetch the buildx image, which is reportedly due to rate limits. Last I checked, DockerHub enforces much stricter limits on unauthenticated requests. I added a bot account and a corresponding read-only token. --- .github/workflows/build-docker.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 9ef0052342ff..843d80e4b2df 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -37,6 +37,12 @@ jobs: with: submodules: recursive + # Login to DockerHub first, to avoid rate-limiting + - uses: docker/login-action@v3 + with: + username: astralshbot + password: ${{ secrets.DOCKERHUB_TOKEN_RO }} + - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 @@ -108,6 +114,12 @@ jobs: - docker-build if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} steps: + # Login to DockerHub first, to avoid rate-limiting + - uses: docker/login-action@v3 + with: + username: astralshbot + password: ${{ secrets.DOCKERHUB_TOKEN_RO }} + - name: Download digests uses: actions/download-artifact@v4 with: @@ -180,6 +192,12 @@ jobs: - python:3.9-slim-bookworm,python3.9-bookworm-slim - python:3.8-slim-bookworm,python3.8-bookworm-slim steps: + # Login to DockerHub first, to avoid rate-limiting + - uses: docker/login-action@v3 + with: + username: astralshbot + password: ${{ secrets.DOCKERHUB_TOKEN_RO }} + - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 @@ -266,6 +284,12 @@ jobs: - docker-publish-extra if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} steps: + # Login to DockerHub first, to avoid rate-limiting + - uses: docker/login-action@v3 + with: + username: astralshbot + password: ${{ secrets.DOCKERHUB_TOKEN_RO }} + - name: Download digests uses: actions/download-artifact@v4 with: From 391ab757b85ad6c2c8955f39ecc578e70de25c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Varl=C4=B1?= Date: Wed, 8 Jan 2025 19:09:20 +0000 Subject: [PATCH 072/135] Add `ls` alias to `uv {tool, python, pip} list` (#10240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR adds `ls` alias to `uv {tool, python, pip} list` for convenience. Not sure if folks previously discussed this or have any opinion on having aliases – but I have a muscle memory for `ls` for listing things in commands I'm using (like `docker images ls`, `zellij ls`, `helm ls` etc.) and thought having `ls` alias for `list` command would be useful. ## Test Plan I simply compiled `uv` and manually checked `./target/release/uv {tool, python, pip} ls`. --- crates/uv-cli/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index b829483189a5..c798bec075d7 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -613,7 +613,8 @@ pub enum PipCommand { /// List, in tabular format, packages installed in an environment. #[command( after_help = "Use `uv help pip list` for more details.", - after_long_help = "" + after_long_help = "", + alias = "ls" )] List(PipListArgs), /// Show information about one or more installed packages. @@ -3724,6 +3725,7 @@ pub enum ToolCommand { #[command(alias = "update")] Upgrade(ToolUpgradeArgs), /// List installed tools. + #[command(alias = "ls")] List(ToolListArgs), /// Uninstall a tool. Uninstall(ToolUninstallArgs), @@ -4203,6 +4205,7 @@ pub enum PythonCommand { /// Use `--all-versions` to view all available patch versions. /// /// Use `--only-installed` to omit available downloads. + #[command(alias = "ls")] List(PythonListArgs), /// Download and install Python versions. From e22b728e3f39816c21cb4e597f51da53dc82ce82 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 15:34:45 -0500 Subject: [PATCH 073/135] Respect PEP 723 script lockfiles in uv run (#10136) ## Summary If a script has a lockfile, then `uv run` will now reuse it. Closes https://github.com/astral-sh/uv/issues/7483. Closes https://github.com/astral-sh/uv/issues/9688. --- crates/uv-configuration/src/dev.rs | 4 +- crates/uv-scripts/src/lib.rs | 20 ++ crates/uv/src/commands/project/add.rs | 1 + crates/uv/src/commands/project/environment.rs | 138 ++++++-- .../uv/src/commands/project/install_target.rs | 136 +++++++- crates/uv/src/commands/project/mod.rs | 4 +- crates/uv/src/commands/project/run.rs | 298 +++++++++++------- crates/uv/src/commands/project/sync.rs | 133 +------- crates/uv/src/commands/tool/run.rs | 4 +- crates/uv/tests/it/run.rs | 187 +++++++++++ 10 files changed, 658 insertions(+), 267 deletions(-) diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index 80ae4f0640e4..cce967ba1f89 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -316,7 +316,7 @@ impl From for DevGroupsSpecification { /// The manifest of `dependency-groups` to include, taking into account the user-provided /// [`DevGroupsSpecification`] and the project-specific default groups. -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct DevGroupsManifest { /// The specification for the development dependencies. pub(crate) spec: DevGroupsSpecification, @@ -347,7 +347,7 @@ impl DevGroupsManifest { } /// Returns `true` if the group was enabled by default. - pub fn default(&self, group: &GroupName) -> bool { + pub fn is_default(&self, group: &GroupName) -> bool { if self.spec.contains(group) { // If the group was explicitly requested, then it wasn't enabled by default. false diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index cdcccc96e613..2f812622747b 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -54,6 +54,14 @@ impl Pep723Item { Self::Remote(_) => None, } } + + /// Return the PEP 723 script, if any. + pub fn as_script(&self) -> Option<&Pep723Script> { + match self { + Self::Script(script) => Some(script), + _ => None, + } + } } /// A reference to a PEP 723 item. @@ -234,6 +242,18 @@ impl Pep723Script { Ok(()) } + + /// Return the [`Sources`] defined in the PEP 723 metadata. + pub fn sources(&self) -> &BTreeMap { + static EMPTY: BTreeMap = BTreeMap::new(); + + self.metadata + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .unwrap_or(&EMPTY) + } } /// PEP 723 metadata as parsed from a `script` comment block. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index c721f1527d06..017ebd5c8043 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -592,6 +592,7 @@ pub(crate) async fn add( Target::Project(project, environment) => (project, environment), // If `--script`, exit early. There's no reason to lock and sync. Target::Script(script, _) => { + // TODO(charlie): Lock the script, if a lockfile already exists. writeln!( printer.stderr(), "Updated `{}`", diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index e6b696518e39..0654ec6bc953 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -1,6 +1,7 @@ use tracing::debug; use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; +use crate::commands::project::install_target::InstallTarget; use crate::commands::project::{ resolve_environment, sync_environment, EnvironmentSpecification, ProjectError, }; @@ -9,10 +10,13 @@ use crate::settings::ResolverInstallerSettings; use uv_cache::{Cache, CacheBucket}; use uv_cache_key::{cache_digest, hash_digest}; use uv_client::Connectivity; -use uv_configuration::{Concurrency, PreviewMode, TrustedHost}; +use uv_configuration::{ + Concurrency, DevGroupsManifest, ExtrasSpecification, InstallOptions, PreviewMode, TrustedHost, +}; use uv_dispatch::SharedState; use uv_distribution_types::{Name, Resolution}; use uv_python::{Interpreter, PythonEnvironment}; +use uv_resolver::Installable; /// A [`PythonEnvironment`] stored in the cache. #[derive(Debug)] @@ -25,9 +29,8 @@ impl From for PythonEnvironment { } impl CachedEnvironment { - /// Get or create an [`CachedEnvironment`] based on a given set of requirements and a base - /// interpreter. - pub(crate) async fn get_or_create( + /// Get or create an [`CachedEnvironment`] based on a given set of requirements. + pub(crate) async fn from_spec( spec: EnvironmentSpecification<'_>, interpreter: &Interpreter, settings: &ResolverInstallerSettings, @@ -43,21 +46,7 @@ impl CachedEnvironment { printer: Printer, preview: PreviewMode, ) -> Result { - // When caching, always use the base interpreter, rather than that of the virtual - // environment. - let interpreter = if let Some(interpreter) = interpreter.to_base_interpreter(cache)? { - debug!( - "Caching via base interpreter: `{}`", - interpreter.sys_executable().display() - ); - interpreter - } else { - debug!( - "Caching via interpreter: `{}`", - interpreter.sys_executable().display() - ); - interpreter.clone() - }; + let interpreter = Self::base_interpreter(interpreter, cache)?; // Resolve the requirements with the interpreter. let resolution = Resolution::from( @@ -78,6 +67,93 @@ impl CachedEnvironment { .await?, ); + Self::from_resolution( + resolution, + interpreter, + settings, + state, + install, + installer_metadata, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await + } + + /// Get or create an [`CachedEnvironment`] based on a given [`InstallTarget`]. + pub(crate) async fn from_lock( + target: InstallTarget<'_>, + extras: &ExtrasSpecification, + dev: &DevGroupsManifest, + install_options: InstallOptions, + settings: &ResolverInstallerSettings, + interpreter: &Interpreter, + state: &SharedState, + install: Box, + installer_metadata: bool, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, + allow_insecure_host: &[TrustedHost], + cache: &Cache, + printer: Printer, + preview: PreviewMode, + ) -> Result { + let interpreter = Self::base_interpreter(interpreter, cache)?; + + // Determine the tags, markers, and interpreter to use for resolution. + let tags = interpreter.tags()?; + let marker_env = interpreter.resolver_marker_environment(); + + // Read the lockfile. + let resolution = target.to_resolution( + &marker_env, + tags, + extras, + dev, + &settings.build_options, + &install_options, + )?; + + Self::from_resolution( + resolution, + interpreter, + settings, + state, + install, + installer_metadata, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await + } + + /// Get or create an [`CachedEnvironment`] based on a given [`Resolution`]. + pub(crate) async fn from_resolution( + resolution: Resolution, + interpreter: Interpreter, + settings: &ResolverInstallerSettings, + state: &SharedState, + install: Box, + installer_metadata: bool, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, + allow_insecure_host: &[TrustedHost], + cache: &Cache, + printer: Printer, + preview: PreviewMode, + ) -> Result { // Hash the resolution by hashing the generated lockfile. // TODO(charlie): If the resolution contains any mutable metadata (like a path or URL // dependency), skip this step. @@ -144,4 +220,28 @@ impl CachedEnvironment { pub(crate) fn into_interpreter(self) -> Interpreter { self.0.into_interpreter() } + + /// Return the [`Interpreter`] to use for the cached environment, based on a given + /// [`Interpreter`]. + /// + /// When caching, always use the base interpreter, rather than that of the virtual + /// environment. + fn base_interpreter( + interpreter: &Interpreter, + cache: &Cache, + ) -> Result { + if let Some(interpreter) = interpreter.to_base_interpreter(cache)? { + debug!( + "Caching via base interpreter: `{}`", + interpreter.sys_executable().display() + ); + Ok(interpreter) + } else { + debug!( + "Caching via interpreter: `{}`", + interpreter.sys_executable().display() + ); + Ok(interpreter.clone()) + } + } } diff --git a/crates/uv/src/commands/project/install_target.rs b/crates/uv/src/commands/project/install_target.rs index 69999d4d77cf..78d88640960f 100644 --- a/crates/uv/src/commands/project/install_target.rs +++ b/crates/uv/src/commands/project/install_target.rs @@ -1,9 +1,14 @@ +use std::borrow::Cow; use std::path::Path; +use std::str::FromStr; use itertools::Either; use uv_normalize::PackageName; +use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl}; use uv_resolver::{Installable, Lock, Package}; +use uv_scripts::Pep723Script; +use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; use uv_workspace::Workspace; /// A target that can be installed from a lockfile. @@ -25,6 +30,11 @@ pub(crate) enum InstallTarget<'lock> { workspace: &'lock Workspace, lock: &'lock Lock, }, + /// A PEP 723 script. + Script { + script: &'lock Pep723Script, + lock: &'lock Lock, + }, } impl<'lock> Installable<'lock> for InstallTarget<'lock> { @@ -33,6 +43,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { Self::Project { workspace, .. } => workspace.install_path(), Self::Workspace { workspace, .. } => workspace.install_path(), Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(), + Self::Script { script, .. } => script.path.parent().unwrap(), } } @@ -41,24 +52,28 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { Self::Project { lock, .. } => lock, Self::Workspace { lock, .. } => lock, Self::NonProjectWorkspace { lock, .. } => lock, + Self::Script { lock, .. } => lock, } } fn roots(&self) -> impl Iterator { match self { - Self::Project { name, .. } => Either::Right(Either::Left(std::iter::once(*name))), - Self::NonProjectWorkspace { lock, .. } => Either::Left(lock.members().iter()), + Self::Project { name, .. } => Either::Left(Either::Left(std::iter::once(*name))), + Self::NonProjectWorkspace { lock, .. } => { + Either::Left(Either::Right(lock.members().iter())) + } Self::Workspace { lock, .. } => { // Identify the workspace members. // // The members are encoded directly in the lockfile, unless the workspace contains a // single member at the root, in which case, we identify it by its source. if lock.members().is_empty() { - Either::Right(Either::Right(lock.root().into_iter().map(Package::name))) + Either::Right(Either::Left(lock.root().into_iter().map(Package::name))) } else { - Either::Left(lock.members().iter()) + Either::Left(Either::Right(lock.members().iter())) } } + Self::Script { .. } => Either::Right(Either::Right(std::iter::empty())), } } @@ -67,17 +82,120 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { Self::Project { name, .. } => Some(name), Self::Workspace { .. } => None, Self::NonProjectWorkspace { .. } => None, + Self::Script { .. } => None, } } } impl<'lock> InstallTarget<'lock> { - /// Return the [`Workspace`] of the target. - pub(crate) fn workspace(&self) -> &'lock Workspace { + /// Return an iterator over all [`Sources`] defined by the target. + pub(crate) fn sources(&self) -> impl Iterator { + match self { + Self::Project { workspace, .. } + | Self::Workspace { workspace, .. } + | Self::NonProjectWorkspace { workspace, .. } => { + Either::Left(workspace.sources().values().flat_map(Sources::iter).chain( + workspace.packages().values().flat_map(|member| { + member + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .map(ToolUvSources::inner) + .into_iter() + .flat_map(|sources| sources.values().flat_map(Sources::iter)) + }), + )) + } + Self::Script { script, .. } => { + Either::Right(script.sources().values().flat_map(Sources::iter)) + } + } + } + + /// Return an iterator over all requirements defined by the target. + pub(crate) fn requirements( + &self, + ) -> impl Iterator>> { match self { - Self::Project { workspace, .. } => workspace, - Self::Workspace { workspace, .. } => workspace, - Self::NonProjectWorkspace { workspace, .. } => workspace, + Self::Project { workspace, .. } + | Self::Workspace { workspace, .. } + | Self::NonProjectWorkspace { workspace, .. } => { + Either::Left( + // Iterate over the non-member requirements in the workspace. + workspace + .requirements() + .into_iter() + .map(Cow::Owned) + .chain(workspace.dependency_groups().ok().into_iter().flat_map( + |dependency_groups| { + dependency_groups.into_values().flatten().map(Cow::Owned) + }, + )) + .chain(workspace.packages().values().flat_map(|member| { + // Iterate over all dependencies in each member. + let dependencies = member + .pyproject_toml() + .project + .as_ref() + .and_then(|project| project.dependencies.as_ref()) + .into_iter() + .flatten(); + let optional_dependencies = member + .pyproject_toml() + .project + .as_ref() + .and_then(|project| project.optional_dependencies.as_ref()) + .into_iter() + .flat_map(|optional| optional.values()) + .flatten(); + let dependency_groups = member + .pyproject_toml() + .dependency_groups + .as_ref() + .into_iter() + .flatten() + .flat_map(|(_, dependencies)| { + dependencies.iter().filter_map(|specifier| { + if let DependencyGroupSpecifier::Requirement(requirement) = + specifier + { + Some(requirement) + } else { + None + } + }) + }); + let dev_dependencies = member + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()) + .into_iter() + .flatten(); + dependencies + .chain(optional_dependencies) + .chain(dependency_groups) + .filter_map(|requires_dist| { + LenientRequirement::::from_str(requires_dist) + .map(uv_pep508::Requirement::from) + .map(Cow::Owned) + .ok() + }) + .chain(dev_dependencies.map(Cow::Borrowed)) + })), + ) + } + Self::Script { script, .. } => Either::Right( + script + .metadata + .dependencies + .iter() + .flatten() + .map(Cow::Borrowed), + ), } } } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index c738eba18661..1f85ec11a1d4 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -272,7 +272,7 @@ impl std::fmt::Display for ConflictError { self.conflicts .iter() .map(|conflict| match conflict { - ConflictPackage::Group(ref group) if self.dev.default(group) => + ConflictPackage::Group(ref group) if self.dev.is_default(group) => format!("`{group}` (enabled by default)"), ConflictPackage::Group(ref group) => format!("`{group}`"), ConflictPackage::Extra(..) => unreachable!(), @@ -291,7 +291,7 @@ impl std::fmt::Display for ConflictError { .map(|(i, conflict)| { let conflict = match conflict { ConflictPackage::Extra(ref extra) => format!("extra `{extra}`"), - ConflictPackage::Group(ref group) if self.dev.default(group) => { + ConflictPackage::Group(ref group) if self.dev.is_default(group) => { format!("group `{group}` (enabled by default)") } ConflictPackage::Group(ref group) => format!("group `{group}`"), diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index ac4e37694461..27a2e22b1bcb 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -17,8 +17,8 @@ use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::{ - Concurrency, DevGroupsSpecification, EditableMode, ExtrasSpecification, GroupsSpecification, - InstallOptions, LowerBound, PreviewMode, SourceStrategy, TrustedHost, + Concurrency, DevGroupsManifest, DevGroupsSpecification, EditableMode, ExtrasSpecification, + GroupsSpecification, InstallOptions, LowerBound, PreviewMode, SourceStrategy, TrustedHost, }; use uv_dispatch::SharedState; use uv_distribution::LoweredRequirement; @@ -201,109 +201,57 @@ pub(crate) async fn run( .await? .into_interpreter(); - // Determine the working directory for the script. - let script_dir = match &script { - Pep723Item::Script(script) => std::path::absolute(&script.path)? - .parent() - .expect("script path has no parent") - .to_owned(), - Pep723Item::Stdin(..) | Pep723Item::Remote(..) => std::env::current_dir()?, - }; - let script = script.into_metadata(); - - // Install the script requirements, if necessary. Otherwise, use an isolated environment. - if let Some(dependencies) = script.dependencies { - // Collect any `tool.uv.index` from the script. - let empty = Vec::default(); - let script_indexes = match settings.sources { - SourceStrategy::Enabled => script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.top_level.index.as_deref()) - .unwrap_or(&empty), - SourceStrategy::Disabled => &empty, - }; + // If a lockfile already exists, lock the script. + if let Some(target) = script + .as_script() + .map(LockTarget::from) + .filter(|target| target.lock_path().is_file()) + { + debug!("Found existing lockfile for script"); - // Collect any `tool.uv.sources` from the script. - let empty = BTreeMap::default(); - let script_sources = match settings.sources { - SourceStrategy::Enabled => script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.sources.as_ref()) - .unwrap_or(&empty), - SourceStrategy::Disabled => &empty, + // Determine the lock mode. + let mode = if frozen { + LockMode::Frozen + } else if locked { + LockMode::Locked(&interpreter) + } else { + LockMode::Write(&interpreter) }; - let requirements = dependencies - .into_iter() - .flat_map(|requirement| { - LoweredRequirement::from_non_workspace_requirement( - requirement, - script_dir.as_ref(), - script_sources, - script_indexes, - &settings.index_locations, - LowerBound::Allow, - ) - .map_ok(LoweredRequirement::into_inner) - }) - .collect::>()?; - let constraints = script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.constraint_dependencies.as_ref()) - .into_iter() - .flatten() - .cloned() - .flat_map(|requirement| { - LoweredRequirement::from_non_workspace_requirement( - requirement, - script_dir.as_ref(), - script_sources, - script_indexes, - &settings.index_locations, - LowerBound::Allow, - ) - .map_ok(LoweredRequirement::into_inner) - }) - .collect::, _>>()?; - let overrides = script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.override_dependencies.as_ref()) - .into_iter() - .flatten() - .cloned() - .flat_map(|requirement| { - LoweredRequirement::from_non_workspace_requirement( - requirement, - script_dir.as_ref(), - script_sources, - script_indexes, - &settings.index_locations, - LowerBound::Allow, - ) - .map_ok(LoweredRequirement::into_inner) - }) - .collect::, _>>()?; - - let spec = - RequirementsSpecification::from_overrides(requirements, constraints, overrides); - let result = CachedEnvironment::get_or_create( - EnvironmentSpecification::from(spec), - &interpreter, - &settings, + // Generate a lockfile. + let lock = project::lock::do_safe_lock( + mode, + target, + settings.as_ref().into(), + LowerBound::Allow, &state, if show_resolution { Box::new(DefaultResolveLogger) } else { Box::new(SummaryResolveLogger) }, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await? + .into_lock(); + + let result = CachedEnvironment::from_lock( + InstallTarget::Script { + script: script.as_script().unwrap(), + lock: &lock, + }, + &ExtrasSpecification::default(), + &DevGroupsManifest::default(), + InstallOptions::default(), + &settings, + &interpreter, + &state, if show_resolution { Box::new(DefaultInstallLogger) } else { @@ -332,19 +280,151 @@ pub(crate) async fn run( Some(environment.into_interpreter()) } else { - // Create a virtual environment. - temp_dir = cache.venv_dir()?; - let environment = uv_virtualenv::create_venv( - temp_dir.path(), - interpreter, - uv_virtualenv::Prompt::None, - false, - false, - false, - false, - )?; + // Determine the working directory for the script. + let script_dir = match &script { + Pep723Item::Script(script) => std::path::absolute(&script.path)? + .parent() + .expect("script path has no parent") + .to_owned(), + Pep723Item::Stdin(..) | Pep723Item::Remote(..) => std::env::current_dir()?, + }; + let script = script.into_metadata(); + + // Install the script requirements, if necessary. Otherwise, use an isolated environment. + if let Some(dependencies) = script.dependencies { + // Collect any `tool.uv.index` from the script. + let empty = Vec::default(); + let script_indexes = match settings.sources { + SourceStrategy::Enabled => script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.top_level.index.as_deref()) + .unwrap_or(&empty), + SourceStrategy::Disabled => &empty, + }; - Some(environment.into_interpreter()) + // Collect any `tool.uv.sources` from the script. + let empty = BTreeMap::default(); + let script_sources = match settings.sources { + SourceStrategy::Enabled => script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .unwrap_or(&empty), + SourceStrategy::Disabled => &empty, + }; + + let requirements = dependencies + .into_iter() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + LowerBound::Allow, + ) + .map_ok(LoweredRequirement::into_inner) + }) + .collect::>()?; + let constraints = script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.constraint_dependencies.as_ref()) + .into_iter() + .flatten() + .cloned() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + LowerBound::Allow, + ) + .map_ok(LoweredRequirement::into_inner) + }) + .collect::, _>>()?; + let overrides = script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.override_dependencies.as_ref()) + .into_iter() + .flatten() + .cloned() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + LowerBound::Allow, + ) + .map_ok(LoweredRequirement::into_inner) + }) + .collect::, _>>()?; + + let spec = + RequirementsSpecification::from_overrides(requirements, constraints, overrides); + let result = CachedEnvironment::from_spec( + EnvironmentSpecification::from(spec), + &interpreter, + &settings, + &state, + if show_resolution { + Box::new(DefaultResolveLogger) + } else { + Box::new(SummaryResolveLogger) + }, + if show_resolution { + Box::new(DefaultInstallLogger) + } else { + Box::new(SummaryInstallLogger) + }, + installer_metadata, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await; + + let environment = match result { + Ok(resolution) => resolution, + Err(ProjectError::Operation(err)) => { + return diagnostics::OperationDiagnostic::with_context("script") + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) + } + Err(err) => return Err(err.into()), + }; + + Some(environment.into_interpreter()) + } else { + // Create a virtual environment. + temp_dir = cache.venv_dir()?; + let environment = uv_virtualenv::create_venv( + temp_dir.path(), + interpreter, + uv_virtualenv::Prompt::None, + false, + false, + false, + false, + )?; + + Some(environment.into_interpreter()) + } } } else { None @@ -847,7 +927,7 @@ pub(crate) async fn run( Some(spec) => { debug!("Syncing ephemeral requirements"); - let result = CachedEnvironment::get_or_create( + let result = CachedEnvironment::from_spec( EnvironmentSpecification::from(spec).with_lock( lock.as_ref() .map(|(lock, install_path)| (lock, install_path.as_ref())), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 78a3595c3ff6..755a4314c3b7 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,6 +1,4 @@ -use std::borrow::Cow; use std::path::Path; -use std::str::FromStr; use anyhow::{Context, Result}; use itertools::Itertools; @@ -18,16 +16,14 @@ use uv_distribution_types::{ }; use uv_installer::SitePackages; use uv_normalize::PackageName; -use uv_pep508::{MarkerTree, Requirement, VersionOrUrl}; -use uv_pypi_types::{ - LenientRequirement, ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl, -}; +use uv_pep508::{MarkerTree, VersionOrUrl}; +use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Installable}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user; -use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; +use uv_workspace::pyproject::Source; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; @@ -368,7 +364,7 @@ pub(super) async fn do_sync( } // Populate credentials from the workspace. - store_credentials_from_workspace(target.workspace()); + store_credentials_from_workspace(target); // Initialize the registry client. let client = RegistryClientBuilder::new(cache.clone()) @@ -526,9 +522,9 @@ fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolu /// /// These credentials can come from any of `tool.uv.sources`, `tool.uv.dev-dependencies`, /// `project.dependencies`, and `project.optional-dependencies`. -fn store_credentials_from_workspace(workspace: &Workspace) { - // Iterate over any sources in the workspace root. - for source in workspace.sources().values().flat_map(Sources::iter) { +fn store_credentials_from_workspace(target: InstallTarget<'_>) { + // Iterate over any sources in the target. + for source in target.sources() { match source { Source::Git { git, .. } => { uv_git::store_credentials_from_url(git); @@ -540,29 +536,8 @@ fn store_credentials_from_workspace(workspace: &Workspace) { } } - // Iterate over any dependencies defined in the workspace root. - for requirement in &workspace.requirements() { - let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else { - continue; - }; - match &url.parsed_url { - ParsedUrl::Git(ParsedGitUrl { url, .. }) => { - uv_git::store_credentials_from_url(url.repository()); - } - ParsedUrl::Archive(ParsedArchiveUrl { url, .. }) => { - uv_auth::store_credentials_from_url(url); - } - _ => {} - } - } - - // Iterate over any dependency groups defined in the workspace root. - for requirement in workspace - .dependency_groups() - .ok() - .iter() - .flat_map(|groups| groups.values().flat_map(|group| group.iter())) - { + // Iterate over any dependencies defined in the target. + for requirement in target.requirements() { let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else { continue; }; @@ -576,94 +551,4 @@ fn store_credentials_from_workspace(workspace: &Workspace) { _ => {} } } - - // Iterate over each workspace member. - for member in workspace.packages().values() { - // Iterate over the `tool.uv.sources`. - for source in member - .pyproject_toml() - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.sources.as_ref()) - .map(ToolUvSources::inner) - .iter() - .flat_map(|sources| sources.values().flat_map(Sources::iter)) - { - match source { - Source::Git { git, .. } => { - uv_git::store_credentials_from_url(git); - } - Source::Url { url, .. } => { - uv_auth::store_credentials_from_url(url); - } - _ => {} - } - } - - // Iterate over all dependencies. - let dependencies = member - .pyproject_toml() - .project - .as_ref() - .and_then(|project| project.dependencies.as_ref()) - .into_iter() - .flatten(); - let optional_dependencies = member - .pyproject_toml() - .project - .as_ref() - .and_then(|project| project.optional_dependencies.as_ref()) - .into_iter() - .flat_map(|optional| optional.values()) - .flatten(); - let dependency_groups = member - .pyproject_toml() - .dependency_groups - .as_ref() - .into_iter() - .flatten() - .flat_map(|(_, dependencies)| { - dependencies.iter().filter_map(|specifier| { - if let DependencyGroupSpecifier::Requirement(requirement) = specifier { - Some(requirement) - } else { - None - } - }) - }); - let dev_dependencies = member - .pyproject_toml() - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()) - .into_iter() - .flatten(); - - for requirement in dependencies - .chain(optional_dependencies) - .chain(dependency_groups) - .filter_map(|requires_dist| { - LenientRequirement::::from_str(requires_dist) - .map(Requirement::from) - .map(Cow::Owned) - .ok() - }) - .chain(dev_dependencies.map(Cow::Borrowed)) - { - let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else { - continue; - }; - match &url.parsed_url { - ParsedUrl::Git(ParsedGitUrl { url, .. }) => { - uv_git::store_credentials_from_url(url.repository()); - } - ParsedUrl::Archive(ParsedArchiveUrl { url, .. }) => { - uv_auth::store_credentials_from_url(url); - } - _ => {} - } - } - } } diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index bdeb735ece4f..79a7a30dab3e 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -620,7 +620,7 @@ async fn get_or_create_environment( // TODO(zanieb): When implementing project-level tools, discover the project and check if it has the tool. // TODO(zanieb): Determine if we should layer on top of the project environment if it is present. - let result = CachedEnvironment::get_or_create( + let result = CachedEnvironment::from_spec( spec.clone(), &interpreter, settings, @@ -679,7 +679,7 @@ async fn get_or_create_environment( interpreter.sys_executable().display() ); - CachedEnvironment::get_or_create( + CachedEnvironment::from_spec( spec, &interpreter, settings, diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index b330ba9cac62..fd506a7a2853 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -324,6 +324,9 @@ fn run_pep723_script() -> Result<()> { Resolved 1 package in [TIME] "###); + // But neither invocation should create a lockfile. + assert!(!context.temp_dir.child("main.py.lock").exists()); + // Otherwise, the script requirements should _not_ be available, but the project requirements // should. let test_non_script = context.temp_dir.child("main.py"); @@ -774,6 +777,190 @@ fn run_pep723_script_overrides() -> Result<()> { Ok(()) } +/// Run a PEP 723-compatible script with a lockfile. +#[test] +fn run_pep723_script_lock() -> Result<()> { + let context = TestContext::new("3.12"); + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "iniconfig", + # ] + # /// + + import iniconfig + + print("Hello, world!") + "# + })?; + + // Explicitly lock the script. + uv_snapshot!(context.filters(), context.lock().arg("--script").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "###); + + let lock = context.read("main.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [{ name = "iniconfig" }] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + "### + ); + }); + + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello, world! + + ----- stderr ----- + Reading inline script metadata from `main.py` + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + // Modify the metadata. + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "anyio", + # ] + # /// + + import anyio + + print("Hello, world!") + "# + })?; + + // Re-running the script with `--locked` should error. + uv_snapshot!(context.filters(), context.run().arg("--locked").arg("main.py"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Reading inline script metadata from `main.py` + Resolved 3 packages in [TIME] + error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. + "###); + + // Re-running the script with `--frozen` should also error, but at runtime. + uv_snapshot!(context.filters(), context.run().arg("--frozen").arg("main.py"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Reading inline script metadata from `main.py` + warning: `--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation + Traceback (most recent call last): + File "[TEMP_DIR]/main.py", line 8, in + import anyio + ModuleNotFoundError: No module named 'anyio' + "###); + + // Re-running the script should update the lockfile. + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello, world! + + ----- stderr ----- + Reading inline script metadata from `main.py` + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + let lock = context.read("main.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [{ name = "anyio" }] + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +} + /// With `managed = false`, we should avoid installing the project itself. #[test] fn run_managed_false() -> Result<()> { From 31b2d3f9885071a5d6ca1379aee2dbb9bdad9a6a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 16:19:12 -0500 Subject: [PATCH 074/135] Update PEP 723 lockfile in `uv add --script` (#10145) ## Summary `uv add --script main.py anyio` will now update the lockfile, _if_ it already exists. (If no such lockfile exists, the behavior is unchanged.) --- crates/uv-cli/src/lib.rs | 7 +- crates/uv-scripts/src/lib.rs | 6 +- crates/uv/src/commands/project/add.rs | 293 ++++++++----- crates/uv/src/commands/project/lock_target.rs | 4 +- crates/uv/src/commands/project/mod.rs | 3 + crates/uv/src/commands/project/remove.rs | 2 +- crates/uv/tests/it/edit.rs | 391 ++++++++++++++++++ 7 files changed, 585 insertions(+), 121 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c798bec075d7..817d35fdfec6 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3257,7 +3257,12 @@ pub struct AddArgs { /// a new one will be created and added to the script. When executed via `uv run`, /// uv will create a temporary environment for the script with all inline /// dependencies installed. - #[arg(long, conflicts_with = "dev", conflicts_with = "optional")] + #[arg( + long, + conflicts_with = "dev", + conflicts_with = "optional", + conflicts_with = "package" + )] pub script: Option, /// The Python interpreter to use for resolving and syncing. diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 2f812622747b..a17398e278a0 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -230,7 +230,7 @@ impl Pep723Script { } /// Replace the existing metadata in the file with new metadata and write the updated content. - pub async fn write(&self, metadata: &str) -> Result<(), Pep723Error> { + pub fn write(&self, metadata: &str) -> Result<(), io::Error> { let content = format!( "{}{}{}", self.prelude, @@ -238,7 +238,7 @@ impl Pep723Script { self.postlude ); - fs_err::tokio::write(&self.path, content).await?; + fs_err::write(&self.path, content)?; Ok(()) } @@ -307,7 +307,7 @@ impl Pep723Metadata { } impl FromStr for Pep723Metadata { - type Err = Pep723Error; + type Err = toml::de::Error; /// Parse `Pep723Metadata` from a raw TOML string. fn from_str(raw: &str) -> Result { diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 017ebd5c8043..99378e28074c 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1,12 +1,14 @@ -use anyhow::{bail, Context, Result}; -use itertools::Itertools; -use owo_colors::OwoColorize; -use rustc_hash::{FxBuildHasher, FxHashMap}; use std::collections::hash_map::Entry; use std::fmt::Write; use std::io; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::Arc; + +use anyhow::{bail, Context, Result}; +use itertools::Itertools; +use owo_colors::OwoColorize; +use rustc_hash::{FxBuildHasher, FxHashMap}; use tracing::debug; use url::Url; @@ -29,7 +31,7 @@ use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimPa use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_resolver::FlatIndex; -use uv_scripts::{Pep723ItemRef, Pep723Script}; +use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user_once; @@ -173,7 +175,7 @@ pub(crate) async fn add( .await? .into_interpreter(); - Target::Script(script, Box::new(interpreter)) + AddTarget::Script(script, Box::new(interpreter)) } else { // Find the project in the workspace. let project = if let Some(package) = package { @@ -221,7 +223,7 @@ pub(crate) async fn add( .await? .into_interpreter(); - Target::Project(project, Box::new(PythonTarget::Interpreter(interpreter))) + AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter))) } else { // Discover or create the virtual environment. let venv = project::get_or_init_environment( @@ -239,7 +241,7 @@ pub(crate) async fn add( ) .await?; - Target::Project(project, Box::new(PythonTarget::Environment(venv))) + AddTarget::Project(project, Box::new(PythonTarget::Environment(venv))) } }; @@ -361,7 +363,7 @@ pub(crate) async fn add( // If any of the requirements are self-dependencies, bail. if matches!(dependency_type, DependencyType::Production) { - if let Target::Project(project, _) = &target { + if let AddTarget::Project(project, _) = &target { if let Some(project_name) = project.project_name() { for requirement in &requirements { if requirement.name == *project_name { @@ -389,10 +391,10 @@ pub(crate) async fn add( // Add the requirements to the `pyproject.toml` or script. let mut toml = match &target { - Target::Script(script, _) => { + AddTarget::Script(script, _) => { PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script) } - Target::Project(project, _) => PyProjectTomlMut::from_toml( + AddTarget::Project(project, _) => PyProjectTomlMut::from_toml( &project.pyproject_toml().raw, DependencyTarget::PyProjectToml, ), @@ -405,10 +407,10 @@ pub(crate) async fn add( requirement.extras.dedup(); let (requirement, source) = match target { - Target::Script(_, _) | Target::Project(_, _) if raw_sources => { + AddTarget::Script(_, _) | AddTarget::Project(_, _) if raw_sources => { (uv_pep508::Requirement::from(requirement), None) } - Target::Script(ref script, _) => { + AddTarget::Script(ref script, _) => { let script_path = std::path::absolute(&script.path)?; let script_dir = script_path.parent().expect("script path has no parent"); resolve_requirement( @@ -422,7 +424,7 @@ pub(crate) async fn add( script_dir, )? } - Target::Project(ref project, _) => { + AddTarget::Project(ref project, _) => { let workspace = project .workspace() .packages() @@ -566,33 +568,17 @@ pub(crate) async fn add( let content = toml.to_string(); // Save the modified `pyproject.toml` or script. - let modified = match &target { - Target::Script(script, _) => { - if content == script.metadata.raw { - debug!("No changes to dependencies; skipping update"); - false - } else { - script.write(&content).await?; - true - } - } - Target::Project(project, _) => { - if content == *project.pyproject_toml().raw { - debug!("No changes to dependencies; skipping update"); - false - } else { - let pyproject_path = project.root().join("pyproject.toml"); - fs_err::write(pyproject_path, &content)?; - true - } - } - }; + let modified = target.write(&content)?; - let (project, environment) = match target { - Target::Project(project, environment) => (project, environment), - // If `--script`, exit early. There's no reason to lock and sync. - Target::Script(script, _) => { - // TODO(charlie): Lock the script, if a lockfile already exists. + // If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock` + // to exist at all. + if frozen { + return Ok(ExitStatus::Success); + } + + // If we're modifying a script, and lockfile doesn't exist, don't create it. + if let AddTarget::Script(ref script, _) = target { + if !LockTarget::from(script).lock_path().is_file() { writeln!( printer.stderr(), "Updated `{}`", @@ -600,39 +586,20 @@ pub(crate) async fn add( )?; return Ok(ExitStatus::Success); } - }; - - // If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock` - // to exist at all. - if frozen { - return Ok(ExitStatus::Success); } // Store the content prior to any modifications. - let project_root = project.root().to_path_buf(); - let workspace_root = project.workspace().install_path().clone(); - let existing_pyproject_toml = project.pyproject_toml().as_ref().to_vec(); - let existing_uv_lock = LockTarget::from(project.workspace()).read_bytes().await?; + let snapshot = target.snapshot().await?; // Update the `pypackage.toml` in-memory. - let project = project - .with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?) - .ok_or(ProjectError::PyprojectTomlUpdate)?; + let target = target.update(&content)?; // Set the Ctrl-C handler to revert changes on exit. let _ = ctrlc::set_handler({ - let project_root = project_root.clone(); - let workspace_root = workspace_root.clone(); - let existing_pyproject_toml = existing_pyproject_toml.clone(); - let existing_uv_lock = existing_uv_lock.clone(); + let snapshot = snapshot.clone(); move || { if modified { - let _ = revert( - &project_root, - &workspace_root, - &existing_pyproject_toml, - existing_uv_lock.as_deref(), - ); + let _ = snapshot.revert(); } #[allow(clippy::exit, clippy::cast_possible_wrap)] @@ -645,10 +612,9 @@ pub(crate) async fn add( }); match lock_and_sync( - project, + target, &mut toml, &edits, - &environment, state, locked, &dependency_type, @@ -668,12 +634,7 @@ pub(crate) async fn add( Ok(()) => Ok(ExitStatus::Success), Err(err) => { if modified { - let _ = revert( - &project_root, - &workspace_root, - &existing_pyproject_toml, - existing_uv_lock.as_deref(), - ); + let _ = snapshot.revert(); } match err { ProjectError::Operation(err) => diagnostics::OperationDiagnostic::with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green())) @@ -688,10 +649,9 @@ pub(crate) async fn add( /// Re-lock and re-sync the project after a series of edits. #[allow(clippy::fn_params_excessive_bools)] async fn lock_and_sync( - mut project: VirtualProject, + mut target: AddTarget, toml: &mut PyProjectTomlMut, edits: &[DependencyEdit], - environment: &PythonTarget, state: SharedState, locked: bool, dependency_type: &DependencyType, @@ -706,15 +666,13 @@ async fn lock_and_sync( printer: Printer, preview: PreviewMode, ) -> Result<(), ProjectError> { - let mode = if locked { - LockMode::Locked(environment.interpreter()) - } else { - LockMode::Write(environment.interpreter()) - }; - let mut lock = project::lock::do_safe_lock( - mode, - project.workspace().into(), + if locked { + LockMode::Locked(target.interpreter()) + } else { + LockMode::Write(target.interpreter()) + }, + (&target).into(), settings.into(), LowerBound::default(), &state, @@ -811,17 +769,13 @@ async fn lock_and_sync( let content = toml.to_string(); // Write the updated `pyproject.toml` to disk. - fs_err::write(project.root().join("pyproject.toml"), &content)?; + target.write(&content)?; // Update the `pypackage.toml` in-memory. - project = project - .with_pyproject_toml( - toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?, - ) - .ok_or(ProjectError::PyprojectTomlUpdate)?; + target = target.update(&content)?; // Invalidate the project metadata. - if let VirtualProject::Project(ref project) = project { + if let AddTarget::Project(VirtualProject::Project(ref project), _) = target { let url = Url::from_file_path(project.project_root()) .expect("project root is a valid URL"); let version_id = VersionId::from_url(&url); @@ -832,8 +786,12 @@ async fn lock_and_sync( // If the file was modified, we have to lock again, though the only expected change is // the addition of the minimum version specifiers. lock = project::lock::do_safe_lock( - mode, - project.workspace().into(), + if locked { + LockMode::Locked(target.interpreter()) + } else { + LockMode::Write(target.interpreter()) + }, + (&target).into(), settings.into(), LowerBound::default(), &state, @@ -851,7 +809,12 @@ async fn lock_and_sync( } } - let PythonTarget::Environment(venv) = environment else { + let AddTarget::Project(project, environment) = target else { + // If we're not adding to a project, exit early. + return Ok(()); + }; + + let PythonTarget::Environment(venv) = &*environment else { // If we're not syncing, exit early. return Ok(()); }; @@ -918,25 +881,6 @@ async fn lock_and_sync( Ok(()) } -/// Revert the changes to the `pyproject.toml` and `uv.lock`, if necessary. -fn revert( - project_root: &Path, - workspace_root: &Path, - pyproject_toml: &[u8], - uv_lock: Option<&[u8]>, -) -> Result<(), io::Error> { - debug!("Reverting changes to `pyproject.toml`"); - let () = fs_err::write(project_root.join("pyproject.toml"), pyproject_toml)?; - if let Some(uv_lock) = uv_lock.as_ref() { - debug!("Reverting changes to `uv.lock`"); - let () = fs_err::write(workspace_root.join("uv.lock"), uv_lock)?; - } else { - debug!("Removing `uv.lock`"); - let () = fs_err::remove_file(workspace_root.join("uv.lock"))?; - } - Ok(()) -} - /// Augment a user-provided requirement by attaching any specification data that was provided /// separately from the requirement itself (e.g., `--branch main`). fn augment_requirement( @@ -1047,8 +991,8 @@ fn resolve_requirement( } /// Represents the destination where dependencies are added, either to a project or a script. -#[derive(Debug)] -enum Target { +#[derive(Debug, Clone)] +enum AddTarget { /// A PEP 723 script, with inline metadata. Script(Pep723Script, Box), @@ -1056,7 +1000,16 @@ enum Target { Project(VirtualProject, Box), } -impl Target { +impl<'lock> From<&'lock AddTarget> for LockTarget<'lock> { + fn from(value: &'lock AddTarget) -> Self { + match value { + AddTarget::Script(script, _) => LockTarget::Script(script), + AddTarget::Project(project, _) => LockTarget::Workspace(project.workspace()), + } + } +} + +impl AddTarget { /// Returns the [`Interpreter`] for the target. fn interpreter(&self) -> &Interpreter { match self { @@ -1064,10 +1017,122 @@ impl Target { Self::Project(_, venv) => venv.interpreter(), } } + + /// Write the updated content to the target. + /// + /// Returns `true` if the content was modified. + fn write(&self, content: &str) -> Result { + match self { + Self::Script(script, _) => { + if content == script.metadata.raw { + debug!("No changes to dependencies; skipping update"); + Ok(false) + } else { + script.write(content)?; + Ok(true) + } + } + Self::Project(project, _) => { + if content == project.pyproject_toml().raw { + debug!("No changes to dependencies; skipping update"); + Ok(false) + } else { + let pyproject_path = project.root().join("pyproject.toml"); + fs_err::write(pyproject_path, content)?; + Ok(true) + } + } + } + } + + /// Update the target in-memory to incorporate the new content. + #[allow(clippy::result_large_err)] + fn update(self, content: &str) -> Result { + match self { + Self::Script(mut script, interpreter) => { + script.metadata = Pep723Metadata::from_str(content) + .map_err(ProjectError::Pep723ScriptTomlParse)?; + Ok(Self::Script(script, interpreter)) + } + Self::Project(project, venv) => { + let project = project + .with_pyproject_toml( + toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?, + ) + .ok_or(ProjectError::PyprojectTomlUpdate)?; + Ok(Self::Project(project, venv)) + } + } + } + + /// Take a snapshot of the target. + async fn snapshot(&self) -> Result { + // Read the lockfile into memory. + let target = match self { + Self::Script(script, _) => LockTarget::from(script), + Self::Project(project, _) => LockTarget::Workspace(project.workspace()), + }; + let lock = target.read_bytes().await?; + + // Clone the target. + match self { + Self::Script(script, _) => Ok(AddTargetSnapshot::Script(script.clone(), lock)), + Self::Project(project, _) => Ok(AddTargetSnapshot::Project(project.clone(), lock)), + } + } +} + +#[derive(Debug, Clone)] +enum AddTargetSnapshot { + Script(Pep723Script, Option>), + Project(VirtualProject, Option>), +} + +impl AddTargetSnapshot { + /// Write the snapshot back to disk (e.g., to a `pyproject.toml` and `uv.lock`). + fn revert(&self) -> Result<(), io::Error> { + match self { + Self::Script(script, lock) => { + // Write the PEP 723 script back to disk. + debug!("Reverting changes to PEP 723 script block"); + script.write(&script.metadata.raw)?; + + // Write the lockfile back to disk. + let target = LockTarget::from(script); + if let Some(lock) = lock { + debug!("Reverting changes to `uv.lock`"); + fs_err::write(target.lock_path(), lock)?; + } else { + debug!("Removing `uv.lock`"); + fs_err::remove_file(target.lock_path())?; + } + Ok(()) + } + Self::Project(project, lock) => { + // Write the `pyproject.toml` back to disk. + debug!("Reverting changes to `pyproject.toml`"); + fs_err::write( + project.root().join("pyproject.toml"), + project.pyproject_toml().as_ref(), + )?; + + // Write the lockfile back to disk. + let target = LockTarget::from(project.workspace()); + if let Some(lock) = lock { + debug!("Reverting changes to `uv.lock`"); + fs_err::write(target.lock_path(), lock)?; + } else { + debug!("Removing `uv.lock`"); + fs_err::remove_file(target.lock_path())?; + } + Ok(()) + } + } + } } /// A Python [`Interpreter`] or [`PythonEnvironment`] for a project. -#[derive(Debug)] +#[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] enum PythonTarget { Interpreter(Interpreter), diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index 2d2cc0197adb..ba3e9727b88c 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -235,11 +235,11 @@ impl<'lock> LockTarget<'lock> { } /// Read the lockfile from the workspace as bytes. - pub(crate) async fn read_bytes(self) -> Result>, ProjectError> { + pub(crate) async fn read_bytes(self) -> Result>, std::io::Error> { match fs_err::tokio::read(self.lock_path()).await { Ok(encoded) => Ok(Some(encoded)), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(err) => Err(err.into()), + Err(err) => Err(err), } } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 1f85ec11a1d4..57cf4d9c2fab 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -167,6 +167,9 @@ pub(crate) enum ProjectError { #[error("Failed to update `pyproject.toml`")] PyprojectTomlUpdate, + #[error("Failed to parse PEP 723 script metadata")] + Pep723ScriptTomlParse(#[source] toml::de::Error), + #[error(transparent)] DependencyGroup(#[from] DependencyGroupError), diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 5a669b74c3cf..415115aa358a 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -164,7 +164,7 @@ pub(crate) async fn remove( // Save the modified dependencies. match &target { Target::Script(script) => { - script.write(&toml.to_string()).await?; + script.write(&toml.to_string())?; } Target::Project(project) => { let pyproject_path = project.root().join("pyproject.toml"); diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index aabfa2435ffb..fc5942223606 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -4929,6 +4929,10 @@ fn add_script() -> Result<()> { "### ); }); + + // Adding to a script without a lockfile shouldn't create a lockfile. + assert!(!context.temp_dir.join("script.py.lock").exists()); + Ok(()) } @@ -5330,6 +5334,393 @@ fn remove_repeated() -> Result<()> { Ok(()) } +/// Add to a PEP 732 script with a lockfile. +#[test] +fn add_script_lock() -> Result<()> { + let context = TestContext::new("3.12"); + + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "requests<3", + # "rich", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "#})?; + + // Explicitly lock the script. + uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + "###); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "requests", specifier = "<3" }, + { name = "rich" }, + ] + + [[package]] + name = "certifi" + version = "2024.2.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774 }, + ] + + [[package]] + name = "charset-normalizer" + version = "3.3.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "markdown-it-py" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "mdurl" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + ] + + [[package]] + name = "mdurl" + version = "0.1.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + ] + + [[package]] + name = "pygments" + version = "2.17.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 }, + ] + + [[package]] + name = "requests" + version = "2.31.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, + ] + + [[package]] + name = "rich" + version = "13.7.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, + ] + + [[package]] + name = "urllib3" + version = "2.2.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067 }, + ] + "### + ); + }); + + // Adding to a locked script should update the lockfile. + uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--script").arg("script.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 packages in [TIME] + "###); + + let script_content = context.read("script.py"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + script_content, @r###" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "anyio>=4.3.0", + # "requests<3", + # "rich", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "### + ); + }); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "anyio", specifier = ">=4.3.0" }, + { name = "requests", specifier = "<3" }, + { name = "rich" }, + ] + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "certifi" + version = "2024.2.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774 }, + ] + + [[package]] + name = "charset-normalizer" + version = "3.3.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "markdown-it-py" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "mdurl" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + ] + + [[package]] + name = "mdurl" + version = "0.1.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + ] + + [[package]] + name = "pygments" + version = "2.17.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 }, + ] + + [[package]] + name = "requests" + version = "2.31.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, + ] + + [[package]] + name = "rich" + version = "13.7.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + + [[package]] + name = "urllib3" + version = "2.2.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067 }, + ] + "### + ); + }); + + Ok(()) +} + /// Remove from a PEP732 script, #[test] fn remove_script() -> Result<()> { From 9d5779b68c88dd67aeafbbf37feb951e86bffdd9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 16:32:46 -0500 Subject: [PATCH 075/135] Add `--script` support to `uv tree` for PEP 723 scripts (#10159) ## Summary You can now run `uv tree --script main.py` to show the dependency tree for a given script. If a lockfile doesn't exist, it will create one. Closes https://github.com/astral-sh/uv/issues/7328. --- crates/uv-cli/src/lib.rs | 8 + crates/uv/src/commands/project/mod.rs | 8 + crates/uv/src/commands/project/tree.rs | 54 +++- crates/uv/src/lib.rs | 18 +- crates/uv/src/settings.rs | 4 + crates/uv/tests/it/tree.rs | 400 ++++++++++++++++++++++++- docs/reference/cli.md | 4 + 7 files changed, 483 insertions(+), 13 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 817d35fdfec6..1ec02fa391d5 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3437,6 +3437,14 @@ pub struct TreeArgs { #[command(flatten)] pub resolver: ResolverArgs, + /// Show the dependency tree the specified PEP 723 Python script, rather than the current + /// project. + /// + /// If provided, uv will resolve the dependencies based on its inline metadata table, in + /// adherence with PEP 723. + #[arg(long)] + pub script: Option, + /// The Python version to use when filtering the tree. /// /// For example, pass `--python-version 3.10` to display the dependencies diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 57cf4d9c2fab..4f3c1416efcb 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -143,6 +143,9 @@ pub(crate) enum ProjectError { #[error("Group `{0}` is not defined in any project's `dependency-group` table")] MissingGroupWorkspace(GroupName), + #[error("PEP 723 scripts do not support dependency groups, but group `{0}` was specified")] + MissingGroupScript(GroupName), + #[error("Default group `{0}` (from `tool.uv.default-groups`) is not defined in the project's `dependency-group` table")] MissingDefaultGroup(GroupName), @@ -1730,6 +1733,8 @@ pub(crate) enum DependencyGroupsTarget<'env> { Workspace(&'env Workspace), /// The dependency groups must be defined in the target project. Project(&'env ProjectWorkspace), + /// The dependency groups must be defined in the target script. + Script, } impl DependencyGroupsTarget<'_> { @@ -1760,6 +1765,9 @@ impl DependencyGroupsTarget<'_> { return Err(ProjectError::MissingGroupProject(group.clone())); } } + Self::Script => { + return Err(ProjectError::MissingGroupScript(group.clone())); + } } } Ok(()) diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 1994862fa7fb..1c612cceabfe 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -15,6 +15,7 @@ use uv_distribution_types::IndexCapabilities; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; use uv_resolver::{PackageMap, TreeDisplay}; +use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_workspace::{DiscoveryOptions, Workspace}; @@ -22,8 +23,10 @@ use crate::commands::pip::latest::LatestClient; use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::resolution_markers; use crate::commands::project::lock::{do_safe_lock, LockMode}; +use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ default_dependency_groups, DependencyGroupsTarget, ProjectError, ProjectInterpreter, + ScriptInterpreter, }; use crate::commands::reporters::LatestVersionReporter; use crate::commands::{diagnostics, ExitStatus}; @@ -49,6 +52,7 @@ pub(crate) async fn tree( python: Option, install_mirrors: PythonInstallMirrors, settings: ResolverSettings, + script: Option, python_preference: PythonPreference, python_downloads: PythonDownloads, connectivity: Connectivity, @@ -61,24 +65,51 @@ pub(crate) async fn tree( preview: PreviewMode, ) -> Result { // Find the project requirements. - let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; + let workspace; + let target = if let Some(script) = script.as_ref() { + LockTarget::Script(script) + } else { + workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; + LockTarget::Workspace(&workspace) + }; - // Validate that any referenced dependency groups are defined in the workspace. + // Validate that any referenced dependency groups are defined in the target. if !frozen { - let target = DependencyGroupsTarget::Workspace(&workspace); + let target = match &target { + LockTarget::Workspace(workspace) => DependencyGroupsTarget::Workspace(workspace), + LockTarget::Script(..) => DependencyGroupsTarget::Script, + }; target.validate(&dev)?; } // Determine the default groups to include. - let defaults = default_dependency_groups(workspace.pyproject_toml())?; + let defaults = match target { + LockTarget::Workspace(workspace) => default_dependency_groups(workspace.pyproject_toml())?, + LockTarget::Script(_) => vec![], + }; // Find an interpreter for the project, unless `--frozen` and `--universal` are both set. let interpreter = if frozen && universal { None } else { - Some( - ProjectInterpreter::discover( - &workspace, + Some(match target { + LockTarget::Script(script) => ScriptInterpreter::discover( + Pep723ItemRef::Script(script), + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(), + LockTarget::Workspace(workspace) => ProjectInterpreter::discover( + workspace, project_dir, python.as_deref().map(PythonRequest::parse), python_preference, @@ -93,7 +124,7 @@ pub(crate) async fn tree( ) .await? .into_interpreter(), - ) + }) }; // Determine the lock mode. @@ -101,6 +132,9 @@ pub(crate) async fn tree( LockMode::Frozen } else if locked { LockMode::Locked(interpreter.as_ref().unwrap()) + } else if matches!(target, LockTarget::Script(_)) && !target.lock_path().is_file() { + // If we're locking a script, avoid creating a lockfile if it doesn't already exist. + LockMode::DryRun(interpreter.as_ref().unwrap()) } else { LockMode::Write(interpreter.as_ref().unwrap()) }; @@ -111,7 +145,7 @@ pub(crate) async fn tree( // Update the lockfile, if necessary. let lock = match do_safe_lock( mode, - (&workspace).into(), + target, settings.as_ref(), LowerBound::Allow, &state, @@ -151,7 +185,7 @@ pub(crate) async fn tree( .packages() .iter() .filter_map(|package| { - let index = match package.index(workspace.install_path()) { + let index = match package.index(target.install_path()) { Ok(Some(index)) => index, Ok(None) => return None, Err(err) => return Some(Err(err)), diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 66ca098ffcc5..e51c2afb4b5b 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -191,6 +191,12 @@ async fn run(mut cli: Cli) -> Result { script: Some(script), .. }) = &**command + { + Pep723Script::read(&script).await?.map(Pep723Item::Script) + } else if let ProjectCommand::Tree(uv_cli::TreeArgs { + script: Some(script), + .. + }) = &**command { Pep723Script::read(&script).await?.map(Pep723Item::Script) } else { @@ -1652,7 +1658,14 @@ async fn run_project( // Initialize the cache. let cache = cache.init()?; - commands::tree( + // Unwrap the script. + let script = script.map(|script| match script { + Pep723Item::Script(script) => script, + Pep723Item::Stdin(_) => unreachable!("`uv tree` does not support stdin"), + Pep723Item::Remote(_) => unreachable!("`uv tree` does not support remote files"), + }); + + Box::pin(commands::tree( project_dir, args.dev, args.locked, @@ -1669,6 +1682,7 @@ async fn run_project( args.python, args.install_mirrors, args.resolver, + script, globals.python_preference, globals.python_downloads, globals.connectivity, @@ -1679,7 +1693,7 @@ async fn run_project( &cache, printer, globals.preview, - ) + )) .await } ProjectCommand::Export(args) => { diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index e4d030ceb542..8d6da1557432 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1293,6 +1293,8 @@ pub(crate) struct TreeSettings { pub(crate) no_dedupe: bool, pub(crate) invert: bool, pub(crate) outdated: bool, + #[allow(dead_code)] + pub(crate) script: Option, pub(crate) python_version: Option, pub(crate) python_platform: Option, pub(crate) python: Option, @@ -1317,6 +1319,7 @@ impl TreeSettings { frozen, build, resolver, + script, python_version, python_platform, python, @@ -1339,6 +1342,7 @@ impl TreeSettings { no_dedupe: tree.no_dedupe, invert: tree.invert, outdated: tree.outdated, + script, python_version, python_platform, python: python.and_then(Maybe::into_option), diff --git a/crates/uv/tests/it/tree.rs b/crates/uv/tests/it/tree.rs index 879fe3315569..7e71fc4e2b1e 100644 --- a/crates/uv/tests/it/tree.rs +++ b/crates/uv/tests/it/tree.rs @@ -1,7 +1,8 @@ use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; -use indoc::formatdoc; +use indoc::{formatdoc, indoc}; +use insta::assert_snapshot; use url::Url; use crate::common::{uv_snapshot, TestContext}; @@ -1172,3 +1173,400 @@ fn non_project_member() -> Result<()> { Ok(()) } + +#[test] +fn script() -> Result<()> { + let context = TestContext::new("3.12"); + + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "requests<3", + # "rich", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "#})?; + + uv_snapshot!(context.filters(), context.tree().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + rich v13.7.1 + ├── markdown-it-py v3.0.0 + │ └── mdurl v0.1.2 + └── pygments v2.17.2 + requests v2.31.0 + ├── certifi v2024.2.2 + ├── charset-normalizer v3.3.2 + ├── idna v3.6 + └── urllib3 v2.2.1 + + ----- stderr ----- + Resolved 9 packages in [TIME] + "###); + + // If the lockfile didn't exist already, it shouldn't be persisted to disk. + assert!(!context.temp_dir.child("uv.lock").exists()); + + // Explicitly lock the script. + uv_snapshot!(context.filters(), context.lock().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + "###); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "requests", specifier = "<3" }, + { name = "rich" }, + ] + + [[package]] + name = "certifi" + version = "2024.2.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774 }, + ] + + [[package]] + name = "charset-normalizer" + version = "3.3.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "markdown-it-py" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "mdurl" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + ] + + [[package]] + name = "mdurl" + version = "0.1.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + ] + + [[package]] + name = "pygments" + version = "2.17.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 }, + ] + + [[package]] + name = "requests" + version = "2.31.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, + ] + + [[package]] + name = "rich" + version = "13.7.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, + ] + + [[package]] + name = "urllib3" + version = "2.2.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067 }, + ] + "### + ); + }); + + // Update the dependencies. + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "iniconfig", + # "requests<3", + # "rich", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "#})?; + + // `uv tree` should update the lockfile. + uv_snapshot!(context.filters(), context.tree().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + rich v13.7.1 + ├── markdown-it-py v3.0.0 + │ └── mdurl v0.1.2 + └── pygments v2.17.2 + requests v2.31.0 + ├── certifi v2024.2.2 + ├── charset-normalizer v3.3.2 + ├── idna v3.6 + └── urllib3 v2.2.1 + iniconfig v2.0.0 + + ----- stderr ----- + Resolved 10 packages in [TIME] + "###); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "iniconfig" }, + { name = "requests", specifier = "<3" }, + { name = "rich" }, + ] + + [[package]] + name = "certifi" + version = "2024.2.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774 }, + ] + + [[package]] + name = "charset-normalizer" + version = "3.3.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "markdown-it-py" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "mdurl" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + ] + + [[package]] + name = "mdurl" + version = "0.1.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + ] + + [[package]] + name = "pygments" + version = "2.17.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 }, + ] + + [[package]] + name = "requests" + version = "2.31.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, + ] + + [[package]] + name = "rich" + version = "13.7.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, + ] + + [[package]] + name = "urllib3" + version = "2.2.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067 }, + ] + "### + ); + }); + + Ok(()) +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index b673ce64bc3b..e8efbbea0cbf 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2959,6 +2959,10 @@ uv tree [OPTIONS]
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
    --script script

    Show the dependency tree the specified PEP 723 Python script, rather than the current project.

    + +

    If provided, uv will resolve the dependencies based on its inline metadata table, in adherence with PEP 723.

    +
    --universal

    Show a platform-independent dependency tree.

    Shows resolved package versions for all Python versions and platforms, rather than filtering to those that are relevant for the current environment.

    From 18b53c5b45459d1bba135bfe035b1ed148a835cc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 16:48:53 -0500 Subject: [PATCH 076/135] Add `--script` support to `uv export` for PEP 723 scripts (#10160) ## Summary You can now run `uv export --script main.py` to show the dependency tree for a given script. If a lockfile doesn't exist, it will create one. Closes https://github.com/astral-sh/uv/issues/8609. Closes https://github.com/astral-sh/uv/issues/9657. --- crates/uv-cli/src/lib.rs | 8 + crates/uv/src/commands/project/add.rs | 4 +- crates/uv/src/commands/project/export.rs | 161 +++++++++----- crates/uv/src/lib.rs | 14 ++ crates/uv/src/settings.rs | 3 + crates/uv/tests/it/export.rs | 259 +++++++++++++++++++++++ docs/reference/cli.md | 4 + 7 files changed, 400 insertions(+), 53 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 1ec02fa391d5..8c0dc6f8cee9 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3651,6 +3651,14 @@ pub struct ExportArgs { #[command(flatten)] pub refresh: RefreshArgs, + /// Export the dependencies for the specified PEP 723 Python script, rather than the current + /// project. + /// + /// If provided, uv will resolve the dependencies based on its inline metadata table, in + /// adherence with PEP 723. + #[arg(long, conflicts_with_all = ["all_packages", "package", "no_emit_project", "no_emit_workspace"])] + pub script: Option, + /// The Python interpreter to use during resolution. /// /// A Python interpreter is required for building source distributions to diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 99378e28074c..950e7470af52 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1003,8 +1003,8 @@ enum AddTarget { impl<'lock> From<&'lock AddTarget> for LockTarget<'lock> { fn from(value: &'lock AddTarget) -> Self { match value { - AddTarget::Script(script, _) => LockTarget::Script(script), - AddTarget::Project(project, _) => LockTarget::Workspace(project.workspace()), + AddTarget::Script(script, _) => Self::Script(script), + AddTarget::Project(project, _) => Self::Workspace(project.workspace()), } } } diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index eb1d0ebe3bb8..64b70d466915 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -16,19 +16,39 @@ use uv_dispatch::SharedState; use uv_normalize::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_resolver::RequirementsTxtExport; +use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::{do_safe_lock, LockMode}; +use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ default_dependency_groups, detect_conflicts, DependencyGroupsTarget, ProjectError, - ProjectInterpreter, + ProjectInterpreter, ScriptInterpreter, }; use crate::commands::{diagnostics, ExitStatus, OutputWriter}; use crate::printer::Printer; use crate::settings::ResolverSettings; +#[derive(Debug, Clone)] +enum ExportTarget { + /// A PEP 723 script, with inline metadata. + Script(Pep723Script), + + /// A project with a `pyproject.toml`. + Project(VirtualProject), +} + +impl<'lock> From<&'lock ExportTarget> for LockTarget<'lock> { + fn from(value: &'lock ExportTarget) -> Self { + match value { + ExportTarget::Script(script) => Self::Script(script), + ExportTarget::Project(project) => Self::Workspace(project.workspace()), + } + } +} + /// Export the project's `uv.lock` in an alternate format. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn export( @@ -46,6 +66,7 @@ pub(crate) async fn export( locked: bool, frozen: bool, include_header: bool, + script: Option, python: Option, install_mirrors: PythonInstallMirrors, settings: ResolverSettings, @@ -61,74 +82,108 @@ pub(crate) async fn export( printer: Printer, preview: PreviewMode, ) -> Result { - // Identify the project. - let project = if frozen { - VirtualProject::discover( - project_dir, - &DiscoveryOptions { - members: MemberDiscovery::None, - ..DiscoveryOptions::default() - }, - ) - .await? - } else if let Some(package) = package.as_ref() { - VirtualProject::Project( - Workspace::discover(project_dir, &DiscoveryOptions::default()) - .await? - .with_current_project(package.clone()) - .with_context(|| format!("Package `{package}` not found in workspace"))?, - ) + // Identify the target. + let target = if let Some(script) = script { + ExportTarget::Script(script) } else { - VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? + let project = if frozen { + VirtualProject::discover( + project_dir, + &DiscoveryOptions { + members: MemberDiscovery::None, + ..DiscoveryOptions::default() + }, + ) + .await? + } else if let Some(package) = package.as_ref() { + VirtualProject::Project( + Workspace::discover(project_dir, &DiscoveryOptions::default()) + .await? + .with_current_project(package.clone()) + .with_context(|| format!("Package `{package}` not found in workspace"))?, + ) + } else { + VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? + }; + ExportTarget::Project(project) }; // Validate that any referenced dependency groups are defined in the workspace. if !frozen { - let target = match &project { - VirtualProject::Project(project) => { + let target = match &target { + ExportTarget::Project(VirtualProject::Project(project)) => { if all_packages { DependencyGroupsTarget::Workspace(project.workspace()) } else { DependencyGroupsTarget::Project(project) } } - VirtualProject::NonProject(workspace) => DependencyGroupsTarget::Workspace(workspace), + ExportTarget::Project(VirtualProject::NonProject(workspace)) => { + DependencyGroupsTarget::Workspace(workspace) + } + ExportTarget::Script(_) => DependencyGroupsTarget::Script, }; target.validate(&dev)?; } // Determine the default groups to include. - let defaults = default_dependency_groups(project.pyproject_toml())?; + let defaults = match &target { + ExportTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?, + ExportTarget::Script(_) => vec![], + }; let dev = dev.with_defaults(defaults); + // Find an interpreter for the project, unless `--frozen` is set. + let interpreter = if frozen { + None + } else { + Some(match &target { + ExportTarget::Script(script) => ScriptInterpreter::discover( + Pep723ItemRef::Script(script), + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(), + ExportTarget::Project(project) => ProjectInterpreter::discover( + project.workspace(), + project_dir, + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(), + }) + }; + // Determine the lock mode. - let interpreter; let mode = if frozen { LockMode::Frozen + } else if locked { + LockMode::Locked(interpreter.as_ref().unwrap()) + } else if matches!(target, ExportTarget::Script(_)) + && !LockTarget::from(&target).lock_path().is_file() + { + // If we're locking a script, avoid creating a lockfile if it doesn't already exist. + LockMode::DryRun(interpreter.as_ref().unwrap()) } else { - // Find an interpreter for the project - interpreter = ProjectInterpreter::discover( - project.workspace(), - project_dir, - python.as_deref().map(PythonRequest::parse), - python_preference, - python_downloads, - connectivity, - native_tls, - allow_insecure_host, - &install_mirrors, - no_config, - cache, - printer, - ) - .await? - .into_interpreter(); - - if locked { - LockMode::Locked(&interpreter) - } else { - LockMode::Write(&interpreter) - } + LockMode::Write(interpreter.as_ref().unwrap()) }; // Initialize any shared state. @@ -137,7 +192,7 @@ pub(crate) async fn export( // Lock the project. let lock = match do_safe_lock( mode, - project.workspace().into(), + (&target).into(), settings.as_ref(), LowerBound::Warn, &state, @@ -165,8 +220,8 @@ pub(crate) async fn export( detect_conflicts(&lock, &extras, &dev)?; // Identify the installation target. - let target = match &project { - VirtualProject::Project(project) => { + let target = match &target { + ExportTarget::Project(VirtualProject::Project(project)) => { if all_packages { InstallTarget::Workspace { workspace: project.workspace(), @@ -187,7 +242,7 @@ pub(crate) async fn export( } } } - VirtualProject::NonProject(workspace) => { + ExportTarget::Project(VirtualProject::NonProject(workspace)) => { if all_packages { InstallTarget::NonProjectWorkspace { workspace, @@ -207,6 +262,10 @@ pub(crate) async fn export( } } } + ExportTarget::Script(script) => InstallTarget::Script { + script, + lock: &lock, + }, }; // Write the resolved dependencies to the output channel. diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e51c2afb4b5b..69d4e6f414a3 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -197,6 +197,12 @@ async fn run(mut cli: Cli) -> Result { script: Some(script), .. }) = &**command + { + Pep723Script::read(&script).await?.map(Pep723Item::Script) + } else if let ProjectCommand::Export(uv_cli::ExportArgs { + script: Some(script), + .. + }) = &**command { Pep723Script::read(&script).await?.map(Pep723Item::Script) } else { @@ -1704,6 +1710,13 @@ async fn run_project( // Initialize the cache. let cache = cache.init()?; + // Unwrap the script. + let script = script.map(|script| match script { + Pep723Item::Script(script) => script, + Pep723Item::Stdin(_) => unreachable!("`uv export` does not support stdin"), + Pep723Item::Remote(_) => unreachable!("`uv export` does not support remote files"), + }); + commands::export( project_dir, args.format, @@ -1719,6 +1732,7 @@ async fn run_project( args.locked, args.frozen, args.include_header, + script, args.python, args.install_mirrors, args.settings, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 8d6da1557432..1e282657dd31 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1369,6 +1369,7 @@ pub(crate) struct ExportSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) include_header: bool, + pub(crate) script: Option, pub(crate) python: Option, pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, @@ -1409,6 +1410,7 @@ impl ExportSettings { resolver, build, refresh, + script, python, } = args; let install_mirrors = filesystem @@ -1440,6 +1442,7 @@ impl ExportSettings { locked, frozen, include_header: flag(header, no_header).unwrap_or(true), + script, python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 19e7a388d937..aedf471d36f7 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4,6 +4,8 @@ use crate::common::{apply_filters, uv_snapshot, TestContext}; use anyhow::{Ok, Result}; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; +use indoc::indoc; +use insta::assert_snapshot; use std::process::Stdio; #[test] @@ -2054,6 +2056,263 @@ fn export_group() -> Result<()> { Ok(()) } +#[test] +fn script() -> Result<()> { + let context = TestContext::new("3.12"); + + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "anyio==2.0.0 ; sys_platform == 'win32'", + # "anyio==3.0.0 ; sys_platform == 'linux'" + # ] + # /// + "#})?; + + uv_snapshot!(context.filters(), context.export().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --script [TEMP_DIR]/script.py + anyio==2.0.0 ; sys_platform == 'win32' \ + --hash=sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547 \ + --hash=sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826 + anyio==3.0.0 ; sys_platform == 'linux' \ + --hash=sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9 \ + --hash=sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395 + idna==3.6 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + sniffio==1.3.1 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // If the lockfile didn't exist already, it shouldn't be persisted to disk. + assert!(!context.temp_dir.child("uv.lock").exists()); + + // Explicitly lock the script. + uv_snapshot!(context.filters(), context.lock().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'linux'", + "sys_platform != 'linux' and sys_platform != 'win32'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "anyio", marker = "sys_platform == 'linux'", specifier = "==3.0.0" }, + { name = "anyio", marker = "sys_platform == 'win32'", specifier = "==2.0.0" }, + ] + + [[package]] + name = "anyio" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'win32'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/fe/dc/daeadb9b34093d3968afcc93946ee567cd6d2b402a96c608cb160f74d737/anyio-2.0.0.tar.gz", hash = "sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826", size = 91291 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/19/10fe682e962efd1610aa41376399fc3f3e002425449b02d0fb04749bb712/anyio-2.0.0-py3-none-any.whl", hash = "sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547", size = 62675 }, + ] + + [[package]] + name = "anyio" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'linux'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "sniffio", marker = "sys_platform == 'linux'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/99/0d/65165f99e5f4f3b4c43a5ed9db0fb7aa655f5a58f290727a30528a87eb45/anyio-3.0.0.tar.gz", hash = "sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9", size = 116952 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/49/ebee263b69fe243bd1fd0a88bc6bb0f7732bf1794ba3273cb446351f9482/anyio-3.0.0-py3-none-any.whl", hash = "sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395", size = 72182 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Update the dependencies. + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "anyio==2.0.0 ; sys_platform == 'win32'", + # "anyio==3.0.0 ; sys_platform == 'linux'", + # "iniconfig", + # ] + # /// + "#})?; + + // `uv tree` should update the lockfile. + uv_snapshot!(context.filters(), context.export().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --script [TEMP_DIR]/script.py + anyio==2.0.0 ; sys_platform == 'win32' \ + --hash=sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547 \ + --hash=sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826 + anyio==3.0.0 ; sys_platform == 'linux' \ + --hash=sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9 \ + --hash=sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395 + idna==3.6 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + sniffio==1.3.1 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + + ----- stderr ----- + Resolved 5 packages in [TIME] + "###); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'linux'", + "sys_platform != 'linux' and sys_platform != 'win32'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "anyio", marker = "sys_platform == 'linux'", specifier = "==3.0.0" }, + { name = "anyio", marker = "sys_platform == 'win32'", specifier = "==2.0.0" }, + { name = "iniconfig" }, + ] + + [[package]] + name = "anyio" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'win32'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/fe/dc/daeadb9b34093d3968afcc93946ee567cd6d2b402a96c608cb160f74d737/anyio-2.0.0.tar.gz", hash = "sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826", size = 91291 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/19/10fe682e962efd1610aa41376399fc3f3e002425449b02d0fb04749bb712/anyio-2.0.0-py3-none-any.whl", hash = "sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547", size = 62675 }, + ] + + [[package]] + name = "anyio" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'linux'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "sniffio", marker = "sys_platform == 'linux'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/99/0d/65165f99e5f4f3b4c43a5ed9db0fb7aa655f5a58f290727a30528a87eb45/anyio-3.0.0.tar.gz", hash = "sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9", size = 116952 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/49/ebee263b69fe243bd1fd0a88bc6bb0f7732bf1794ba3273cb446351f9482/anyio-3.0.0-py3-none-any.whl", hash = "sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395", size = 72182 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +} + #[test] fn conflicts() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index e8efbbea0cbf..ed6db02acf7b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2532,6 +2532,10 @@ uv export [OPTIONS]
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
    --script script

    Export the dependencies for the specified PEP 723 Python script, rather than the current project.

    + +

    If provided, uv will resolve the dependencies based on its inline metadata table, in adherence with PEP 723.

    +
    --upgrade, -U

    Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

    --upgrade-package, -P upgrade-package

    Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package

    From 53dd554919f3c570ed15b1697ff78bb8aa4f5032 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 17:05:37 -0500 Subject: [PATCH 077/135] Update PEP 723 lockfile in `uv remove --script` (#10162) ## Summary Counterpart to #10145 covering `uv remove` for PEP 723 scripts with lockfiles. --- crates/uv/src/commands/project/add.rs | 40 ++--- crates/uv/src/commands/project/remove.rs | 198 +++++++++++++++++------ crates/uv/tests/it/edit.rs | 191 +++++++++++++++++++++- 3 files changed, 357 insertions(+), 72 deletions(-) diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 950e7470af52..227bf676faed 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -990,9 +990,27 @@ fn resolve_requirement( Ok((processed_requirement, source)) } +/// A Python [`Interpreter`] or [`PythonEnvironment`] for a project. +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub(super) enum PythonTarget { + Interpreter(Interpreter), + Environment(PythonEnvironment), +} + +impl PythonTarget { + /// Return the [`Interpreter`] for the project. + fn interpreter(&self) -> &Interpreter { + match self { + Self::Interpreter(interpreter) => interpreter, + Self::Environment(venv) => venv.interpreter(), + } + } +} + /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug, Clone)] -enum AddTarget { +pub(super) enum AddTarget { /// A PEP 723 script, with inline metadata. Script(Pep723Script, Box), @@ -1011,7 +1029,7 @@ impl<'lock> From<&'lock AddTarget> for LockTarget<'lock> { impl AddTarget { /// Returns the [`Interpreter`] for the target. - fn interpreter(&self) -> &Interpreter { + pub(super) fn interpreter(&self) -> &Interpreter { match self { Self::Script(_, interpreter) => interpreter, Self::Project(_, venv) => venv.interpreter(), @@ -1131,24 +1149,6 @@ impl AddTargetSnapshot { } } -/// A Python [`Interpreter`] or [`PythonEnvironment`] for a project. -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -enum PythonTarget { - Interpreter(Interpreter), - Environment(PythonEnvironment), -} - -impl PythonTarget { - /// Return the [`Interpreter`] for the project. - fn interpreter(&self) -> &Interpreter { - match self { - Self::Interpreter(interpreter) => interpreter, - Self::Environment(venv) => venv.interpreter(), - } - } -} - #[derive(Debug, Clone)] struct DependencyEdit { dependency_type: DependencyType, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 415115aa358a..6c6243410db1 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -1,8 +1,11 @@ use std::fmt::Write; +use std::io; use std::path::Path; +use std::str::FromStr; use anyhow::{Context, Result}; use owo_colors::OwoColorize; +use tracing::debug; use uv_cache::Cache; use uv_client::Connectivity; @@ -15,7 +18,7 @@ use uv_fs::Simplified; use uv_normalize::DEV_DEPENDENCIES; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; -use uv_scripts::Pep723Script; +use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_warnings::warn_user_once; use uv_workspace::pyproject::DependencyType; @@ -24,9 +27,13 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::Modifications; +use crate::commands::project::add::{AddTarget, PythonTarget}; use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; -use crate::commands::project::{default_dependency_groups, ProjectError}; +use crate::commands::project::lock_target::LockTarget; +use crate::commands::project::{ + default_dependency_groups, ProjectError, ProjectInterpreter, ScriptInterpreter, +}; use crate::commands::{diagnostics, project, ExitStatus}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -79,7 +86,7 @@ pub(crate) async fn remove( "`--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation" ); } - Target::Script(script) + RemoveTarget::Script(script) } else { // Find the project in the workspace. let project = if let Some(package) = package { @@ -93,14 +100,14 @@ pub(crate) async fn remove( VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? }; - Target::Project(project) + RemoveTarget::Project(project) }; let mut toml = match &target { - Target::Script(script) => { + RemoveTarget::Script(script) => { PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script) } - Target::Project(project) => PyProjectTomlMut::from_toml( + RemoveTarget::Project(project) => PyProjectTomlMut::from_toml( project.pyproject_toml().raw.as_ref(), DependencyTarget::PyProjectToml, ), @@ -161,27 +168,20 @@ pub(crate) async fn remove( } } - // Save the modified dependencies. - match &target { - Target::Script(script) => { - script.write(&toml.to_string())?; - } - Target::Project(project) => { - let pyproject_path = project.root().join("pyproject.toml"); - fs_err::write(pyproject_path, toml.to_string())?; - } - }; + let content = toml.to_string(); + + // Save the modified `pyproject.toml` or script. + target.write(&content)?; - // If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock` + // If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock` // to exist at all. if frozen { return Ok(ExitStatus::Success); } - let project = match target { - Target::Project(project) => project, - // If `--script`, exit early. There's no reason to lock and sync. - Target::Script(script) => { + // If we're modifying a script, and lockfile doesn't exist, don't create it. + if let RemoveTarget::Script(ref script) = target { + if !LockTarget::from(script).lock_path().is_file() { writeln!( printer.stderr(), "Updated `{}`", @@ -189,31 +189,80 @@ pub(crate) async fn remove( )?; return Ok(ExitStatus::Success); } - }; + } - // Discover or create the virtual environment. - let venv = project::get_or_init_environment( - project.workspace(), - python.as_deref().map(PythonRequest::parse), - &install_mirrors, - python_preference, - python_downloads, - connectivity, - native_tls, - allow_insecure_host, - no_config, - cache, - printer, - ) - .await?; + // Update the `pypackage.toml` in-memory. + let target = target.update(&content)?; + + // Convert to an `AddTarget` by attaching the appropriate interpreter or environment. + let target = match target { + RemoveTarget::Project(project) => { + if no_sync { + // Discover the interpreter. + let interpreter = ProjectInterpreter::discover( + project.workspace(), + project_dir, + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(); + + AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter))) + } else { + // Discover or create the virtual environment. + let venv = project::get_or_init_environment( + project.workspace(), + python.as_deref().map(PythonRequest::parse), + &install_mirrors, + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + no_config, + cache, + printer, + ) + .await?; + + AddTarget::Project(project, Box::new(PythonTarget::Environment(venv))) + } + } + RemoveTarget::Script(script) => { + let interpreter = ScriptInterpreter::discover( + Pep723ItemRef::Script(&script), + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(); + + AddTarget::Script(script, Box::new(interpreter)) + } + }; // Determine the lock mode. - let mode = if frozen { - LockMode::Frozen - } else if locked { - LockMode::Locked(venv.interpreter()) + let mode = if locked { + LockMode::Locked(target.interpreter()) } else { - LockMode::Write(venv.interpreter()) + LockMode::Write(target.interpreter()) }; // Initialize any shared state. @@ -222,7 +271,7 @@ pub(crate) async fn remove( // Lock and sync the environment, if necessary. let lock = match project::lock::do_safe_lock( mode, - project.workspace().into(), + (&target).into(), settings.as_ref().into(), LowerBound::Allow, &state, @@ -246,9 +295,15 @@ pub(crate) async fn remove( Err(err) => return Err(err.into()), }; - if no_sync { + let AddTarget::Project(project, environment) = target else { + // If we're not adding to a project, exit early. return Ok(ExitStatus::Success); - } + }; + + let PythonTarget::Environment(venv) = &*environment else { + // If we're not syncing, exit early. + return Ok(ExitStatus::Success); + }; // Perform a full sync, because we don't know what exactly is affected by the removal. // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? @@ -273,7 +328,7 @@ pub(crate) async fn remove( match project::sync::do_sync( target, - &venv, + venv, &extras, &DevGroupsManifest::from_defaults(defaults), EditableMode::Editable, @@ -306,13 +361,62 @@ pub(crate) async fn remove( /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug)] -enum Target { +enum RemoveTarget { /// A PEP 723 script, with inline metadata. Project(VirtualProject), /// A project with a `pyproject.toml`. Script(Pep723Script), } +impl RemoveTarget { + /// Write the updated content to the target. + /// + /// Returns `true` if the content was modified. + fn write(&self, content: &str) -> Result { + match self { + Self::Script(script) => { + if content == script.metadata.raw { + debug!("No changes to dependencies; skipping update"); + Ok(false) + } else { + script.write(content)?; + Ok(true) + } + } + Self::Project(project) => { + if content == project.pyproject_toml().raw { + debug!("No changes to dependencies; skipping update"); + Ok(false) + } else { + let pyproject_path = project.root().join("pyproject.toml"); + fs_err::write(pyproject_path, content)?; + Ok(true) + } + } + } + } + + /// Update the target in-memory to incorporate the new content. + #[allow(clippy::result_large_err)] + fn update(self, content: &str) -> Result { + match self { + Self::Script(mut script) => { + script.metadata = Pep723Metadata::from_str(content) + .map_err(ProjectError::Pep723ScriptTomlParse)?; + Ok(Self::Script(script)) + } + Self::Project(project) => { + let project = project + .with_pyproject_toml( + toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?, + ) + .ok_or(ProjectError::PyprojectTomlUpdate)?; + Ok(Self::Project(project)) + } + } + } +} + /// Show a hint if a dependency with the given name is present as any dependency type. /// /// This is useful when a dependency of the user-specified type was not found, but it may be present diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index fc5942223606..e3e1d8b39012 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -5334,9 +5334,9 @@ fn remove_repeated() -> Result<()> { Ok(()) } -/// Add to a PEP 732 script with a lockfile. +/// Add to (and remove from) a PEP 732 script with a lockfile. #[test] -fn add_script_lock() -> Result<()> { +fn add_remove_script_lock() -> Result<()> { let context = TestContext::new("3.12"); let script = context.temp_dir.child("script.py"); @@ -5718,10 +5718,191 @@ fn add_script_lock() -> Result<()> { ); }); + // Removing from a locked script should update the lockfile. + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--script").arg("script.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + "###); + + let script_content = context.read("script.py"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + script_content, @r###" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "requests<3", + # "rich", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "### + ); + }); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "requests", specifier = "<3" }, + { name = "rich" }, + ] + + [[package]] + name = "certifi" + version = "2024.2.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774 }, + ] + + [[package]] + name = "charset-normalizer" + version = "3.3.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "markdown-it-py" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "mdurl" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + ] + + [[package]] + name = "mdurl" + version = "0.1.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + ] + + [[package]] + name = "pygments" + version = "2.17.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 }, + ] + + [[package]] + name = "requests" + version = "2.31.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, + ] + + [[package]] + name = "rich" + version = "13.7.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, + ] + + [[package]] + name = "urllib3" + version = "2.2.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067 }, + ] + "### + ); + }); + Ok(()) } -/// Remove from a PEP732 script, +/// Remove from a PEP 723 script. #[test] fn remove_script() -> Result<()> { let context = TestContext::new("3.12"); @@ -5781,7 +5962,7 @@ fn remove_script() -> Result<()> { Ok(()) } -/// Remove last dependency PEP732 script +/// Remove last dependency PEP 723 script #[test] fn remove_last_dep_script() -> Result<()> { let context = TestContext::new("3.12"); @@ -5836,7 +6017,7 @@ fn remove_last_dep_script() -> Result<()> { Ok(()) } -/// Add a Git requirement to PEP732 script. +/// Add a Git requirement to PEP 723 script. #[test] #[cfg(feature = "git")] fn add_git_to_script() -> Result<()> { From 2e0d7429ef3d31fa12f57dcef2dcdf9a6114ba31 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 17:12:52 -0500 Subject: [PATCH 078/135] Add Lambda layer instructions to AWS Lambda guide (#10411) ## Summary Closes https://github.com/astral-sh/uv/issues/10406. --- docs/guides/integration/aws-lambda.md | 114 ++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index d8cd7983bb94..11ed1d7ea50a 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -1,3 +1,10 @@ +--- +title: Using uv with AWS Lambda +description: + A complete guide to using uv with AWS Lambda to manage Python dependencies and deploy serverless + functions via Docker containers or zip archives. +--- + # Using uv with AWS Lambda [AWS Lambda](https://aws.amazon.com/lambda/) is a serverless computing service that lets you run @@ -90,9 +97,12 @@ FROM ghcr.io/astral-sh/uv:0.5.16 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder -# Enable bytecode compilation. +# Enable bytecode compilation, to improve cold-start performance. ENV UV_COMPILE_BYTECODE=1 +# Disable installer metadata, to create a deterministic layer. +ENV UV_NO_INSTALLER_METADATA=1 + # Enable copy mode to support bind mount caching. ENV UV_LINK_MODE=copy @@ -289,9 +299,12 @@ FROM ghcr.io/astral-sh/uv:0.5.16 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder -# Enable bytecode compilation. +# Enable bytecode compilation, to improve cold-start performance. ENV UV_COMPILE_BYTECODE=1 +# Disable installer metadata, to create a deterministic layer. +ENV UV_NO_INSTALLER_METADATA=1 + # Enable copy mode to support bind mount caching. ENV UV_LINK_MODE=copy @@ -352,9 +365,10 @@ for AWS Lambda via: ```console $ uv export --frozen --no-dev --no-editable -o requirements.txt $ uv pip install \ + --no-installer-metadata \ --no-compile-bytecode \ --python-platform x86_64-manylinux2014 \ - --python-version 3.13 \ + --python 3.13 \ --target packages \ -r requirements.txt ``` @@ -370,12 +384,12 @@ then bundle these dependencies into a zip as follows: ```console $ cd packages $ zip -r ../package.zip . +$ cd .. ``` Finally, we can add the application code to the zip archive: ```console -$ cd .. $ zip -r package.zip app ``` @@ -386,7 +400,7 @@ e.g.: $ aws lambda create-function \ --function-name myFunction \ --runtime python3.13 \ - --zip-file fileb://package.zip + --zip-file fileb://package.zip \ --handler app.main.handler \ --role arn:aws:iam::111122223333:role/service-role/my-lambda-role ``` @@ -414,3 +428,93 @@ $ aws lambda update-function-code \ By default, the AWS Management Console assumes a Lambda entrypoint of `lambda_function.lambda_handler`. If your application uses a different entrypoint, you'll need to modify it in the AWS Management Console. For example, the above FastAPI application uses `app.main.handler`. + +### Using a Lambda layer + +AWS Lambda also supports the deployment of multiple composed +[Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/python-layers.html) when working with +zip archives. These layers are conceptually similar to layers in a Docker image, allowing you to +separate application code from dependencies. + +In particular, we can create a lambda layer for application dependencies and attach it to the Lambda +function, separate from the application code itself. This setup can improve cold-start performance +for application updates, as the dependencies layer can be reused across deployments. + +To create a Lambda layer, we'll follow similar steps, but create two separate zip archives: one for +the application code and one for the application dependencies. + +First, we'll create the dependency layer. Lambda layers are expected to follow a slightly different +structure, so we'll use `--prefix` rather than `--target`: + +```console +$ uv export --frozen --no-dev --no-editable -o requirements.txt +$ uv pip install \ + --no-installer-metadata \ + --no-compile-bytecode \ + --python-platform x86_64-manylinux2014 \ + --python 3.13 \ + --prefix packages \ + -r requirements.txt +``` + +We'll then zip the dependencies in adherence with the expected layout for Lambda layers: + +```console +$ mkdir python +$ cp -r packages/lib python/ +$ zip -r layer_content.zip python +``` + +!!! tip + + To generate deterministic zip archives, consider passing the `-X` flag to `zip` to exclude + extended attributes and file system metadata. + +And publish the Lambda layer: + +```console +$ aws lambda publish-layer-version --layer-name dependencies-layer \ + --zip-file fileb://layer_content.zip \ + --compatible-runtimes python3.13 \ + --compatible-architectures "x86_64" +``` + +We can then create the Lambda function as in the previous example, omitting the dependencies: + +```console +$ # Zip the application code. +$ zip -r app.zip app + +$ # Create the Lambda function. +$ aws lambda create-function \ + --function-name myFunction \ + --runtime python3.13 \ + --zip-file fileb://app.zip \ + --handler app.main.handler \ + --role arn:aws:iam::111122223333:role/service-role/my-lambda-role +``` + +Finally, we can attach the dependencies layer to the Lambda function, using the ARN returned by the +`publish-layer-version` step: + +```console +$ aws lambda update-function-configuration --function-name myFunction \ + --cli-binary-format raw-in-base64-out \ + --layers "arn:aws:lambda:region:111122223333:layer:dependencies-layer:1" +``` + +When the application dependencies change, the layer can be updated independently of the application +by republishing the layer and updating the Lambda function configuration: + +```console +$ # Update the dependencies in the layer. +$ aws lambda publish-layer-version --layer-name dependencies-layer \ + --zip-file fileb://layer_content.zip \ + --compatible-runtimes python3.13 \ + --compatible-architectures "x86_64" + +$ # Update the Lambda function configuration. +$ aws lambda update-function-configuration --function-name myFunction \ + --cli-binary-format raw-in-base64-out \ + --layers "arn:aws:lambda:region:111122223333:layer:dependencies-layer:2" +``` From f65fcf23b3c01e08a0dee772c01d38c3f44848e2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 18:06:09 -0500 Subject: [PATCH 079/135] Remove duplicated comment (#10416) --- crates/uv-resolver/src/resolver/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 2b3e4f69613f..3bc26c62d36d 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1039,7 +1039,6 @@ impl ResolverState { // Dependencies on Python are only added when a package is incompatible; as such, // we don't need to do anything here. - // we don't need to do anything here. Ok(None) } From 5551f9f3da0e74cdb790d78a1ed97545141001ab Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 18:59:13 -0500 Subject: [PATCH 080/135] Remove unnecessary `.to_string()` call (#10419) --- crates/uv-platform-tags/src/tags.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 6cfe3709db8d..9dc99c618af1 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -83,11 +83,11 @@ impl Tags { pub fn new(tags: Vec<(String, String, String)>) -> Self { let mut map = FxHashMap::default(); for (index, (py, abi, platform)) in tags.into_iter().rev().enumerate() { - map.entry(py.to_string()) + map.entry(py) .or_insert(FxHashMap::default()) - .entry(abi.to_string()) + .entry(abi) .or_insert(FxHashMap::default()) - .entry(platform.to_string()) + .entry(platform) .or_insert(TagPriority::try_from(index).expect("valid tag priority")); } Self { map: Arc::new(map) } From 58a81f7c4700ed79bb5870334fe0b52198f91973 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 19:33:55 -0500 Subject: [PATCH 081/135] Add `uv lock --script` to the docs (#10414) --- docs/guides/scripts.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/guides/scripts.md b/docs/guides/scripts.md index 0696af57ac4e..c6d10af63fde 100644 --- a/docs/guides/scripts.md +++ b/docs/guides/scripts.md @@ -210,11 +210,30 @@ print(Point) is not installed — see the documentation on [Python versions](../concepts/python-versions.md) for more details. +## Locking dependencies + +uv supports locking dependencies for PEP 723 scripts using the `uv.lock` file format. Unlike with +projects, scripts must be explicitly locked using `uv lock`: + +```console +$ uv lock --script example.py +``` + +Running `uv lock --script` will create a `.lock` file adjacent to the script (e.g., +`example.py.lock`). + +Once locked, subsequent operations like `uv run --script`, `uv add --script`, `uv export --script`, +and `uv tree --script` will reuse the locked dependencies, updating the lockfile if necessary. + +If no such lockfile is present, commands like `uv export --script` will still function as expected, +but will not create a lockfile. + ## Improving reproducibility -uv supports an `exclude-newer` field in the `tool.uv` section of inline script metadata to limit uv -to only considering distributions released before a specific date. This is useful for improving the -reproducibility of your script when run at a later point in time. +In addition to locking dependencies, uv supports an `exclude-newer` field in the `tool.uv` section +of inline script metadata to limit uv to only considering distributions released before a specific +date. This is useful for improving the reproducibility of your script when run at a later point in +time. The date must be specified as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) timestamp (e.g., `2006-12-02T02:07:43Z`). From 15ec830beab1aa7f7adc13b125e8e2122fa78343 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Jan 2025 21:32:09 -0500 Subject: [PATCH 082/135] Use `matches` rather than `contains` in `requirements.txt` parsing (#10423) --- crates/uv-requirements-txt/src/lib.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/uv-requirements-txt/src/lib.rs b/crates/uv-requirements-txt/src/lib.rs index e08c09cf28cd..3c8264d53e9d 100644 --- a/crates/uv-requirements-txt/src/lib.rs +++ b/crates/uv-requirements-txt/src/lib.rs @@ -416,6 +416,11 @@ impl RequirementsTxt { } } +/// Returns `true` if the character is a newline or a comment character. +const fn is_terminal(c: char) -> bool { + matches!(c, '\n' | '\r' | '#') +} + /// Parse a single entry, that is a requirement, an inclusion or a comment line /// /// Consumes all preceding trivia (whitespace and comments). If it returns None, we've reached @@ -436,7 +441,7 @@ fn parse_entry( let start = s.cursor(); Ok(Some(if s.eat_if("-r") || s.eat_if("--requirement") { - let requirements_file = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; + let requirements_file = parse_value(content, s, |c: char| !is_terminal(c))?; let end = s.cursor(); RequirementsTxtStatement::Requirements { filename: requirements_file.to_string(), @@ -444,7 +449,7 @@ fn parse_entry( end, } } else if s.eat_if("-c") || s.eat_if("--constraint") { - let constraints_file = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; + let constraints_file = parse_value(content, s, |c: char| !is_terminal(c))?; let end = s.cursor(); RequirementsTxtStatement::Constraint { filename: constraints_file.to_string(), @@ -475,7 +480,7 @@ fn parse_entry( hashes, }) } else if s.eat_if("-i") || s.eat_if("--index-url") { - let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; + let given = parse_value(content, s, |c: char| !is_terminal(c))?; let expanded = expand_env_vars(given); let url = if let Some(path) = std::path::absolute(expanded.as_ref()) .ok() @@ -501,7 +506,7 @@ fn parse_entry( }; RequirementsTxtStatement::IndexUrl(url.with_given(given)) } else if s.eat_if("--extra-index-url") { - let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; + let given = parse_value(content, s, |c: char| !is_terminal(c))?; let expanded = expand_env_vars(given); let url = if let Some(path) = std::path::absolute(expanded.as_ref()) .ok() @@ -529,7 +534,7 @@ fn parse_entry( } else if s.eat_if("--no-index") { RequirementsTxtStatement::NoIndex } else if s.eat_if("--find-links") || s.eat_if("-f") { - let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; + let given = parse_value(content, s, |c: char| !is_terminal(c))?; let expanded = expand_env_vars(given); let url = if let Some(path) = std::path::absolute(expanded.as_ref()) .ok() @@ -555,7 +560,7 @@ fn parse_entry( }; RequirementsTxtStatement::FindLinks(url.with_given(given)) } else if s.eat_if("--no-binary") { - let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; + let given = parse_value(content, s, |c: char| !is_terminal(c))?; let specifier = PackageNameSpecifier::from_str(given).map_err(|err| { RequirementsTxtParserError::NoBinary { source: err, @@ -566,7 +571,7 @@ fn parse_entry( })?; RequirementsTxtStatement::NoBinary(NoBinary::from_pip_arg(specifier)) } else if s.eat_if("--only-binary") { - let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; + let given = parse_value(content, s, |c: char| !is_terminal(c))?; let specifier = PackageNameSpecifier::from_str(given).map_err(|err| { RequirementsTxtParserError::NoBinary { source: err, From 359ef288f8204ad504aa8b81cdd5b0517fc32bcb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 10:47:25 -0500 Subject: [PATCH 083/135] Upgrade to the latest Ruff version (#10433) --- scripts/benchmark/src/benchmark/resolver.py | 24 +++++++++---------- .../pypi_10k_most_dependents.ipynb | 2 +- scripts/publish/test_publish.py | 8 +++---- scripts/scenarios/generate.py | 6 ++--- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/scripts/benchmark/src/benchmark/resolver.py b/scripts/benchmark/src/benchmark/resolver.py index 923a4c313722..54ae8471944a 100644 --- a/scripts/benchmark/src/benchmark/resolver.py +++ b/scripts/benchmark/src/benchmark/resolver.py @@ -443,9 +443,9 @@ def resolve_incremental( self.setup(requirements_file, cwd=cwd) poetry_lock = os.path.join(cwd, "poetry.lock") - assert not os.path.exists( - poetry_lock - ), f"Lockfile already exists at: {poetry_lock}" + assert not os.path.exists(poetry_lock), ( + f"Lockfile already exists at: {poetry_lock}" + ) # Run a resolution, to ensure that the lockfile exists. # TODO(charlie): Make this a `setup`. @@ -499,9 +499,9 @@ def resolve_noop(self, requirements_file: str, *, cwd: str) -> Command | None: self.setup(requirements_file, cwd=cwd) poetry_lock = os.path.join(cwd, "poetry.lock") - assert not os.path.exists( - poetry_lock - ), f"Lockfile already exists at: {poetry_lock}" + assert not os.path.exists(poetry_lock), ( + f"Lockfile already exists at: {poetry_lock}" + ) # Run a resolution, to ensure that the lockfile exists. # TODO(charlie): Make this a `setup`. @@ -536,9 +536,9 @@ def install_cold(self, requirements_file: str, *, cwd: str) -> Command | None: self.setup(requirements_file, cwd=cwd) poetry_lock = os.path.join(cwd, "poetry.lock") - assert not os.path.exists( - poetry_lock - ), f"Lockfile already exists at: {poetry_lock}" + assert not os.path.exists(poetry_lock), ( + f"Lockfile already exists at: {poetry_lock}" + ) # Run a resolution, to ensure that the lockfile exists. # TODO(charlie): Make this a `setup`. @@ -581,9 +581,9 @@ def install_warm(self, requirements_file: str, *, cwd: str) -> Command | None: self.setup(requirements_file, cwd=cwd) poetry_lock = os.path.join(cwd, "poetry.lock") - assert not os.path.exists( - poetry_lock - ), f"Lockfile already exists at: {poetry_lock}" + assert not os.path.exists(poetry_lock), ( + f"Lockfile already exists at: {poetry_lock}" + ) # Run a resolution, to ensure that the lockfile exists. subprocess.check_call( diff --git a/scripts/popular_packages/pypi_10k_most_dependents.ipynb b/scripts/popular_packages/pypi_10k_most_dependents.ipynb index 9dcf3c0d1b3c..6b9d401e68ca 100644 --- a/scripts/popular_packages/pypi_10k_most_dependents.ipynb +++ b/scripts/popular_packages/pypi_10k_most_dependents.ipynb @@ -37,7 +37,7 @@ " if i not in responses:\n", " # https://libraries.io/api#project-search\n", " sort = \"dependents_count\"\n", - " url = f\"https://libraries.io/api/search?platforms=Pypi&per_page=100&page={i+1}&sort{sort}&api_key={api_key}\"\n", + " url = f\"https://libraries.io/api/search?platforms=Pypi&per_page=100&page={i + 1}&sort{sort}&api_key={api_key}\"\n", " responses[i] = httpx.get(url, timeout=30.0).json()" ] }, diff --git a/scripts/publish/test_publish.py b/scripts/publish/test_publish.py index 55666274c1c7..f3f8e8d0fdb0 100644 --- a/scripts/publish/test_publish.py +++ b/scripts/publish/test_publish.py @@ -371,8 +371,8 @@ def publish_project(target: str, uv: Path, client: httpx.Client): ): raise RuntimeError( f"PyPI re-upload of the same files failed: " - f"{output.count("Uploading")} != {len(expected_filenames)}, " - f"{output.count("already exists")} != 0\n" + f"{output.count('Uploading')} != {len(expected_filenames)}, " + f"{output.count('already exists')} != 0\n" f"---\n{output}\n---" ) @@ -407,8 +407,8 @@ def publish_project(target: str, uv: Path, client: httpx.Client): ): raise RuntimeError( f"Re-upload with check URL failed: " - f"{output.count("Uploading")} != 0, " - f"{output.count("already exists")} != {len(expected_filenames)}\n" + f"{output.count('Uploading')} != 0, " + f"{output.count('already exists')} != {len(expected_filenames)}\n" f"---\n{output}\n---" ) diff --git a/scripts/scenarios/generate.py b/scripts/scenarios/generate.py index 6b9c4e462115..d6db67eeb354 100755 --- a/scripts/scenarios/generate.py +++ b/scripts/scenarios/generate.py @@ -269,9 +269,9 @@ def update_common_mod_rs(packse_version: str): url_matcher = re.compile( re.escape(before_version) + '[^"]+' + re.escape(after_version) ) - assert ( - len(url_matcher.findall(test_common)) == 1 - ), f"PACKSE_VERSION not found in {TESTS_COMMON_MOD_RS}" + assert len(url_matcher.findall(test_common)) == 1, ( + f"PACKSE_VERSION not found in {TESTS_COMMON_MOD_RS}" + ) test_common = url_matcher.sub(build_vendor_links_url, test_common) TESTS_COMMON_MOD_RS.write_text(test_common) From 19589e0614ad063fed39056e4968476576a48054 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 10:50:16 -0500 Subject: [PATCH 084/135] Add meta descriptions for some guides (#10421) ## Summary Part of: https://github.com/astral-sh/uv/issues/10418. --- docs/guides/integration/docker.md | 7 +++++++ docs/guides/integration/fastapi.md | 7 +++++++ docs/guides/integration/jupyter.md | 7 +++++++ docs/guides/integration/pre-commit.md | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 834e93c46f54..fee04193f3d1 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -1,3 +1,10 @@ +--- +title: Using uv in Docker +description: + A complete guide to using uv in Docker to manage Python dependencies while optimizing build times + and image size via multi-stage builds, intermediate layers, and more. +--- + # Using uv in Docker ## Getting started diff --git a/docs/guides/integration/fastapi.md b/docs/guides/integration/fastapi.md index 654b2ed14265..6693f46d3c6b 100644 --- a/docs/guides/integration/fastapi.md +++ b/docs/guides/integration/fastapi.md @@ -1,3 +1,10 @@ +--- +title: Using uv with FastAPI +description: + A guide to using uv with FastAPI to manage Python dependencies, run applications, and deploy with + Docker. +--- + # Using uv with FastAPI [FastAPI](https://github.com/fastapi/fastapi) is a modern, high-performance Python web framework. diff --git a/docs/guides/integration/jupyter.md b/docs/guides/integration/jupyter.md index 358a090d49b6..f1292a58123c 100644 --- a/docs/guides/integration/jupyter.md +++ b/docs/guides/integration/jupyter.md @@ -1,3 +1,10 @@ +--- +title: Using uv with Jupyter +description: + A complete guide to using uv with Jupyter notebooks for interactive computing, data analysis, and + visualization, including kernel management and virtual environment integration. +--- + # Using uv with Jupyter The [Jupyter](https://jupyter.org/) notebook is a popular tool for interactive computing, data diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 6d963f8a43f3..e60c1785465b 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -1,3 +1,10 @@ +--- +title: Using uv with pre-commit +description: + A guide to using uv with pre-commit to automatically update lock files, export requirements, and + compile requirements files. +--- + # Using uv in pre-commit An official pre-commit hook is provided at From 201726cda527340bd3becf84547f3dc36f5098a2 Mon Sep 17 00:00:00 2001 From: FishAlchemist <48265002+FishAlchemist@users.noreply.github.com> Date: Fri, 10 Jan 2025 01:41:04 +0800 Subject: [PATCH 085/135] docs: Clarify build system specific features usage. (#10261) ## Summary Since there are occasional inquiries about how to configure UV for build-system specific features, I want to raise awareness that users should refer to the documentation of the build system they are using for relevant settings. ## Test Plan Run docs service in local. https://github.com/astral-sh/uv/pull/10261/commits/9821d58d35dce6bbafe4bdadd9031afba0f787a5 ![image](https://github.com/user-attachments/assets/3c07ac15-a562-40e2-9289-204c0975261f) --------- Signed-off-by: FishAlchemist <48265002+FishAlchemist@users.noreply.github.com> Co-authored-by: Zanie Blue --- docs/concepts/projects/config.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/concepts/projects/config.md b/docs/concepts/projects/config.md index 3fa7c088890c..6e1f84632a3e 100644 --- a/docs/concepts/projects/config.md +++ b/docs/concepts/projects/config.md @@ -119,6 +119,18 @@ with the default build system. installable. Similarly, if you add a dependency on a local package or install it with `uv pip`, uv will always attempt to build and install it. +### Build system options + +Build systems are used to power the following features: + +- Including or excluding files from distributions +- Editable install behavior +- Dynamic project metadata +- Compilation of native code +- Vendoring shared libraries + +To configure these features, refer to the documentation of your chosen build system. + ## Project packaging As discussed in [build systems](#build-systems), a Python project must be built to be installed. From a0494bb059992df44abcb37e0c492a56c53159d2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 12:45:20 -0500 Subject: [PATCH 086/135] Fetch concurrently for non-first-match index strategies (#10432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary On a basic test, this speeds up cold resolution by about 25%: ``` ❯ hyperfine "uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache" "../target/release/uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache" --warmup 10 --runs 30 Benchmark 1: uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache Time (mean ± σ): 585.8 ms ± 28.2 ms [User: 149.7 ms, System: 97.4 ms] Range (min … max): 541.5 ms … 654.8 ms 30 runs Benchmark 2: ../target/release/uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache Time (mean ± σ): 468.3 ms ± 52.0 ms [User: 131.7 ms, System: 76.9 ms] Range (min … max): 380.2 ms … 607.0 ms 30 runs Summary ../target/release/uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache ran 1.25 ± 0.15 times faster than uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache ``` Given: ```toml [project] name = "foo" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12.0" dependencies = [ "black>=24.10.0", "django>=5.1.4", "flask>=3.1.0", "requests>=2.32.3", ] ``` And: ```shell hyperfine "uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache" "../target/release/uv lock --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match --upgrade --no-cache" --warmup 10 --runs 30 ``` Closes https://github.com/astral-sh/uv/issues/10429. --- crates/uv-client/src/registry_client.rs | 117 +++++++++++++++++------- 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index da8c3b18c427..d9812253a2d4 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -1,14 +1,15 @@ -use async_http_range_reader::AsyncHttpRangeReader; -use futures::{FutureExt, TryStreamExt}; -use http::HeaderMap; -use itertools::Either; -use reqwest::{Client, Response, StatusCode}; -use reqwest_middleware::ClientWithMiddleware; use std::collections::BTreeMap; use std::fmt::Debug; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; + +use async_http_range_reader::AsyncHttpRangeReader; +use futures::{FutureExt, StreamExt, TryStreamExt}; +use http::HeaderMap; +use itertools::Either; +use reqwest::{Client, Response, StatusCode}; +use reqwest_middleware::ClientWithMiddleware; use tracing::{info_span, instrument, trace, warn, Instrument}; use url::Url; @@ -247,38 +248,86 @@ impl RegistryClient { } let mut results = Vec::new(); - for index in it { - match self.simple_single_index(package_name, index).await { - Ok(metadata) => { - results.push((index, metadata)); - - // If we're only using the first match, we can stop here. - if self.index_strategy == IndexStrategy::FirstIndex { - break; - } - } - Err(err) => match err.into_kind() { - // The package could not be found in the remote index. - ErrorKind::WrappedReqwestError(url, err) => match err.status() { - Some(StatusCode::NOT_FOUND) => {} - Some(StatusCode::UNAUTHORIZED) => { - capabilities.set_unauthorized(index.clone()); - } - Some(StatusCode::FORBIDDEN) => { - capabilities.set_forbidden(index.clone()); + + match self.index_strategy { + // If we're searching for the first index that contains the package, fetch serially. + IndexStrategy::FirstIndex => { + for index in it { + match self.simple_single_index(package_name, index).await { + Ok(metadata) => { + results.push((index, metadata)); + break; } - _ => return Err(ErrorKind::WrappedReqwestError(url, err).into()), - }, + Err(err) => match err.into_kind() { + // The package could not be found in the remote index. + ErrorKind::WrappedReqwestError(url, err) => match err.status() { + Some(StatusCode::NOT_FOUND) => {} + Some(StatusCode::UNAUTHORIZED) => { + capabilities.set_unauthorized(index.clone()); + } + Some(StatusCode::FORBIDDEN) => { + capabilities.set_forbidden(index.clone()); + } + _ => return Err(ErrorKind::WrappedReqwestError(url, err).into()), + }, + + // The package is unavailable due to a lack of connectivity. + ErrorKind::Offline(_) => {} + + // The package could not be found in the local index. + ErrorKind::FileNotFound(_) => {} + + err => return Err(err.into()), + }, + }; + } + } - // The package is unavailable due to a lack of connectivity. - ErrorKind::Offline(_) => {} + // Otherwise, fetch concurrently. + IndexStrategy::UnsafeBestMatch | IndexStrategy::UnsafeFirstMatch => { + let fetches = futures::stream::iter(it) + .map(|index| async move { + match self.simple_single_index(package_name, index).await { + Ok(metadata) => Ok(Some((index, metadata))), + Err(err) => match err.into_kind() { + // The package could not be found in the remote index. + ErrorKind::WrappedReqwestError(url, err) => match err.status() { + Some(StatusCode::NOT_FOUND) => Ok(None), + Some(StatusCode::UNAUTHORIZED) => { + capabilities.set_unauthorized(index.clone()); + Ok(None) + } + Some(StatusCode::FORBIDDEN) => { + capabilities.set_forbidden(index.clone()); + Ok(None) + } + _ => Err(ErrorKind::WrappedReqwestError(url, err).into()), + }, + + // The package is unavailable due to a lack of connectivity. + ErrorKind::Offline(_) => Ok(None), + + // The package could not be found in the local index. + ErrorKind::FileNotFound(_) => Ok(None), + + err => Err(err.into()), + }, + } + }) + .buffered(8); - // The package could not be found in the local index. - ErrorKind::FileNotFound(_) => {} + futures::pin_mut!(fetches); - other => return Err(other.into()), - }, - }; + while let Some(result) = fetches.next().await { + match result { + Ok(Some((index, metadata))) => { + results.push((index, metadata)); + } + Ok(None) => continue, + Err(err) => return Err(err), + } + } + } } if results.is_empty() { From 14b685d9fb8e059744225b3b9ca7e49e34e8373a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 13:19:51 -0500 Subject: [PATCH 087/135] Warn-and-ignore for unsupported `requirements.txt` options (#10420) ## Summary Closes https://github.com/astral-sh/uv/issues/10366. --- Cargo.lock | 1 + crates/uv-requirements-txt/Cargo.toml | 9 ++- crates/uv-requirements-txt/src/lib.rs | 107 +++++++++++++++++++++++--- crates/uv/tests/it/pip_install.rs | 33 ++++++++ 4 files changed, 135 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a8e8184e2df..198ac0923b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5485,6 +5485,7 @@ dependencies = [ "uv-normalize", "uv-pep508", "uv-pypi-types", + "uv-warnings", ] [[package]] diff --git a/crates/uv-requirements-txt/Cargo.toml b/crates/uv-requirements-txt/Cargo.toml index 11e7d1666866..7dcc2964ecd2 100644 --- a/crates/uv-requirements-txt/Cargo.toml +++ b/crates/uv-requirements-txt/Cargo.toml @@ -16,13 +16,14 @@ doctest = false workspace = true [dependencies] -uv-distribution-types = { workspace = true } -uv-pep508 = { workspace = true } -uv-pypi-types = { workspace = true } uv-client = { workspace = true } +uv-configuration = { workspace = true } +uv-distribution-types = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } -uv-configuration = { workspace = true } +uv-pep508 = { workspace = true } +uv-pypi-types = { workspace = true } +uv-warnings = { workspace = true } fs-err = { workspace = true } regex = { workspace = true } diff --git a/crates/uv-requirements-txt/src/lib.rs b/crates/uv-requirements-txt/src/lib.rs index 3c8264d53e9d..c4893f22bd6e 100644 --- a/crates/uv-requirements-txt/src/lib.rs +++ b/crates/uv-requirements-txt/src/lib.rs @@ -88,6 +88,8 @@ enum RequirementsTxtStatement { NoBinary(NoBinary), /// `--only-binary` OnlyBinary(NoBuild), + /// An unsupported option (e.g., `--trusted-host`). + UnsupportedOption(UnsupportedOption), } /// A [Requirement] with additional metadata from the `requirements.txt`, currently only hashes but in @@ -384,6 +386,28 @@ impl RequirementsTxt { RequirementsTxtStatement::OnlyBinary(only_binary) => { data.only_binary.extend(only_binary); } + RequirementsTxtStatement::UnsupportedOption(flag) => { + if requirements_txt == Path::new("-") { + if flag.cli() { + uv_warnings::warn_user!("Ignoring unsupported option from stdin: `{flag}` (hint: pass `{flag}` on the command line instead)", flag = flag.green()); + } else { + uv_warnings::warn_user!( + "Ignoring unsupported option from stdin: `{flag}`", + flag = flag.green() + ); + } + } else { + if flag.cli() { + uv_warnings::warn_user!("Ignoring unsupported option in `{path}`: `{flag}` (hint: pass `{flag}` on the command line instead)", path = requirements_txt.user_display().cyan(), flag = flag.green()); + } else { + uv_warnings::warn_user!( + "Ignoring unsupported option in `{path}`: `{flag}`", + path = requirements_txt.user_display().cyan(), + flag = flag.green() + ); + } + } + } } } Ok(data) @@ -416,15 +440,70 @@ impl RequirementsTxt { } } +/// An unsupported option (e.g., `--trusted-host`). +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UnsupportedOption { + PreferBinary, + RequireHashes, + Pre, + TrustedHost, + UseFeature, +} + +impl UnsupportedOption { + /// The name of the unsupported option. + fn name(self) -> &'static str { + match self { + UnsupportedOption::PreferBinary => "--prefer-binary", + UnsupportedOption::RequireHashes => "--require-hashes", + UnsupportedOption::Pre => "--pre", + UnsupportedOption::TrustedHost => "--trusted-host", + UnsupportedOption::UseFeature => "--use-feature", + } + } + + /// Returns `true` if the option is supported on the CLI. + fn cli(self) -> bool { + match self { + UnsupportedOption::PreferBinary => false, + UnsupportedOption::RequireHashes => true, + UnsupportedOption::Pre => true, + UnsupportedOption::TrustedHost => true, + UnsupportedOption::UseFeature => false, + } + } + + /// Returns an iterator over all unsupported options. + fn iter() -> impl Iterator { + [ + UnsupportedOption::PreferBinary, + UnsupportedOption::RequireHashes, + UnsupportedOption::Pre, + UnsupportedOption::TrustedHost, + UnsupportedOption::UseFeature, + ] + .iter() + .copied() + } +} + +impl Display for UnsupportedOption { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + /// Returns `true` if the character is a newline or a comment character. const fn is_terminal(c: char) -> bool { matches!(c, '\n' | '\r' | '#') } -/// Parse a single entry, that is a requirement, an inclusion or a comment line +/// Parse a single entry, that is a requirement, an inclusion or a comment line. /// -/// Consumes all preceding trivia (whitespace and comments). If it returns None, we've reached -/// the end of file +/// Consumes all preceding trivia (whitespace and comments). If it returns `None`, we've reached +/// the end of file. fn parse_entry( s: &mut Scanner, content: &str, @@ -595,14 +674,20 @@ fn parse_entry( hashes, }) } else if let Some(char) = s.peek() { - let (line, column) = calculate_row_column(content, s.cursor()); - return Err(RequirementsTxtParserError::Parser { - message: format!( - "Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement" - ), - line, - column, - }); + // Identify an unsupported option, like `--trusted-host`. + if let Some(option) = UnsupportedOption::iter().find(|option| s.eat_if(option.name())) { + s.eat_while(|c: char| !is_terminal(c)); + RequirementsTxtStatement::UnsupportedOption(option) + } else { + let (line, column) = calculate_row_column(content, s.cursor()); + return Err(RequirementsTxtParserError::Parser { + message: format!( + "Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement" + ), + line, + column, + }); + } } else { // EOF return Ok(None); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index cdd067ee5c85..789ce3835e4d 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -486,6 +486,39 @@ fn install_requirements_txt() -> Result<()> { Ok(()) } +/// Warn (but don't fail) when unsupported flags are set in the `requirements.txt`. +#[test] +fn install_unsupported_flag() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {r" + --pre + --prefer-binary :all: + iniconfig + "})?; + + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Ignoring unsupported option in `requirements.txt`: `--pre` (hint: pass `--pre` on the command line instead) + warning: Ignoring unsupported option in `requirements.txt`: `--prefer-binary` + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "### + ); + + Ok(()) +} + /// Install a requirements file with pins that conflict /// /// This is likely to occur in the real world when compiled on one platform then installed on another. From 57367ed327566b2eb1141ca29cf1ad44dcf88d26 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 9 Jan 2025 19:31:51 +0100 Subject: [PATCH 088/135] Use faster disjointness check for markers (#10439) --- crates/uv-pep508/src/marker/algebra.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/uv-pep508/src/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs index 089e22432176..892c38f8ddc1 100644 --- a/crates/uv-pep508/src/marker/algebra.rs +++ b/crates/uv-pep508/src/marker/algebra.rs @@ -1412,8 +1412,7 @@ impl Edges { // not the resulting edges. for (left_range, left_child) in left_edges { for (right_range, right_child) in right_edges { - let intersection = right_range.intersection(left_range); - if intersection.is_empty() { + if right_range.is_disjoint(left_range) { continue; } From 56d39d21c23100ad51f9c3f10cb49412045754e1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 15:37:28 -0500 Subject: [PATCH 089/135] Visit proxy packages eagerly (#10441) ## Summary The issue here is that we add `urllib3{python_full_version >= '3.8'}` as a dependency, then `requests{python_full_version >= '3.8'}`, which adds `urllib3`, but at that point, we haven't expanded `urllib3{python_full_version >= '3.8'}`, so we "lose" the singleton constraint. The solution is to ensure that we visit proxies eagerly, so that we accumulate constraints as early as possible. Closes https://github.com/astral-sh/uv/issues/10425#issuecomment-2580324578. --- crates/uv-resolver/src/pubgrub/priority.rs | 24 ++- crates/uv-resolver/src/resolver/mod.rs | 8 +- crates/uv-resolver/src/version_map.rs | 2 + crates/uv/tests/it/lock_conflict.rs | 24 +-- crates/uv/tests/it/lock_scenarios.rs | 10 +- crates/uv/tests/it/pip_compile.rs | 4 +- crates/uv/tests/it/pip_install.rs | 142 +++++++++++++++++ crates/uv/tests/it/pip_install_scenarios.rs | 4 +- ...it__ecosystem__transformers-lock-file.snap | 145 ++++-------------- ...cosystem__transformers-uv-lock-output.snap | 2 +- 10 files changed, 219 insertions(+), 146 deletions(-) diff --git a/crates/uv-resolver/src/pubgrub/priority.rs b/crates/uv-resolver/src/pubgrub/priority.rs index 588b8f6bcc67..80b1acc38c5b 100644 --- a/crates/uv-resolver/src/pubgrub/priority.rs +++ b/crates/uv-resolver/src/pubgrub/priority.rs @@ -1,12 +1,13 @@ -use pubgrub::Range; -use rustc_hash::FxHashMap; use std::cmp::Reverse; use std::collections::hash_map::OccupiedEntry; -use crate::fork_urls::ForkUrls; +use pubgrub::Range; +use rustc_hash::FxHashMap; + use uv_normalize::PackageName; use uv_pep440::Version; +use crate::fork_urls::ForkUrls; use crate::pubgrub::package::PubGrubPackage; use crate::pubgrub::PubGrubPackageInner; @@ -57,6 +58,7 @@ impl PubGrubPriorities { let priority = if urls.get(name).is_some() { PubGrubPriority::DirectUrl(Reverse(index)) } else if version.as_singleton().is_some() { + // TODO(charlie): Take local version ranges into account (e.g., `[2.0, 2.0+[max])`). PubGrubPriority::Singleton(Reverse(index)) } else { // Keep the conflict-causing packages to avoid loops where we seesaw between @@ -80,6 +82,7 @@ impl PubGrubPriorities { let priority = if urls.get(name).is_some() { PubGrubPriority::DirectUrl(Reverse(next)) } else if version.as_singleton().is_some() { + // TODO(charlie): Take local version ranges into account (e.g., `[2.0, 2.0+[max])`). PubGrubPriority::Singleton(Reverse(next)) } else { PubGrubPriority::Unspecified(Reverse(next)) @@ -98,7 +101,7 @@ impl PubGrubPriorities { | PubGrubPriority::ConflictEarly(Reverse(index)) | PubGrubPriority::Singleton(Reverse(index)) | PubGrubPriority::DirectUrl(Reverse(index)) => Some(*index), - PubGrubPriority::Root => None, + PubGrubPriority::Proxy | PubGrubPriority::Root => None, } } @@ -107,9 +110,9 @@ impl PubGrubPriorities { let package_priority = match &**package { PubGrubPackageInner::Root(_) => Some(PubGrubPriority::Root), PubGrubPackageInner::Python(_) => Some(PubGrubPriority::Root), - PubGrubPackageInner::Marker { name, .. } => self.package_priority.get(name).copied(), - PubGrubPackageInner::Extra { name, .. } => self.package_priority.get(name).copied(), - PubGrubPackageInner::Dev { name, .. } => self.package_priority.get(name).copied(), + PubGrubPackageInner::Marker { .. } => Some(PubGrubPriority::Proxy), + PubGrubPackageInner::Extra { .. } => Some(PubGrubPriority::Proxy), + PubGrubPackageInner::Dev { .. } => Some(PubGrubPriority::Proxy), PubGrubPackageInner::Package { name, .. } => self.package_priority.get(name).copied(), }; let virtual_package_tiebreaker = self @@ -224,6 +227,13 @@ pub(crate) enum PubGrubPriority { /// [`ForkUrls`]. DirectUrl(Reverse), + /// The package is a proxy package. + /// + /// We process proxy packages eagerly since each proxy package expands into two "regular" + /// [`PubGrubPackage`] packages, which gives us additional constraints while not affecting the + /// priorities (since the expanded dependencies are all linked to the same package name). + Proxy, + /// The package is the root package. Root, } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 3bc26c62d36d..60d309bb8194 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -2591,7 +2591,9 @@ impl ForkState { } } - if let Some(name) = self.pubgrub.package_store[for_package] + let for_package = &self.pubgrub.package_store[for_package]; + + if let Some(name) = for_package .name_no_root() .filter(|name| !workspace_members.contains(name)) { @@ -2622,7 +2624,9 @@ impl ForkState { } // Update the package priorities. - self.priorities.insert(package, version, &self.fork_urls); + if !for_package.is_proxy() { + self.priorities.insert(package, version, &self.fork_urls); + } } let conflict = self.pubgrub.add_package_version_dependencies( diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index bf9a87983242..36ece551f3aa 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -162,6 +162,8 @@ impl VersionMap { range: &Ranges, ) -> impl DoubleEndedIterator { // Performance optimization: If we only have a single version, return that version directly. + // + // TODO(charlie): Now that we use local version sentinels, does this ever trigger? if let Some(version) = range.as_singleton() { either::Either::Left(match self.inner { VersionMapInner::Eager(ref eager) => { diff --git a/crates/uv/tests/it/lock_conflict.rs b/crates/uv/tests/it/lock_conflict.rs index a056f7b366bb..334150536728 100644 --- a/crates/uv/tests/it/lock_conflict.rs +++ b/crates/uv/tests/it/lock_conflict.rs @@ -50,7 +50,7 @@ fn extra_basic() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project[extra2] depends on sortedcontainers==2.4.0 and project[extra1] depends on sortedcontainers==2.3.0, we can conclude that project[extra1] and project[extra2] are incompatible. + ╰─▶ Because project[extra1] depends on sortedcontainers==2.3.0 and project[extra2] depends on sortedcontainers==2.4.0, we can conclude that project[extra1] and project[extra2] are incompatible. And because your project requires project[extra1] and project[extra2], we can conclude that your project's requirements are unsatisfiable. "###); @@ -250,7 +250,7 @@ fn extra_basic_three_extras() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project[project3] depends on sortedcontainers==2.4.0 and project[extra2] depends on sortedcontainers==2.3.0, we can conclude that project[extra2] and project[project3] are incompatible. + ╰─▶ Because project[extra2] depends on sortedcontainers==2.3.0 and project[project3] depends on sortedcontainers==2.4.0, we can conclude that project[extra2] and project[project3] are incompatible. And because your project requires project[extra2] and project[project3], we can conclude that your project's requirements are unsatisfiable. "###); @@ -538,7 +538,7 @@ fn extra_multiple_not_conflicting2() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project[project4] depends on sortedcontainers==2.4.0 and project[project3] depends on sortedcontainers==2.3.0, we can conclude that project[project3] and project[project4] are incompatible. + ╰─▶ Because project[project3] depends on sortedcontainers==2.3.0 and project[project4] depends on sortedcontainers==2.4.0, we can conclude that project[project3] and project[project4] are incompatible. And because your project requires project[project3] and project[project4], we can conclude that your project's requirements are unsatisfiable. "###); @@ -582,7 +582,7 @@ fn extra_multiple_not_conflicting2() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project[project3] depends on sortedcontainers==2.3.0 and project[extra2] depends on sortedcontainers==2.4.0, we can conclude that project[extra2] and project[project3] are incompatible. + ╰─▶ Because project[extra2] depends on sortedcontainers==2.4.0 and project[project3] depends on sortedcontainers==2.3.0, we can conclude that project[extra2] and project[project3] are incompatible. And because your project requires project[extra2] and project[project3], we can conclude that your project's requirements are unsatisfiable. "###); @@ -715,7 +715,7 @@ fn extra_multiple_independent() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project[project4] depends on anyio==4.2.0 and project[project3] depends on anyio==4.1.0, we can conclude that project[project3] and project[project4] are incompatible. + ╰─▶ Because project[project3] depends on anyio==4.1.0 and project[project4] depends on anyio==4.2.0, we can conclude that project[project3] and project[project4] are incompatible. And because your project requires project[project3] and project[project4], we can conclude that your project's requirements are unsatisfiable. "###); @@ -755,7 +755,7 @@ fn extra_multiple_independent() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project[extra2] depends on sortedcontainers==2.4.0 and project[extra1] depends on sortedcontainers==2.3.0, we can conclude that project[extra1] and project[extra2] are incompatible. + ╰─▶ Because project[extra1] depends on sortedcontainers==2.3.0 and project[extra2] depends on sortedcontainers==2.4.0, we can conclude that project[extra1] and project[extra2] are incompatible. And because your project requires project[extra1] and project[extra2], we can conclude that your project's requirements are unsatisfiable. "###); @@ -1047,7 +1047,7 @@ fn extra_config_change_ignore_lockfile() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project[extra2] depends on sortedcontainers==2.4.0 and project[extra1] depends on sortedcontainers==2.3.0, we can conclude that project[extra1] and project[extra2] are incompatible. + ╰─▶ Because project[extra1] depends on sortedcontainers==2.3.0 and project[extra2] depends on sortedcontainers==2.4.0, we can conclude that project[extra1] and project[extra2] are incompatible. And because your project requires project[extra1] and project[extra2], we can conclude that your project's requirements are unsatisfiable. "###); @@ -1289,9 +1289,9 @@ fn extra_nested_across_workspace() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because dummy[extra2] depends on proxy1[extra2] and only proxy1[extra2]==0.1.0 is available, we can conclude that dummy[extra2] depends on proxy1[extra2]==0.1.0. - And because proxy1[extra2]==0.1.0 depends on anyio==4.2.0 and proxy1[extra1]==0.1.0 depends on anyio==4.1.0, we can conclude that proxy1[extra1]==0.1.0 and dummy[extra2] are incompatible. - And because only proxy1[extra1]==0.1.0 is available and dummysub[extra1] depends on proxy1[extra1], we can conclude that dummysub[extra1] and dummy[extra2] are incompatible. + ╰─▶ Because proxy1[extra1]==0.1.0 depends on anyio==4.1.0 and proxy1[extra2]==0.1.0 depends on anyio==4.2.0, we can conclude that proxy1[extra1]==0.1.0 and proxy1[extra2]==0.1.0 are incompatible. + And because only proxy1[extra1]==0.1.0 is available and dummysub[extra1] depends on proxy1[extra1], we can conclude that dummysub[extra1] and proxy1[extra2]==0.1.0 are incompatible. + And because only proxy1[extra2]==0.1.0 is available and dummy[extra2] depends on proxy1[extra2], we can conclude that dummy[extra2] and dummysub[extra1] are incompatible. And because your workspace requires dummy[extra2] and dummysub[extra1], we can conclude that your workspace's requirements are unsatisfiable. "###); @@ -1415,7 +1415,7 @@ fn group_basic() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project:group2 depends on sortedcontainers==2.4.0 and project:group1 depends on sortedcontainers==2.3.0, we can conclude that project:group1 and project:group2 are incompatible. + ╰─▶ Because project:group1 depends on sortedcontainers==2.3.0 and project:group2 depends on sortedcontainers==2.4.0, we can conclude that project:group1 and project:group2 are incompatible. And because your project requires project:group1 and project:group2, we can conclude that your project's requirements are unsatisfiable. "###); @@ -1793,7 +1793,7 @@ fn mixed() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because project:group1 depends on sortedcontainers==2.3.0 and project[extra1] depends on sortedcontainers==2.4.0, we can conclude that project[extra1] and project:group1 are incompatible. + ╰─▶ Because project[extra1] depends on sortedcontainers==2.4.0 and project:group1 depends on sortedcontainers==2.3.0, we can conclude that project:group1 and project[extra1] are incompatible. And because your project requires project[extra1] and project:group1, we can conclude that your project's requirements are unsatisfiable. "###); diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index febac6b07a09..4ac688b7f457 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -588,7 +588,7 @@ fn conflict_in_fork() -> Result<()> { × No solution found when resolving dependencies for split (sys_platform == 'darwin'): ╰─▶ Because only package-b==1.0.0 is available and package-b==1.0.0 depends on package-d==1, we can conclude that all versions of package-b depend on package-d==1. And because package-c==1.0.0 depends on package-d==2 and only package-c==1.0.0 is available, we can conclude that all versions of package-b and all versions of package-c are incompatible. - And because package-a{sys_platform == 'darwin'}==1.0.0 depends on package-b and package-c, we can conclude that package-a{sys_platform == 'darwin'}==1.0.0 cannot be used. + And because package-a==1.0.0 depends on package-b and package-c, we can conclude that package-a==1.0.0 cannot be used. And because only the following versions of package-a{sys_platform == 'darwin'} are available: package-a{sys_platform == 'darwin'}==1.0.0 package-a{sys_platform == 'darwin'}>2 @@ -2940,7 +2940,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because package-a{sys_platform == 'linux'}==1.0.0 depends on package-c<2.0.0 and package-b{sys_platform == 'darwin'}==1.0.0 depends on package-c>=2.0.0, we can conclude that package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0 are incompatible. + ╰─▶ Because package-b==1.0.0 depends on package-c>=2.0.0 and package-a==1.0.0 depends on package-c<2.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. And because your project depends on package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0, we can conclude that your project's requirements are unsatisfiable. "### ); @@ -3015,8 +3015,10 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { ╰─▶ Because package-a==1.0.0 depends on package-c{sys_platform == 'linux'}<2.0.0 and only the following versions of package-c{sys_platform == 'linux'} are available: package-c{sys_platform == 'linux'}==1.0.0 package-c{sys_platform == 'linux'}>2.0.0 - we can conclude that package-a==1.0.0 depends on package-c{sys_platform == 'linux'}==1.0.0. - And because only package-c{sys_platform == 'darwin'}<=2.0.0 is available and package-b==1.0.0 depends on package-c{sys_platform == 'darwin'}>=2.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. + we can conclude that package-a==1.0.0 depends on package-c{sys_platform == 'linux'}==1.0.0. (1) + + Because only package-c{sys_platform == 'darwin'}<=2.0.0 is available and package-b==1.0.0 depends on package-c{sys_platform == 'darwin'}>=2.0.0, we can conclude that package-b==1.0.0 depends on package-c==2.0.0. + And because we know from (1) that package-a==1.0.0 depends on package-c{sys_platform == 'linux'}==1.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. And because your project depends on package-a==1.0.0 and package-b==1.0.0, we can conclude that your project's requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 87a3264068f3..0d997d65e98e 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -13622,9 +13622,9 @@ fn unsupported_requires_python_dynamic_metadata() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies for split (python_full_version >= '3.10'): - ╰─▶ Because source-distribution{python_full_version >= '3.10'}==0.0.3 requires Python >=3.10 and you require source-distribution{python_full_version >= '3.10'}==0.0.3, we can conclude that your requirements are unsatisfiable. + ╰─▶ Because source-distribution==0.0.3 requires Python >=3.10 and you require source-distribution{python_full_version >= '3.10'}==0.0.3, we can conclude that your requirements are unsatisfiable. - hint: The source distribution for `source-distribution{python_full_version >= '3.10'}` (v0.0.3) does not include static metadata. Generating metadata for this package requires Python >=3.10, but Python 3.8.[X] is installed. + hint: The source distribution for `source-distribution` (v0.0.3) does not include static metadata. Generating metadata for this package requires Python >=3.10, but Python 3.8.[X] is installed. "###); Ok(()) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 789ce3835e4d..93d410560a9c 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -6340,6 +6340,148 @@ fn require_hashes_override() -> Result<()> { Ok(()) } +/// Provide valid hashes for all dependencies with `--require-hashes` with accompanying markers. +/// Critically, one package (`requests`) depends on another (`urllib3`). +#[test] +fn require_hashes_marker() -> Result<()> { + static EXCLUDE_NEWER: &str = "2025-01-01T00:00:00Z"; + + let context = TestContext::new("3.12"); + + // Write to a requirements file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc::indoc! {r" + certifi==2024.12.14 ; python_version >= '3.8' \ + --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ + --hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db + charset-normalizer==3.4.1 ; python_version >= '3.8' \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 + idna==3.10 ; python_version >= '3.8' \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + requests==2.32.3 ; python_version >= '3.8' \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + urllib3==2.2.3 ; python_version >= '3.8' \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 + "})?; + + uv_snapshot!(context.pip_install() + .env(EnvVars::UV_EXCLUDE_NEWER, EXCLUDE_NEWER) + .arg("-r") + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + + certifi==2024.12.14 + + charset-normalizer==3.4.1 + + idna==3.10 + + requests==2.32.3 + + urllib3==2.2.3 + "### + ); + + Ok(()) +} + /// Provide valid hashes for all dependencies with `--require-hashes`. #[test] fn verify_hashes() -> Result<()> { diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index cd8882c870e7..4dce0c7337e2 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -836,8 +836,8 @@ fn extra_incompatible_with_extra() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only package-a[extra-c]==1.0.0 is available and package-a[extra-c]==1.0.0 depends on package-b==2.0.0, we can conclude that all versions of package-a[extra-c] depend on package-b==2.0.0. - And because package-a[extra-b]==1.0.0 depends on package-b==1.0.0 and only package-a[extra-b]==1.0.0 is available, we can conclude that all versions of package-a[extra-b] and all versions of package-a[extra-c] are incompatible. + ╰─▶ Because package-a[extra-b]==1.0.0 depends on package-b==1.0.0 and package-a[extra-c]==1.0.0 depends on package-b==2.0.0, we can conclude that package-a[extra-b]==1.0.0 and package-a[extra-c]==1.0.0 are incompatible. + And because only package-a[extra-c]==1.0.0 is available and only package-a[extra-b]==1.0.0 is available, we can conclude that all versions of package-a[extra-b] and all versions of package-a[extra-c] are incompatible. And because you require package-a[extra-b] and package-a[extra-c], we can conclude that your requirements are unsatisfiable. "###); diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap index 67d4e4fe2bf2..c0b0a9ba8d5b 100644 --- a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap +++ b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap @@ -770,69 +770,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/ce/43f77dc8e7bbad02a9f88d07bf794eaf68359df756a28bb9f2f78e255bb1/dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9", size = 3912 }, ] -[[package]] -name = "datasets" -version = "2.14.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "aiohttp", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "dill", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["http"], marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "huggingface-hub", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "multiprocess", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "numpy", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "packaging", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "pandas", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "pyarrow", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "pyyaml", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "requests", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "tqdm", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "xxhash", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/69/8cc725b5d38968fd118e4ce56a483b16e75b7793854c1a392ec4a34eeb31/datasets-2.14.4.tar.gz", hash = "sha256:ef29c2b5841de488cd343cfc26ab979bff77efa4d2285af51f1ad7db5c46a83b", size = 2178719 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/f8/38298237d18d4b6a8ee5dfe390e97bed5adb8e01ec6f9680c0ddf3066728/datasets-2.14.4-py3-none-any.whl", hash = "sha256:29336bd316a7d827ccd4da2236596279b20ca2ac78f64c04c9483da7cbc2459b", size = 519335 }, -] - [[package]] name = "datasets" version = "2.20.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "aiohttp", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "dill", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "filelock", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "fsspec", version = "2024.5.0", source = { registry = "https://pypi.org/simple" }, extra = ["http"], marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "huggingface-hub", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "multiprocess", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "numpy", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "packaging", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "pandas", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "pyarrow", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "pyarrow-hotfix", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "requests", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "tqdm", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "xxhash", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, + { name = "aiohttp" }, + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyarrow-hotfix" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/59/b94bfb5f6225c4c931cd516390b3f006e232a036a48337f72889c6c9ab27/datasets-2.20.0.tar.gz", hash = "sha256:3c4dbcd27e0f642b9d41d20ff2efa721a5e04b32b2ca4009e0fc9139e324553f", size = 2225757 } wheels = [ @@ -945,7 +902,7 @@ wheels = [ [package.optional-dependencies] epath = [ - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", marker = "python_full_version < '3.10'" }, { name = "importlib-resources", marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, { name = "zipp", marker = "python_full_version < '3.10'" }, @@ -970,7 +927,7 @@ wheels = [ [package.optional-dependencies] epath = [ - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "fsspec", marker = "python_full_version == '3.10.*'" }, { name = "importlib-resources", marker = "python_full_version == '3.10.*'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, { name = "zipp", marker = "python_full_version == '3.10.*'" }, @@ -1001,8 +958,7 @@ wheels = [ [package.optional-dependencies] epath = [ - { name = "fsspec", version = "2024.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and sys_platform == 'darwin')" }, + { name = "fsspec", marker = "python_full_version >= '3.11'" }, { name = "importlib-resources", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, { name = "zipp", marker = "python_full_version >= '3.11'" }, @@ -1016,11 +972,9 @@ name = "evaluate" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "datasets", version = "2.14.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "datasets", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, + { name = "datasets" }, { name = "dill" }, - { name = "fsspec", version = "2024.5.0", source = { registry = "https://pypi.org/simple" }, extra = ["http"], marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["http"], marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, + { name = "fsspec", extra = ["http"] }, { name = "huggingface-hub" }, { name = "multiprocess" }, { name = "numpy" }, @@ -1246,10 +1200,6 @@ wheels = [ name = "fsspec" version = "2024.5.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/71/28/cbf337fddd6f22686b7c2639b80e006accd904db152fe333fd98f4cd8d1e/fsspec-2024.5.0.tar.gz", hash = "sha256:1d021b0b0f933e3b3029ed808eb400c08ba101ca2de4b3483fbc9ca23fcee94a", size = 400066 } wheels = [ { url = "https://files.pythonhosted.org/packages/ba/a3/16e9fe32187e9c8bc7f9b7bcd9728529faa725231a0c96f2f98714ff2fc5/fsspec-2024.5.0-py3-none-any.whl", hash = "sha256:e0fdbc446d67e182f49a70b82cf7889028a63588fde6b222521f10937b2b670c", size = 316106 }, @@ -1257,36 +1207,7 @@ wheels = [ [package.optional-dependencies] http = [ - { name = "aiohttp", marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, -] - -[[package]] -name = "fsspec" -version = "2024.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/90/b6/eba5024a9889fcfff396db543a34bef0ab9d002278f163129f9f01005960/fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49", size = 284584 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/44/73bea497ac69bafde2ee4269292fa3b41f1198f4bb7bbaaabde30ad29d4a/fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e", size = 177561 }, -] - -[package.optional-dependencies] -http = [ - { name = "aiohttp", marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, + { name = "aiohttp" }, ] [[package]] @@ -1565,8 +1486,7 @@ version = "0.24.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, - { name = "fsspec", version = "2024.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, + { name = "fsspec" }, { name = "packaging" }, { name = "pyyaml" }, { name = "requests" }, @@ -3572,8 +3492,7 @@ wheels = [ [package.optional-dependencies] tune = [ - { name = "fsspec", version = "2024.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, + { name = "fsspec" }, { name = "pandas" }, { name = "pyarrow" }, { name = "requests" }, @@ -4933,8 +4852,7 @@ version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, - { name = "fsspec", version = "2024.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, - { name = "fsspec", version = "2024.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, + { name = "fsspec" }, { name = "jinja2" }, { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "networkx", version = "3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -5074,8 +4992,7 @@ accelerate = [ ] agents = [ { name = "accelerate" }, - { name = "datasets", version = "2.14.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "datasets", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, + { name = "datasets" }, { name = "diffusers" }, { name = "opencv-python" }, { name = "pillow" }, @@ -5129,8 +5046,7 @@ deepspeed-testing = [ { name = "accelerate" }, { name = "beautifulsoup4" }, { name = "cookiecutter" }, - { name = "datasets", version = "2.14.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "datasets", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, + { name = "datasets" }, { name = "deepspeed" }, { name = "dill" }, { name = "evaluate" }, @@ -5161,8 +5077,7 @@ dev-dependencies = [ { name = "beautifulsoup4" }, { name = "codecarbon" }, { name = "cookiecutter" }, - { name = "datasets", version = "2.14.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "datasets", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, + { name = "datasets" }, { name = "decord" }, { name = "dill" }, { name = "evaluate" }, @@ -5300,8 +5215,7 @@ optuna = [ { name = "optuna" }, ] quality = [ - { name = "datasets", version = "2.14.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "datasets", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, + { name = "datasets" }, { name = "gitpython" }, { name = "hf-doc-builder" }, { name = "isort" }, @@ -5312,8 +5226,7 @@ ray = [ { name = "ray", extra = ["tune"] }, ] retrieval = [ - { name = "datasets", version = "2.14.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13' or sys_platform == 'darwin'" }, - { name = "datasets", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and sys_platform != 'darwin'" }, + { name = "datasets" }, { name = "faiss-cpu" }, ] sagemaker = [ diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-uv-lock-output.snap index f1a57fbd8613..a3408eda5ba2 100644 --- a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-uv-lock-output.snap +++ b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-uv-lock-output.snap @@ -7,4 +7,4 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -Resolved 288 packages in [TIME] +Resolved 286 packages in [TIME] From e2c5526fbb5bf4192c6cee70ad9676fa5a3a44c6 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 9 Jan 2025 16:01:23 -0500 Subject: [PATCH 090/135] replace backoff with backon (#10442) This should be essentially the exact same behaviour, but backon is a total API redesign, so things had to be expressed slightly differently. Overall I think the code is more readable, which is nice. Fixes #10001 --- Cargo.lock | 27 +++-- Cargo.toml | 2 +- crates/uv-fs/Cargo.toml | 4 +- crates/uv-fs/src/lib.rs | 220 ++++++++++++++++++++++------------------ 4 files changed, 143 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 198ac0923b59..5bdaed13303f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,16 +295,13 @@ dependencies = [ ] [[package]] -name = "backoff" -version = "0.4.0" +name = "backon" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +checksum = "ba5289ec98f68f28dd809fd601059e6aa908bb8f6108620930828283d4ee23d7" dependencies = [ - "futures-core", - "getrandom", - "instant", - "pin-project-lite", - "rand", + "fastrand", + "gloo-timers", "tokio", ] @@ -1372,6 +1369,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "goblin" version = "0.9.2" @@ -5055,7 +5064,7 @@ dependencies = [ name = "uv-fs" version = "0.0.1" dependencies = [ - "backoff", + "backon", "cachedir", "dunce", "either", diff --git a/Cargo.toml b/Cargo.toml index bae813d11bda..6bba2c93e93b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ async-trait = { version = "0.1.82" } async_http_range_reader = { version = "0.9.1" } async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "c909fda63fcafe4af496a07bfda28a5aae97e58d", features = ["deflate", "tokio"] } axoupdater = { version = "0.9.0", default-features = false } -backoff = { version = "0.4.0" } +backon = { version = "1.3.0" } base64 = { version = "0.22.1" } bitflags = { version = "2.6.0" } boxcar = { version = "0.2.5" } diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index a77c71e10907..8c58022ac12c 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -38,9 +38,9 @@ winsafe = { workspace = true } rustix = { workspace = true } [target.'cfg(windows)'.dependencies] -backoff = { workspace = true } +backon = { workspace = true } junction = { workspace = true } [features] default = [] -tokio = ["dep:tokio", "fs-err/tokio", "backoff/tokio"] +tokio = ["dep:tokio", "fs-err/tokio"] diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index 31abae0e3ce1..f7dadb6aeddb 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -187,10 +187,15 @@ pub fn copy_atomic_sync(from: impl AsRef, to: impl AsRef) -> std::io } #[cfg(windows)] -fn backoff_file_move() -> backoff::ExponentialBackoff { - backoff::ExponentialBackoffBuilder::default() - .with_initial_interval(std::time::Duration::from_millis(10)) - .with_max_elapsed_time(Some(std::time::Duration::from_secs(10))) +fn backoff_file_move() -> backon::ExponentialBackoff { + use backon::BackoffBuilder; + // This amounts to 10 total seconds of trying the operation. + // We start at 10 milliseconds and try 9 times, doubling each time, so the last try will take + // about 10*(2^9) milliseconds ~= 5 seconds. All other attempts combined should equal + // the length of the last attempt (because it's a sum of powers of 2), so 10 seconds overall. + backon::ExponentialBuilder::default() + .with_min_delay(std::time::Duration::from_millis(10)) + .with_max_times(9) .build() } @@ -202,6 +207,7 @@ pub async fn rename_with_retry( ) -> Result<(), std::io::Error> { #[cfg(windows)] { + use backon::Retryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. @@ -210,23 +216,21 @@ pub async fn rename_with_retry( let from = from.as_ref(); let to = to.as_ref(); - let backoff = backoff_file_move(); - backoff::future::retry(backoff, || async move { - match fs_err::rename(from, to) { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { - warn!( - "Retrying rename from {} to {} due to transient error: {}", - from.display(), - to.display(), - err - ); - Err(backoff::Error::transient(err)) - } - Err(err) => Err(backoff::Error::permanent(err)), - } - }) - .await + let rename = || async { fs_err::rename(from, to) }; + + rename + .retry(backoff_file_move()) + .sleep(tokio::time::sleep) + .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied) + .notify(|err, _dur| { + warn!( + "Retrying rename from {} to {} due to transient error: {}", + from.display(), + to.display(), + err + ); + }) + .await } #[cfg(not(windows))] { @@ -241,6 +245,7 @@ pub fn rename_with_retry_sync( ) -> Result<(), std::io::Error> { #[cfg(windows)] { + use backon::BlockingRetryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. @@ -248,32 +253,32 @@ pub fn rename_with_retry_sync( // See: & let from = from.as_ref(); let to = to.as_ref(); + let rename = || fs_err::rename(from, to); - let backoff = backoff_file_move(); - backoff::retry(backoff, || match fs_err::rename(from, to) { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { + rename + .retry(backoff_file_move()) + .sleep(std::thread::sleep) + .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied) + .notify(|err, _dur| { warn!( "Retrying rename from {} to {} due to transient error: {}", from.display(), to.display(), err ); - Err(backoff::Error::transient(err)) - } - Err(err) => Err(backoff::Error::permanent(err)), - }) - .map_err(|err| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!( - "Failed to rename {} to {}: {}", - from.display(), - to.display(), - err - ), - ) - }) + }) + .call() + .map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to rename {} to {}: {}", + from.display(), + to.display(), + err + ), + ) + }) } #[cfg(not(windows))] { @@ -281,6 +286,15 @@ pub fn rename_with_retry_sync( } } +/// Why a file persist failed +#[cfg(windows)] +enum PersistRetryError { + /// Something went wrong while persisting, maybe retry (contains error message) + Persist(String), + /// Something went wrong trying to retrieve the file to persist, we must bail + LostState, +} + /// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system errors, in a synchronous context. pub async fn persist_with_retry( from: NamedTempFile, @@ -288,6 +302,7 @@ pub async fn persist_with_retry( ) -> Result<(), std::io::Error> { #[cfg(windows)] { + use backon::Retryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. @@ -300,52 +315,55 @@ pub async fn persist_with_retry( // So we will update the `from` optional value in safe and borrow-checker friendly way every retry // Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry let mut from = Some(from); - - let backoff = backoff_file_move(); - let persisted = backoff::future::retry(backoff, move || { + let persist = move || { // Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block - let mut from = from.take(); + let mut from: Option = from.take(); async move { if let Some(file) = from.take() { file.persist(to).map_err(|err| { let error_message = err.to_string(); - warn!( - "Retrying to persist temporary file to {}: {}", - to.display(), - error_message - ); - // Set back the NamedTempFile returned back by the Error from = Some(err.file); - - backoff::Error::transient(std::io::Error::new( - std::io::ErrorKind::Other, - format!( - "Failed to persist temporary file to {}: {}", - to.display(), - error_message - ), - )) + PersistRetryError::Persist(error_message) }) } else { - Err(backoff::Error::permanent(std::io::Error::new( - std::io::ErrorKind::Other, - format!( - "Failed to retrieve temporary file while trying to persist to {}", - to.display() - ), - ))) + Err(PersistRetryError::LostState) } } - }) - .await; + }; + + let persisted = persist + .retry(backoff_file_move()) + .sleep(tokio::time::sleep) + .when(|err| matches!(err, PersistRetryError::Persist(_))) + .notify(|err, _dur| { + if let PersistRetryError::Persist(error_message) = err { + warn!( + "Retrying to persist temporary file to {}: {}", + to.display(), + error_message, + ); + }; + }) + .await; match persisted { Ok(_) => Ok(()), - Err(err) => Err(std::io::Error::new( + Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::new( std::io::ErrorKind::Other, - err.to_string(), + format!( + "Failed to persist temporary file to {}: {}", + to.display(), + error_message, + ), + )), + Err(PersistRetryError::LostState) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to retrieve temporary file while trying to persist to {}", + to.display() + ), )), } } @@ -362,6 +380,7 @@ pub fn persist_with_retry_sync( ) -> Result<(), std::io::Error> { #[cfg(windows)] { + use backon::BlockingRetryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. @@ -374,46 +393,51 @@ pub fn persist_with_retry_sync( // So we will update the `from` optional value in safe and borrow-checker friendly way every retry // Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry let mut from = Some(from); - - let backoff = backoff_file_move(); - let persisted = backoff::retry(backoff, move || { + let persist = || { + // Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block if let Some(file) = from.take() { file.persist(to).map_err(|err| { let error_message = err.to_string(); - warn!( - "Retrying to persist temporary file to {}: {}", - to.display(), - error_message - ); - // Set back the NamedTempFile returned back by the Error from = Some(err.file); - - backoff::Error::transient(std::io::Error::new( - std::io::ErrorKind::Other, - format!( - "Failed to persist temporary file to {}: {}", - to.display(), - error_message - ), - )) + PersistRetryError::Persist(error_message) }) } else { - Err(backoff::Error::permanent(std::io::Error::new( - std::io::ErrorKind::Other, - format!( - "Failed to retrieve temporary file while trying to persist to {}", - to.display() - ), - ))) + Err(PersistRetryError::LostState) } - }); + }; + + let persisted = persist + .retry(backoff_file_move()) + .sleep(std::thread::sleep) + .when(|err| matches!(err, PersistRetryError::Persist(_))) + .notify(|err, _dur| { + if let PersistRetryError::Persist(error_message) = err { + warn!( + "Retrying to persist temporary file to {}: {}", + to.display(), + error_message, + ); + }; + }) + .call(); match persisted { Ok(_) => Ok(()), - Err(err) => Err(std::io::Error::new( + Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to persist temporary file to {}: {}", + to.display(), + error_message, + ), + )), + Err(PersistRetryError::LostState) => Err(std::io::Error::new( std::io::ErrorKind::Other, - err.to_string(), + format!( + "Failed to retrieve temporary file while trying to persist to {}", + to.display() + ), )), } } From 7096e83812fc5103008a60f21fbcf323eeddac28 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 16:19:49 -0500 Subject: [PATCH 091/135] Respect sentinels in prioritization (#10443) ## Summary If a user provides a constraint like `flask==3.0.0`, that gets expanded to `[3.0.0, 3.0.0+[max])`. So it's not a _singleton_, but it should be treated as such for the purposes of prioritization, since in practice it will almost always map to a single version. --- crates/uv-resolver/src/error.rs | 23 +++++++++++++--- crates/uv-resolver/src/pubgrub/priority.rs | 20 +++++++------- crates/uv-resolver/src/version_map.rs | 2 -- crates/uv/tests/it/pip_install_scenarios.rs | 8 +++--- ...it__ecosystem__transformers-lock-file.snap | 27 +++++++++---------- 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index de9c15646a9a..be35a4fc9ef5 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -235,7 +235,7 @@ impl NoSolutionError { match derivation_tree { DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree), DerivationTree::External(External::NoVersions(package, versions)) => { - if SentinelRange::from(&versions).is_sentinel() { + if SentinelRange::from(&versions).is_complement() { return None; } @@ -977,13 +977,30 @@ impl<'range> From<&'range Range> for SentinelRange<'range> { } impl SentinelRange<'_> { - /// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]`. + /// Returns `true` if the range appears to be, e.g., `>=1.0.0, <1.0.0+[max]`. pub fn is_sentinel(&self) -> bool { + self.0.iter().all(|(lower, upper)| { + let (Bound::Included(lower), Bound::Excluded(upper)) = (lower, upper) else { + return false; + }; + if !lower.local().is_empty() { + return false; + } + if upper.local() != LocalVersionSlice::Max { + return false; + } + *lower == upper.clone().without_local() + }) + } + + /// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]` (i.e., a sentinel + /// range with the non-local version removed). + pub fn is_complement(&self) -> bool { self.0.iter().all(|(lower, upper)| { let (Bound::Excluded(lower), Bound::Excluded(upper)) = (lower, upper) else { return false; }; - if lower.local() == LocalVersionSlice::Max { + if !lower.local().is_empty() { return false; } if upper.local() != LocalVersionSlice::Max { diff --git a/crates/uv-resolver/src/pubgrub/priority.rs b/crates/uv-resolver/src/pubgrub/priority.rs index 80b1acc38c5b..5fc5a97575b6 100644 --- a/crates/uv-resolver/src/pubgrub/priority.rs +++ b/crates/uv-resolver/src/pubgrub/priority.rs @@ -10,6 +10,7 @@ use uv_pep440::Version; use crate::fork_urls::ForkUrls; use crate::pubgrub::package::PubGrubPackage; use crate::pubgrub::PubGrubPackageInner; +use crate::SentinelRange; /// A prioritization map to guide the PubGrub resolution process. /// @@ -57,8 +58,9 @@ impl PubGrubPriorities { // Compute the priority. let priority = if urls.get(name).is_some() { PubGrubPriority::DirectUrl(Reverse(index)) - } else if version.as_singleton().is_some() { - // TODO(charlie): Take local version ranges into account (e.g., `[2.0, 2.0+[max])`). + } else if version.as_singleton().is_some() + || SentinelRange::from(version).is_sentinel() + { PubGrubPriority::Singleton(Reverse(index)) } else { // Keep the conflict-causing packages to avoid loops where we seesaw between @@ -81,8 +83,9 @@ impl PubGrubPriorities { // Compute the priority. let priority = if urls.get(name).is_some() { PubGrubPriority::DirectUrl(Reverse(next)) - } else if version.as_singleton().is_some() { - // TODO(charlie): Take local version ranges into account (e.g., `[2.0, 2.0+[max])`). + } else if version.as_singleton().is_some() + || SentinelRange::from(version).is_sentinel() + { PubGrubPriority::Singleton(Reverse(next)) } else { PubGrubPriority::Unspecified(Reverse(next)) @@ -140,10 +143,7 @@ impl PubGrubPriorities { }; match self.package_priority.entry(name.clone()) { std::collections::hash_map::Entry::Occupied(mut entry) => { - if matches!( - entry.get(), - PubGrubPriority::ConflictEarly(_) | PubGrubPriority::Singleton(_) - ) { + if matches!(entry.get(), PubGrubPriority::ConflictEarly(_)) { // Already in the right category return false; }; @@ -178,9 +178,7 @@ impl PubGrubPriorities { // The ConflictEarly` match avoids infinite loops. if matches!( entry.get(), - PubGrubPriority::ConflictLate(_) - | PubGrubPriority::ConflictEarly(_) - | PubGrubPriority::Singleton(_) + PubGrubPriority::ConflictLate(_) | PubGrubPriority::ConflictEarly(_) ) { // Already in the right category return false; diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 36ece551f3aa..bf9a87983242 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -162,8 +162,6 @@ impl VersionMap { range: &Ranges, ) -> impl DoubleEndedIterator { // Performance optimization: If we only have a single version, return that version directly. - // - // TODO(charlie): Now that we use local version sentinels, does this ever trigger? if let Some(version) = range.as_singleton() { either::Either::Left(match self.inner { VersionMapInner::Eager(ref eager) => { diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index 4dce0c7337e2..efd65ae30cca 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -531,17 +531,17 @@ fn excluded_only_compatible_version() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of package-a are available: + ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available: package-a==1.0.0 package-a==2.0.0 package-a==3.0.0 - and package-a==1.0.0 depends on package-b==1.0.0, we can conclude that package-a<2.0.0 depends on package-b==1.0.0. + we can conclude that package-a<2.0.0 depends on package-b==1.0.0. And because package-a==3.0.0 depends on package-b==3.0.0, we can conclude that all of: package-a<2.0.0 package-a>2.0.0 depend on one of: - package-b==1.0.0 - package-b==3.0.0 + package-b<=1.0.0 + package-b>=3.0.0 And because you require one of: package-a<2.0.0 diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap index c0b0a9ba8d5b..2ed54f7ce7e7 100644 --- a/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap +++ b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap @@ -3001,20 +3001,18 @@ wheels = [ [[package]] name = "protobuf" -version = "3.20.3" +version = "4.25.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/55/5b/e3d951e34f8356e5feecacd12a8e3b258a1da6d9a03ad1770f28925f29bc/protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2", size = 216768 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ab/cb61a4b87b2e7e6c312dce33602bd5884797fd054e0e53205f1c27cf0f66/protobuf-4.25.4.tar.gz", hash = "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d", size = 380283 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/55/b80e8567ec327c060fa39b242392e25690c8899c489ecd7bb65b46b7bb55/protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99", size = 918427 }, - { url = "https://files.pythonhosted.org/packages/31/be/80a9c6f16dfa4d41be3edbe655349778ae30882407fa8275eb46b4d34854/protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e", size = 1051042 }, - { url = "https://files.pythonhosted.org/packages/db/96/948d3fcc1fa816e7ae1d27af59b9d8c5c5e582f3994fd14394f31da95b99/protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c", size = 780167 }, - { url = "https://files.pythonhosted.org/packages/6f/5e/fc6feb366b0a9f28e0a2de3b062667c521cd9517d4ff55077b8f351ba2f3/protobuf-3.20.3-cp310-cp310-win_amd64.whl", hash = "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7", size = 904029 }, - { url = "https://files.pythonhosted.org/packages/00/e7/d23c439c55c90ae2e52184363162f7079ca3e7d86205b411d4e9dc266f81/protobuf-3.20.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b", size = 982826 }, - { url = "https://files.pythonhosted.org/packages/99/25/5825472ecd911f4ac2ac4e9ab039a48b6d03874e2add92fb633e080bf3eb/protobuf-3.20.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b", size = 918423 }, - { url = "https://files.pythonhosted.org/packages/c7/df/ec3ecb8c940b36121c7b77c10acebf3d1c736498aa2f1fe3b6231ee44e76/protobuf-3.20.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402", size = 1019250 }, - { url = "https://files.pythonhosted.org/packages/36/8b/433071fed0058322090a55021bdc8da76d16c7bc9823f5795797803dd6d0/protobuf-3.20.3-cp39-cp39-win32.whl", hash = "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480", size = 780270 }, - { url = "https://files.pythonhosted.org/packages/11/a5/e52b731415ad6ef3d841e9e6e337a690249e800cc7c06f0749afab26348c/protobuf-3.20.3-cp39-cp39-win_amd64.whl", hash = "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7", size = 904215 }, - { url = "https://files.pythonhosted.org/packages/8d/14/619e24a4c70df2901e1f4dbc50a6291eb63a759172558df326347dce1f0d/protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db", size = 162128 }, + { url = "https://files.pythonhosted.org/packages/c8/43/27b48d9040763b78177d3083e16c70dba6e3c3ee2af64b659f6332c2b06e/protobuf-4.25.4-cp310-abi3-win32.whl", hash = "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4", size = 392409 }, + { url = "https://files.pythonhosted.org/packages/0c/d4/589d673ada9c4c62d5f155218d7ff7ac796efb9c6af95b0bd29d438ae16e/protobuf-4.25.4-cp310-abi3-win_amd64.whl", hash = "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d", size = 413398 }, + { url = "https://files.pythonhosted.org/packages/34/ca/bf85ffe3dd16f1f2aaa6c006da8118800209af3da160ae4d4f47500eabd9/protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b", size = 394160 }, + { url = "https://files.pythonhosted.org/packages/68/1d/e8961af9a8e534d66672318d6b70ea8e3391a6b13e16a29b039e4a99c214/protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835", size = 293700 }, + { url = "https://files.pythonhosted.org/packages/ca/6c/cc7ab2fb3a4a7f07f211d8a7bbb76bba633eb09b148296dbd4281e217f95/protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040", size = 294612 }, + { url = "https://files.pythonhosted.org/packages/a4/b5/f7e2460dec8347d67e6108bef6ad3291c76e38c898a1087e2c836c02951e/protobuf-4.25.4-cp39-cp39-win32.whl", hash = "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca", size = 392490 }, + { url = "https://files.pythonhosted.org/packages/c7/0b/15bd1a224e5e5744a0dcccf11bcd5dc1405877be38e477b1359d7c2c3737/protobuf-4.25.4-cp39-cp39-win_amd64.whl", hash = "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f", size = 413357 }, + { url = "https://files.pythonhosted.org/packages/b5/95/0ba7f66934a0a798006f06fc3d74816da2b7a2bcfd9b98c53d26f684c89e/protobuf-4.25.4-py3-none-any.whl", hash = "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978", size = 156464 }, ] [[package]] @@ -4700,18 +4698,17 @@ wheels = [ [[package]] name = "tf2onnx" -version = "1.16.1" +version = "1.8.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, { name = "numpy" }, { name = "onnx" }, - { name = "protobuf" }, { name = "requests" }, { name = "six" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/48/826db3d02645d84e7ee5d5ce8407f771057d40fe224d9c3e89536674ccef/tf2onnx-1.16.1-py3-none-any.whl", hash = "sha256:90fb5f62575896d47884d27dc313cfebff36b8783e1094335ad00824ce923a8a", size = 455820 }, + { url = "https://files.pythonhosted.org/packages/db/32/33ce509a79c207a39cf04bfa3ec3353da15d1e6553a6ad912f117cc29130/tf2onnx-1.8.4-py3-none-any.whl", hash = "sha256:1ebabb96c914da76e23222b6107a8b248a024bf259d77f027e6690099512d457", size = 345298 }, ] [[package]] From d77598a08ca230350d1023c8e0e3f0cfcd85e704 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 16:43:09 -0500 Subject: [PATCH 092/135] Use Windows-specific instructions in Jupyter guide (#10446) ## Summary Closes https://github.com/astral-sh/uv/issues/10407. --- docs/guides/integration/jupyter.md | 28 +++++++++++++++++++++------- docs/guides/projects.md | 23 +++++++++++++++++------ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/guides/integration/jupyter.md b/docs/guides/integration/jupyter.md index f1292a58123c..a81890287811 100644 --- a/docs/guides/integration/jupyter.md +++ b/docs/guides/integration/jupyter.md @@ -106,12 +106,23 @@ If you need to run Jupyter in a virtual environment that isn't associated with a [project](../../concepts/projects/index.md) (e.g., has no `pyproject.toml` or `uv.lock`), you can do so by adding Jupyter to the environment directly. For example: -```console -$ uv venv --seed -$ uv pip install pydantic -$ uv pip install jupyterlab -$ .venv/bin/jupyter lab -``` +=== "macOS and Linux" + + ```console + $ uv venv --seed + $ uv pip install pydantic + $ uv pip install jupyterlab + $ .venv/bin/jupyter lab + ``` + +=== "Windows" + + ```powershell + uv venv --seed + uv pip install pydantic + uv pip install jupyterlab + .venv\Scripts\jupyter lab + ``` From here, `import pydantic` will work within the notebook, and you can install additional packages via `!uv pip install`, or even `!pip install`. @@ -125,10 +136,13 @@ project, as in the following: ```console # Create a project. $ uv init project + # Move into the project directory. $ cd project + # Add ipykernel as a dev dependency. $ uv add --dev ipykernel + # Open the project in VS Code. $ code . ``` @@ -136,7 +150,7 @@ $ code . Once the project directory is open in VS Code, you can create a new Jupyter notebook by selecting "Create: New Jupyter Notebook" from the command palette. When prompted to select a kernel, choose "Python Environments" and select the virtual environment you created earlier (e.g., -`.venv/bin/python`). +`.venv/bin/python` on macOS and Linux, or `.venv\Scripts\python` on Windows). !!! note diff --git a/docs/guides/projects.md b/docs/guides/projects.md index 867a25238b44..a9d219162b55 100644 --- a/docs/guides/projects.md +++ b/docs/guides/projects.md @@ -177,12 +177,23 @@ $ uv run example.py Alternatively, you can use `uv sync` to manually update the environment then activate it before executing a command: -```console -$ uv sync -$ source .venv/bin/activate -$ flask run -p 3000 -$ python example.py -``` +=== "macOS and Linux" + + ```console + $ uv sync + $ source .venv/bin/activate + $ flask run -p 3000 + $ python example.py + ``` + +=== "Windows" + + ```powershell + uv sync + source .venv\Scripts\activate + flask run -p 3000 + python example.py + ``` !!! note From 452cafc63988e797f4fc897112a911196ba47e45 Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Thu, 9 Jan 2025 22:48:06 +0100 Subject: [PATCH 093/135] Improve tool list output when tool environment is broken (#10409) ## Summary Closes #9579 ## Test Plan `cargo test` Screenshot 2025-01-08 at 21 13 11 --- crates/uv/src/commands/tool/list.rs | 9 ++++++++- crates/uv/tests/it/tool_list.rs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/tool/list.rs b/crates/uv/src/commands/tool/list.rs index 35e5860af239..721ed9de25be 100644 --- a/crates/uv/src/commands/tool/list.rs +++ b/crates/uv/src/commands/tool/list.rs @@ -51,7 +51,14 @@ pub(crate) async fn list( let version = match installed_tools.version(&name, cache) { Ok(version) => version, Err(e) => { - writeln!(printer.stderr(), "{e}")?; + if let uv_tool::Error::EnvironmentError(e) = e { + warn_user!( + "{e} (run `{}` to reinstall)", + format!("uv tool install {name} --reinstall").green() + ); + } else { + writeln!(printer.stderr(), "{e}")?; + } continue; } }; diff --git a/crates/uv/tests/it/tool_list.rs b/crates/uv/tests/it/tool_list.rs index 71e17f87c78a..d0b0d6b9389f 100644 --- a/crates/uv/tests/it/tool_list.rs +++ b/crates/uv/tests/it/tool_list.rs @@ -156,7 +156,7 @@ fn tool_list_bad_environment() -> Result<()> { - ruff ----- stderr ----- - Invalid environment at `tools/black`: missing Python executable at `tools/black/[BIN]/python` + warning: Invalid environment at `tools/black`: missing Python executable at `tools/black/[BIN]/python` (run `uv tool install black --reinstall` to reinstall) "### ); From 129a75e2d08df0b992a21d98d660bc1212dea468 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 17:39:00 -0500 Subject: [PATCH 094/135] Add meta descriptions for remaining guides (#10450) ## Summary Closes https://github.com/astral-sh/uv/issues/10418. --- docs/guides/integration/alternative-indexes.md | 7 +++++++ docs/guides/integration/dependency-bots.md | 5 +++++ docs/guides/integration/github.md | 7 +++++++ docs/guides/integration/gitlab.md | 6 ++++++ docs/guides/integration/pytorch.md | 7 +++++++ 5 files changed, 32 insertions(+) diff --git a/docs/guides/integration/alternative-indexes.md b/docs/guides/integration/alternative-indexes.md index 9e8142891056..b686ed5a9b67 100644 --- a/docs/guides/integration/alternative-indexes.md +++ b/docs/guides/integration/alternative-indexes.md @@ -1,3 +1,10 @@ +--- +title: Using alternative package indexes +description: + A guide to using alternative package indexes with uv, including Azure Artifacts, Google Artifact + Registry, AWS CodeArtifact, and more. +--- + # Using alternative package indexes While uv uses the official Python Package Index (PyPI) by default, it also supports alternative diff --git a/docs/guides/integration/dependency-bots.md b/docs/guides/integration/dependency-bots.md index 63cff791052d..90a45f61e1eb 100644 --- a/docs/guides/integration/dependency-bots.md +++ b/docs/guides/integration/dependency-bots.md @@ -1,3 +1,8 @@ +--- +title: Using uv with dependency bots +description: A guide to using uv with dependency bots like Renovate and Dependabot. +--- + # Dependency bots It is considered best practice to regularly update dependencies, to avoid being exposed to diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 26204abd1f0c..f8543c7750cc 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -1,3 +1,10 @@ +--- +title: Using uv in GitHub Actions +description: + A guide to using uv in GitHub Actions, including installation, setting up Python, installing + dependencies, and more. +--- + # Using uv in GitHub Actions ## Installation diff --git a/docs/guides/integration/gitlab.md b/docs/guides/integration/gitlab.md index 60bff070e9f5..ef84efbcf7c0 100644 --- a/docs/guides/integration/gitlab.md +++ b/docs/guides/integration/gitlab.md @@ -1,3 +1,9 @@ +--- +title: Using uv in GitLab CI/CD +description: A guide to using uv in GitLab CI/CD, including installation, setting up Python, + installing dependencies, and more. +--- + # Using uv in GitLab CI/CD ## Using the uv image diff --git a/docs/guides/integration/pytorch.md b/docs/guides/integration/pytorch.md index 8b71d8567e41..ea1104706261 100644 --- a/docs/guides/integration/pytorch.md +++ b/docs/guides/integration/pytorch.md @@ -1,3 +1,10 @@ +--- +title: Using uv with PyTorch +description: + A guide to using uv with PyTorch, including installing PyTorch, configuring per-platform and + per-accelerator builds, and more. +--- + # Using uv with PyTorch The [PyTorch](https://pytorch.org/) ecosystem is a popular choice for deep learning research and From 22222e945f28f4e0d2e7ba70605620caa7b744cc Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 9 Jan 2025 16:39:37 -0600 Subject: [PATCH 095/135] Allow reading `--with-requirements` from stdin in `uv add` and `uv run` (#10447) For some reason this was banned when originally added (I did not see discussion about it). I think it's fine to allow. With `uv run`, there's a bit of nuance because we also allow the script to be read from stdin. --- crates/uv/src/commands/project/add.rs | 5 ---- crates/uv/src/commands/project/run.rs | 15 ++++++++++- crates/uv/tests/it/edit.rs | 13 +++++++++ crates/uv/tests/it/run.rs | 38 ++++++++++++++++++++++++--- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 227bf676faed..0191c3a0745c 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -98,11 +98,6 @@ pub(crate) async fn add( RequirementsSource::SetupCfg(_) => { bail!("Adding requirements from a `setup.cfg` is not supported in `uv add`"); } - RequirementsSource::RequirementsTxt(path) => { - if path == Path::new("-") { - bail!("Reading requirements from stdin is not supported in `uv add`"); - } - } _ => {} } } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 27a2e22b1bcb..dd327ede6c91 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -93,6 +93,7 @@ pub(crate) async fn run( ) -> anyhow::Result { // These cases seem quite complex because (in theory) they should change the "current package". // Let's ban them entirely for now. + let mut requirements_from_stdin: bool = false; for source in &requirements { match source { RequirementsSource::PyprojectToml(_) => { @@ -106,13 +107,22 @@ pub(crate) async fn run( } RequirementsSource::RequirementsTxt(path) => { if path == Path::new("-") { - bail!("Reading requirements from stdin is not supported in `uv run`"); + requirements_from_stdin = true; } } _ => {} } } + // Fail early if stdin is used for multiple purposes. + if matches!( + command, + Some(RunCommand::PythonStdin(..) | RunCommand::PythonGuiStdin(..)) + ) && requirements_from_stdin + { + bail!("Cannot read both requirements file and script from stdin"); + } + // Initialize any shared state. let state = SharedState::default(); @@ -169,6 +179,9 @@ pub(crate) async fn run( )?; } Pep723Item::Stdin(_) => { + if requirements_from_stdin { + bail!("Cannot read both requirements file and script from stdin"); + } writeln!( printer.stderr(), "Reading inline script metadata from `{}`", diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index e3e1d8b39012..537f2f83d454 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_types)] + use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; @@ -4611,6 +4613,17 @@ fn add_requirements_file() -> Result<()> { ); }); + // Passing stdin should succeed + uv_snapshot!(context.filters(), context.add().arg("-r").arg("-").stdin(std::fs::File::open(requirements_txt)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Audited [N] packages in [TIME] + "###); + // Passing a `setup.py` should fail. uv_snapshot!(context.filters(), context.add().arg("-r").arg("setup.py"), @r###" success: false diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index fd506a7a2853..d915d58522b7 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -2124,19 +2124,51 @@ fn run_requirements_txt() -> Result<()> { + sniffio==1.3.1 "###); - // But reject `-` as a requirements file. + // Allow `-` for stdin. uv_snapshot!(context.filters(), context.run() .arg("--with-requirements") .arg("-") .arg("--with") .arg("iniconfig") - .arg("main.py"), @r###" + .arg("main.py") + .stdin(std::fs::File::open(&requirements_txt)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + Resolved 2 packages in [TIME] + "###); + + // But not in combination with reading the script from stdin + uv_snapshot!(context.filters(), context.run() + .arg("--with-requirements") + .arg("-") + // The script to run + .arg("-") + .stdin(std::fs::File::open(&requirements_txt)?), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Cannot read both requirements file and script from stdin + "###); + + uv_snapshot!(context.filters(), context.run() + .arg("--with-requirements") + .arg("-") + .arg("--script") + .arg("-") + .stdin(std::fs::File::open(&requirements_txt)?), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Reading requirements from stdin is not supported in `uv run` + error: Cannot read both requirements file and script from stdin "###); Ok(()) From c5e536f0ec66515e20dacf237817914f3b56fc79 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 17:45:13 -0500 Subject: [PATCH 096/135] De-duplicate result handling in Simple API responses (#10449) ## Summary See: https://github.com/astral-sh/uv/pull/10432#issuecomment-2581084234 --- crates/uv-client/src/registry_client.rs | 122 +++++++++++------------- 1 file changed, 54 insertions(+), 68 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index d9812253a2d4..6901123abe07 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -253,80 +253,36 @@ impl RegistryClient { // If we're searching for the first index that contains the package, fetch serially. IndexStrategy::FirstIndex => { for index in it { - match self.simple_single_index(package_name, index).await { - Ok(metadata) => { - results.push((index, metadata)); - break; - } - Err(err) => match err.into_kind() { - // The package could not be found in the remote index. - ErrorKind::WrappedReqwestError(url, err) => match err.status() { - Some(StatusCode::NOT_FOUND) => {} - Some(StatusCode::UNAUTHORIZED) => { - capabilities.set_unauthorized(index.clone()); - } - Some(StatusCode::FORBIDDEN) => { - capabilities.set_forbidden(index.clone()); - } - _ => return Err(ErrorKind::WrappedReqwestError(url, err).into()), - }, - - // The package is unavailable due to a lack of connectivity. - ErrorKind::Offline(_) => {} - - // The package could not be found in the local index. - ErrorKind::FileNotFound(_) => {} - - err => return Err(err.into()), - }, - }; + if let Some(metadata) = self + .simple_single_index(package_name, index, capabilities) + .await? + { + results.push((index, metadata)); + break; + } } } // Otherwise, fetch concurrently. IndexStrategy::UnsafeBestMatch | IndexStrategy::UnsafeFirstMatch => { - let fetches = futures::stream::iter(it) + // TODO(charlie): Respect concurrency limits. + results = futures::stream::iter(it) .map(|index| async move { - match self.simple_single_index(package_name, index).await { - Ok(metadata) => Ok(Some((index, metadata))), - Err(err) => match err.into_kind() { - // The package could not be found in the remote index. - ErrorKind::WrappedReqwestError(url, err) => match err.status() { - Some(StatusCode::NOT_FOUND) => Ok(None), - Some(StatusCode::UNAUTHORIZED) => { - capabilities.set_unauthorized(index.clone()); - Ok(None) - } - Some(StatusCode::FORBIDDEN) => { - capabilities.set_forbidden(index.clone()); - Ok(None) - } - _ => Err(ErrorKind::WrappedReqwestError(url, err).into()), - }, - - // The package is unavailable due to a lack of connectivity. - ErrorKind::Offline(_) => Ok(None), - - // The package could not be found in the local index. - ErrorKind::FileNotFound(_) => Ok(None), - - err => Err(err.into()), - }, - } + let metadata = self + .simple_single_index(package_name, index, capabilities) + .await?; + Ok((index, metadata)) }) - .buffered(8); - - futures::pin_mut!(fetches); - - while let Some(result) = fetches.next().await { - match result { - Ok(Some((index, metadata))) => { - results.push((index, metadata)); + .buffered(8) + .filter_map(|result: Result<_, Error>| async move { + match result { + Ok((index, Some(metadata))) => Some(Ok((index, metadata))), + Ok((_, None)) => None, + Err(err) => Some(Err(err)), } - Ok(None) => continue, - Err(err) => return Err(err), - } - } + }) + .try_collect::>() + .await?; } } @@ -346,11 +302,14 @@ impl RegistryClient { /// /// The index can either be a PEP 503-compatible remote repository, or a local directory laid /// out in the same format. + /// + /// Returns `Ok(None)` if the package is not found in the index. async fn simple_single_index( &self, package_name: &PackageName, index: &IndexUrl, - ) -> Result, Error> { + capabilities: &IndexCapabilities, + ) -> Result>, Error> { // Format the URL for PyPI. let mut url: Url = index.clone().into(); url.path_segments_mut() @@ -377,11 +336,38 @@ impl RegistryClient { Connectivity::Offline => CacheControl::AllowStale, }; - if matches!(index, IndexUrl::Path(_)) { + let result = if matches!(index, IndexUrl::Path(_)) { self.fetch_local_index(package_name, &url).await } else { self.fetch_remote_index(package_name, &url, &cache_entry, cache_control) .await + }; + + match result { + Ok(metadata) => Ok(Some(metadata)), + Err(err) => match err.into_kind() { + // The package could not be found in the remote index. + ErrorKind::WrappedReqwestError(url, err) => match err.status() { + Some(StatusCode::NOT_FOUND) => Ok(None), + Some(StatusCode::UNAUTHORIZED) => { + capabilities.set_unauthorized(index.clone()); + Ok(None) + } + Some(StatusCode::FORBIDDEN) => { + capabilities.set_forbidden(index.clone()); + Ok(None) + } + _ => Err(ErrorKind::WrappedReqwestError(url, err).into()), + }, + + // The package is unavailable due to a lack of connectivity. + ErrorKind::Offline(_) => Ok(None), + + // The package could not be found in the local index. + ErrorKind::FileNotFound(_) => Ok(None), + + err => Err(err.into()), + }, } } From 7bf514d8862c5166f0b16fd327938118c8b1b8aa Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Jan 2025 22:32:30 -0500 Subject: [PATCH 097/135] Remove `get_with_version` methods (#10456) ## Summary I think these are vestigial. --- crates/uv-resolver/src/version_map.rs | 29 ++++----------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index bf9a87983242..715d9ab5d27d 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -115,23 +115,9 @@ impl VersionMap { /// Return the [`DistFile`] for the given version, if any. pub(crate) fn get(&self, version: &Version) -> Option<&PrioritizedDist> { - self.get_with_version(version).map(|(_version, dist)| dist) - } - - /// Return the [`DistFile`] and the `Version` from the map for the given - /// version, if any. - /// - /// This is useful when you depend on access to the specific `Version` - /// stored in this map. For example, the versions `1.2.0` and `1.2` are - /// semantically equivalent, but when converted to strings, they are - /// distinct. - pub(crate) fn get_with_version( - &self, - version: &Version, - ) -> Option<(&Version, &PrioritizedDist)> { match self.inner { - VersionMapInner::Eager(ref eager) => eager.map.get_key_value(version), - VersionMapInner::Lazy(ref lazy) => lazy.get_with_version(version), + VersionMapInner::Eager(ref eager) => eager.map.get(version), + VersionMapInner::Lazy(ref lazy) => lazy.get(version), } } @@ -349,16 +335,9 @@ struct VersionMapLazy { impl VersionMapLazy { /// Returns the distribution for the given version, if it exists. fn get(&self, version: &Version) -> Option<&PrioritizedDist> { - self.get_with_version(version) - .map(|(_, prioritized_dist)| prioritized_dist) - } - - /// Returns the distribution for the given version along with the version - /// in this map, if it exists. - fn get_with_version(&self, version: &Version) -> Option<(&Version, &PrioritizedDist)> { - let (version, lazy_dist) = self.map.get_key_value(version)?; + let lazy_dist = self.map.get(version)?; let priority_dist = self.get_lazy(lazy_dist)?; - Some((version, priority_dist)) + Some(priority_dist) } /// Given a reference to a possibly-initialized distribution that is in From f4f1587549c55aac946e71b8974c6af399946bc4 Mon Sep 17 00:00:00 2001 From: Charles Tapley Hoyt Date: Fri, 10 Jan 2025 07:27:15 -0500 Subject: [PATCH 098/135] Add remaining Python type annotations to build backend (#10434) --- python/uv/_build_backend.py | 40 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/python/uv/_build_backend.py b/python/uv/_build_backend.py index 88bd54c91286..127342d8eea8 100644 --- a/python/uv/_build_backend.py +++ b/python/uv/_build_backend.py @@ -16,15 +16,19 @@ them while IDEs and type checker can see through the quotes. """ +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any # noqa:I001 -def warn_config_settings(config_settings: "dict | None" = None): + +def warn_config_settings(config_settings: "dict[Any, Any] | None" = None) -> None: import sys if config_settings: print("Warning: Config settings are not supported", file=sys.stderr) -def call(args: "list[str]", config_settings: "dict | None" = None) -> str: +def call(args: "list[str]", config_settings: "dict[Any, Any] | None" = None) -> str: """Invoke a uv subprocess and return the filename from stdout.""" import shutil import subprocess @@ -49,7 +53,9 @@ def call(args: "list[str]", config_settings: "dict | None" = None) -> str: return stdout[-1].strip() -def build_sdist(sdist_directory: str, config_settings: "dict | None" = None): +def build_sdist( + sdist_directory: str, config_settings: "dict[Any, Any] | None" = None +) -> str: """PEP 517 hook `build_sdist`.""" args = ["build-backend", "build-sdist", sdist_directory] return call(args, config_settings) @@ -57,9 +63,9 @@ def build_sdist(sdist_directory: str, config_settings: "dict | None" = None): def build_wheel( wheel_directory: str, - config_settings: "dict | None" = None, + config_settings: "dict[Any, Any] | None" = None, metadata_directory: "str | None" = None, -): +) -> str: """PEP 517 hook `build_wheel`.""" args = ["build-backend", "build-wheel", wheel_directory] if metadata_directory: @@ -67,21 +73,25 @@ def build_wheel( return call(args, config_settings) -def get_requires_for_build_sdist(config_settings: "dict | None" = None): +def get_requires_for_build_sdist( + config_settings: "dict[Any, Any] | None" = None, +) -> "list[str]": """PEP 517 hook `get_requires_for_build_sdist`.""" warn_config_settings(config_settings) return [] -def get_requires_for_build_wheel(config_settings: "dict | None" = None): +def get_requires_for_build_wheel( + config_settings: "dict[Any, Any] | None" = None, +) -> "list[str]": """PEP 517 hook `get_requires_for_build_wheel`.""" warn_config_settings(config_settings) return [] def prepare_metadata_for_build_wheel( - metadata_directory: str, config_settings: "dict | None" = None -): + metadata_directory: str, config_settings: "dict[Any, Any] | None" = None +) -> str: """PEP 517 hook `prepare_metadata_for_build_wheel`.""" args = ["build-backend", "prepare-metadata-for-build-wheel", metadata_directory] return call(args, config_settings) @@ -89,9 +99,9 @@ def prepare_metadata_for_build_wheel( def build_editable( wheel_directory: str, - config_settings: "dict | None" = None, + config_settings: "dict[Any, Any] | None" = None, metadata_directory: "str | None" = None, -): +) -> str: """PEP 660 hook `build_editable`.""" args = ["build-backend", "build-editable", wheel_directory] if metadata_directory: @@ -99,15 +109,17 @@ def build_editable( return call(args, config_settings) -def get_requires_for_build_editable(config_settings: "dict | None" = None): +def get_requires_for_build_editable( + config_settings: "dict[Any, Any] | None" = None, +) -> "list[str]": """PEP 660 hook `get_requires_for_build_editable`.""" warn_config_settings(config_settings) return [] def prepare_metadata_for_build_editable( - metadata_directory: str, config_settings: "dict | None" = None -): + metadata_directory: str, config_settings: "dict[Any, Any] | None" = None +) -> str: """PEP 660 hook `prepare_metadata_for_build_editable`.""" args = ["build-backend", "prepare-metadata-for-build-editable", metadata_directory] return call(args, config_settings) From 8d25f295af20f8c822dfbfb5ab24b6134ded4bbb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 10 Jan 2025 08:10:27 -0500 Subject: [PATCH 099/135] Only track markers for compatible versions (#10457) ## Summary We shouldn't consider incompatible distributions (e.g., those that don't match the required Python version) when determining the implied markers. --- .../src/prioritized_distribution.rs | 24 +++++++++++++------ crates/uv-pep440/src/version.rs | 2 +- crates/uv/tests/it/pip_compile.rs | 16 ++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index def371bc247c..c6261076af21 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -45,7 +45,7 @@ impl Default for PrioritizedDistInner { } /// A distribution that can be used for both resolution and installation. -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub enum CompatibleDist<'a> { /// The distribution is already installed and can be used. InstalledDist(&'a InstalledDist), @@ -314,6 +314,12 @@ impl PrioritizedDist { hashes: impl IntoIterator, compatibility: WheelCompatibility, ) { + // Track the implied markers. + if compatibility.is_compatible() { + if !self.0.markers.is_true() { + self.0.markers.or(implied_markers(&dist.filename)); + } + } // Track the highest-priority wheel. if let Some((.., existing_compatibility)) = self.best_wheel() { if compatibility.is_more_compatible(existing_compatibility) { @@ -323,9 +329,6 @@ impl PrioritizedDist { self.0.best_wheel_index = Some(self.0.wheels.len()); } self.0.hashes.extend(hashes); - if !self.0.markers.is_true() { - self.0.markers.or(implied_markers(&dist.filename)); - } self.0.wheels.push((dist, compatibility)); } @@ -336,6 +339,10 @@ impl PrioritizedDist { hashes: impl IntoIterator, compatibility: SourceDistCompatibility, ) { + // Track the implied markers. + if compatibility.is_compatible() { + self.0.markers = MarkerTree::TRUE; + } // Track the highest-priority source. if let Some((.., existing_compatibility)) = &self.0.source { if compatibility.is_more_compatible(existing_compatibility) { @@ -344,9 +351,6 @@ impl PrioritizedDist { } else { self.0.source = Some((dist, compatibility)); } - if !self.0.markers.is_true() { - self.0.markers.or(MarkerTree::TRUE); - } self.0.hashes.extend(hashes); } @@ -526,6 +530,7 @@ impl<'a> CompatibleDist<'a> { } impl WheelCompatibility { + /// Return `true` if the distribution is compatible. pub fn is_compatible(&self) -> bool { matches!(self, Self::Compatible(_, _, _)) } @@ -552,6 +557,11 @@ impl WheelCompatibility { } impl SourceDistCompatibility { + /// Return `true` if the distribution is compatible. + pub fn is_compatible(&self) -> bool { + matches!(self, Self::Compatible(_)) + } + /// Return the higher priority compatibility. /// /// Compatible source distributions are always higher priority than incompatible source distributions. diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 7eeb2ad198e1..aaf8dfc1eb18 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -1616,7 +1616,7 @@ impl LocalVersionSlice<'_> { /// > should be considered an integer for comparison purposes and if a segment contains any ASCII /// > letters then that segment is compared lexicographically with case insensitivity. When /// > comparing a numeric and lexicographic segment, the numeric section always compares as greater -/// > than the lexicographic segment. Additionally a local version with a great number of segments +/// > than the lexicographic segment. Additionally, a local version with a great number of segments /// > will always compare as greater than a local version with fewer segments, as long as the /// > shorter local version’s segments match the beginning of the longer local version’s segments /// > exactly. diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 0d997d65e98e..9fc609c5675e 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -7629,9 +7629,9 @@ fn universal_transitive_disjoint_locals() -> Result<()> { # -r requirements.in # torchvision # triton - torchvision==0.15.1 ; platform_machine != 'x86_64' or sys_platform == 'darwin' or sys_platform == 'win32' + torchvision==0.15.1 ; platform_machine != 'x86_64' or sys_platform == 'darwin' # via -r requirements.in - torchvision==0.15.1+rocm5.4.2 ; platform_machine == 'x86_64' and sys_platform != 'darwin' and sys_platform != 'win32' + torchvision==0.15.1+rocm5.4.2 ; platform_machine == 'x86_64' and sys_platform != 'darwin' # via -r requirements.in triton==2.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' # via torch @@ -8010,7 +8010,7 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { # via sympy networkx==3.2.1 # via torch - pytorch-triton-rocm==2.3.0 ; (implementation_name != 'cpython' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') + pytorch-triton-rocm==2.3.0 ; (implementation_name != 'cpython' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform != 'darwin' and sys_platform != 'linux') # via torch sympy==1.12 # via torch @@ -8021,9 +8021,9 @@ fn universal_nested_overlapping_local_requirement() -> Result<()> { # -r requirements.in # example # triton - torch==2.3.0 ; (implementation_name != 'cpython' and platform_machine == 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform == 'darwin') or (implementation_name != 'cpython' and sys_platform == 'win32') + torch==2.3.0 ; (implementation_name != 'cpython' and platform_machine == 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform == 'darwin') # via -r requirements.in - torch==2.3.0+rocm6.0 ; (implementation_name != 'cpython' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') + torch==2.3.0+rocm6.0 ; (implementation_name != 'cpython' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'cpython' and sys_platform != 'darwin' and sys_platform != 'linux') # via -r requirements.in triton==2.0.0 ; implementation_name == 'cpython' and platform_machine == 'x86_64' and sys_platform == 'linux' # via torch @@ -8174,7 +8174,7 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> { # via sympy networkx==3.2.1 # via torch - pytorch-triton-rocm==2.3.0 ; (os_name != 'Linux' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') + pytorch-triton-rocm==2.3.0 ; (os_name != 'Linux' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux') # via torch sympy==1.12 # via torch @@ -8189,9 +8189,9 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> { # -r requirements.in # example # triton - torch==2.3.0 ; (os_name != 'Linux' and platform_machine == 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform == 'darwin') or (os_name != 'Linux' and sys_platform == 'win32') + torch==2.3.0 ; (os_name != 'Linux' and platform_machine == 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform == 'darwin') # via -r requirements.in - torch==2.3.0+rocm6.0 ; (os_name != 'Linux' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32') + torch==2.3.0+rocm6.0 ; (os_name != 'Linux' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux') # via -r requirements.in triton==2.0.0 ; implementation_name == 'cpython' and os_name == 'Linux' and platform_machine == 'x86_64' and sys_platform == 'linux' # via torch From bee2baa64e37401367ffd32f81a33e2d26e68f48 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 10 Jan 2025 08:10:39 -0500 Subject: [PATCH 100/135] Misc. changes based on ABI explorations (#10458) --- .../src/prioritized_distribution.rs | 4 +-- crates/uv-platform-tags/src/platform.rs | 22 ++++++------ crates/uv-platform-tags/src/tags.rs | 34 ++++++------------- crates/uv-resolver/src/version_map.rs | 24 ++++++------- 4 files changed, 36 insertions(+), 48 deletions(-) diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index c6261076af21..61b010df26b5 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -222,7 +222,7 @@ impl Display for IncompatibleDist { } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum PythonRequirementKind { /// The installed version of Python. Installed, @@ -266,7 +266,7 @@ pub enum IncompatibleSource { NoBuild, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum HashComparison { /// The hash is present, but does not match the expected value. Mismatched, diff --git a/crates/uv-platform-tags/src/platform.rs b/crates/uv-platform-tags/src/platform.rs index 697891b19d31..b82335036286 100644 --- a/crates/uv-platform-tags/src/platform.rs +++ b/crates/uv-platform-tags/src/platform.rs @@ -56,17 +56,17 @@ pub enum Os { impl fmt::Display for Os { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Self::Manylinux { .. } => write!(f, "Manylinux"), - Self::Musllinux { .. } => write!(f, "Musllinux"), - Self::Windows => write!(f, "Windows"), - Self::Macos { .. } => write!(f, "MacOS"), - Self::FreeBsd { .. } => write!(f, "FreeBSD"), - Self::NetBsd { .. } => write!(f, "NetBSD"), - Self::OpenBsd { .. } => write!(f, "OpenBSD"), - Self::Dragonfly { .. } => write!(f, "DragonFly"), - Self::Illumos { .. } => write!(f, "Illumos"), - Self::Haiku { .. } => write!(f, "Haiku"), - Self::Android { .. } => write!(f, "Android"), + Self::Manylinux { .. } => write!(f, "manylinux"), + Self::Musllinux { .. } => write!(f, "musllinux"), + Self::Windows => write!(f, "windows"), + Self::Macos { .. } => write!(f, "macos"), + Self::FreeBsd { .. } => write!(f, "freebsd"), + Self::NetBsd { .. } => write!(f, "netbsd"), + Self::OpenBsd { .. } => write!(f, "openbsd"), + Self::Dragonfly { .. } => write!(f, "dragonfly"), + Self::Illumos { .. } => write!(f, "illumos"), + Self::Haiku { .. } => write!(f, "haiku"), + Self::Android { .. } => write!(f, "android"), } } } diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 9dc99c618af1..8a15f6f13f09 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -21,7 +21,7 @@ pub enum TagsError { GilIsACPythonProblem(String), } -#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Clone)] +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Copy, Clone)] pub enum IncompatibleTag { /// The tag is invalid and cannot be used. Invalid, @@ -35,7 +35,7 @@ pub enum IncompatibleTag { Platform, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum TagCompatibility { Incompatible(IncompatibleTag), Compatible(TagPriority), @@ -59,6 +59,7 @@ impl PartialOrd for TagCompatibility { } impl TagCompatibility { + /// Returns `true` if the tag is compatible. pub fn is_compatible(&self) -> bool { matches!(self, Self::Compatible(_)) } @@ -128,7 +129,7 @@ impl Tags { if let Implementation::CPython { gil_disabled } = implementation { // For some reason 3.2 is the minimum python for the cp abi for minor in (2..=python_version.1).rev() { - // No abi3 for freethreading python + // No abi3 for free-threading python if !gil_disabled { for platform_tag in &platform_tags { tags.push(( @@ -406,8 +407,6 @@ impl Implementation { /// /// We have two cases: Actual platform specific tags (including "merged" tags such as universal2) /// and "any". -/// -/// Bit of a mess, needs to be cleaned up. fn compatible_tags(platform: &Platform) -> Result, PlatformError> { let os = platform.os(); let arch = platform.arch(); @@ -431,13 +430,13 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformError> { } } } - // Non-manylinux is lowest priority + // Non-manylinux is given lowest priority. // platform_tags.push(format!("linux_{arch}")); platform_tags } (Os::Musllinux { major, minor }, _) => { - let mut platform_tags = vec![format!("linux_{}", arch)]; + let mut platform_tags = vec![format!("linux_{arch}")]; // musl 1.1 is the lowest supported version in musllinux platform_tags .extend((1..=*minor).map(|minor| format!("musllinux_{major}_{minor}_{arch}"))); @@ -516,12 +515,7 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformError> { _, ) => { let release = release.replace(['.', '-'], "_"); - vec![format!( - "{}_{}_{}", - os.to_string().to_lowercase(), - release, - arch - )] + vec![format!("{os}_{release}_{arch}")] } (Os::Illumos { release, arch }, _) => { // See https://github.com/python/cpython/blob/46c8d915715aa2bd4d697482aa051fe974d440e1/Lib/sysconfig.py#L722-L730 @@ -533,23 +527,17 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformError> { })?; if major_ver >= 5 { // SunOS 5 == Solaris 2 - let os = "solaris".to_string(); + let os = "solaris"; let release = format!("{}_{}", major_ver - 3, other); let arch = format!("{arch}_64bit"); - return Ok(vec![format!("{}_{}_{}", os, release, arch)]); + return Ok(vec![format!("{os}_{release}_{arch}")]); } } - let os = os.to_string().to_lowercase(); - vec![format!("{}_{}_{}", os, release, arch)] + vec![format!("{os}_{release}_{arch}")] } (Os::Android { api_level }, _) => { - vec![format!( - "{}_{}_{}", - os.to_string().to_lowercase(), - api_level, - arch - )] + vec![format!("{os}_{api_level}_{arch}")] } _ => { return Err(PlatformError::OsVersionDetectionError(format!( diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 715d9ab5d27d..5056916738fa 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -521,14 +521,22 @@ impl VersionMapLazy { } // Determine a compatibility for the wheel based on tags. - let priority = match &self.tags { - Some(tags) => match filename.compatibility(tags) { + let priority = if let Some(tags) = &self.tags { + match filename.compatibility(tags) { TagCompatibility::Incompatible(tag) => { return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag)) } TagCompatibility::Compatible(priority) => Some(priority), - }, - None => None, + } + } else { + // Check if the wheel is compatible with the `requires-python` (i.e., the Python + // ABI tag is not less than the `requires-python` minimum version). + if !self.requires_python.matches_wheel_tag(filename) { + return WheelCompatibility::Incompatible(IncompatibleWheel::Tag( + IncompatibleTag::AbiPythonVersion, + )); + } + None }; // Check if hashes line up. If hashes aren't required, they're considered matching. @@ -546,14 +554,6 @@ impl VersionMapLazy { } }; - // Check if the wheel is compatible with the `requires-python` (i.e., the Python ABI tag - // is not less than the `requires-python` minimum version). - if !self.requires_python.matches_wheel_tag(filename) { - return WheelCompatibility::Incompatible(IncompatibleWheel::Tag( - IncompatibleTag::AbiPythonVersion, - )); - } - // Break ties with the build tag. let build_tag = filename.build_tag.clone(); From 685a53d9656746832a435d68bbb4f6415fd88422 Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 10 Jan 2025 15:42:03 +0100 Subject: [PATCH 101/135] Add second apache airflow test case (#10463) --- scripts/requirements/airflow2-constraints.txt | 299 ++++++++++++++++++ scripts/requirements/airflow2-req.in | 7 + 2 files changed, 306 insertions(+) create mode 100644 scripts/requirements/airflow2-constraints.txt create mode 100644 scripts/requirements/airflow2-req.in diff --git a/scripts/requirements/airflow2-constraints.txt b/scripts/requirements/airflow2-constraints.txt new file mode 100644 index 000000000000..8fdda5d3d7c6 --- /dev/null +++ b/scripts/requirements/airflow2-constraints.txt @@ -0,0 +1,299 @@ +adal==1.2.7 +alembic==1.8.1 +amqp==5.1.1 +anyio==3.6.1 +apache-airflow==2.3.4 +apache-airflow-providers-amazon==5.0.0 +apache-airflow-providers-celery==3.0.0 +apache-airflow-providers-cncf-kubernetes==4.3.0 +apache-airflow-providers-common-sql==1.1.0 +apache-airflow-providers-docker==3.1.0 +apache-airflow-providers-elasticsearch==4.2.0 +apache-airflow-providers-ftp==3.1.0 +apache-airflow-providers-google==8.3.0 +apache-airflow-providers-grpc==3.0.0 +apache-airflow-providers-hashicorp==3.1.0 +apache-airflow-providers-http==4.0.0 +apache-airflow-providers-imap==3.0.0 +apache-airflow-providers-microsoft-azure==4.2.0 +apache-airflow-providers-mysql==3.2.0 +apache-airflow-providers-odbc==3.1.1 +apache-airflow-providers-postgres==5.2.0 +apache-airflow-providers-redis==3.0.0 +apache-airflow-providers-sendgrid==3.0.0 +apache-airflow-providers-sftp==4.0.0 +apache-airflow-providers-slack==5.1.0 +apache-airflow-providers-sqlite==3.2.0 +apache-airflow-providers-ssh==3.1.0 +apispec==3.3.2 +argcomplete==2.0.0 +asn1crypto==1.5.1 +attrs==22.1.0 +Authlib==0.15.5 +azure-batch==12.0.0 +azure-common==1.1.28 +azure-core==1.25.0 +azure-cosmos==4.3.0 +azure-datalake-store==0.0.52 +azure-identity==1.10.0 +azure-keyvault-secrets==4.5.1 +azure-kusto-data==0.0.45 +azure-mgmt-containerinstance==1.5.0 +azure-mgmt-core==1.3.2 +azure-mgmt-datafactory==1.1.0 +azure-mgmt-datalake-nspkg==3.0.1 +azure-mgmt-datalake-store==0.5.0 +azure-mgmt-nspkg==3.0.2 +azure-mgmt-resource==21.1.0 +azure-nspkg==3.0.2 +azure-servicebus==7.8.0 +azure-storage-blob==12.8.1 +azure-storage-common==2.1.0 +azure-storage-file==2.1.0 +Babel==2.10.3 +bcrypt==3.2.2 +beautifulsoup4==4.11.1 +billiard==3.6.4.0 +blinker==1.5 +boto3==1.24.56 +botocore==1.27.56 +cachelib==0.9.0 +cachetools==4.2.2 +cattrs==22.1.0 +celery==5.2.7 +certifi==2022.6.15 +cffi==1.15.1 +charset-normalizer==2.0.12 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 +clickclick==20.10.2 +cloudpickle==2.1.0 +colorama==0.4.5 +colorlog==4.8.0 +commonmark==0.9.1 +connexion==2.14.0 +cron_descriptor==1.2.31 +croniter==1.3.5 +cryptography==36.0.2 +dask==2022.8.0 +db-dtypes==1.0.3 +decorator==5.1.1 +Deprecated==1.2.13 +dill==0.3.1.1 +distlib==0.3.5 +distributed==2022.8.0 +dnspython==2.2.1 +docker==6.1.3 +docutils==0.19 +elasticsearch==7.13.4 +elasticsearch-dbapi==0.2.9 +elasticsearch-dsl==7.4.0 +email-validator==1.2.1 +eventlet==0.33.1 +exceptiongroup==1.0.0rc8 +filelock==3.8.0 +Flask==2.2.2 +Flask-AppBuilder==4.1.3 +Flask-Babel==2.0.0 +Flask-Caching==2.0.1 +Flask-JWT-Extended==4.4.4 +Flask-Login==0.6.2 +Flask-Session==0.4.0 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==0.15.1 +flower==1.2.0 +fsspec==2022.7.1 +future==0.18.2 +gevent==21.12.0 +google-ads==18.0.0 +google-api-core==2.8.2 +google-api-python-client==1.12.11 +google-auth==2.10.0 +google-auth-httplib2==0.1.0 +google-auth-oauthlib==0.5.2 +google-cloud-aiplatform==1.16.1 +google-cloud-appengine-logging==1.1.3 +google-cloud-audit-log==0.2.4 +google-cloud-automl==2.8.0 +google-cloud-bigquery==2.34.4 +google-cloud-bigquery-datatransfer==3.7.0 +google-cloud-bigquery-storage==2.14.1 +google-cloud-bigtable==1.7.2 +google-cloud-build==3.9.0 +google-cloud-container==2.11.1 +google-cloud-core==2.3.2 +google-cloud-datacatalog==3.9.0 +google-cloud-dataform==0.2.0 +google-cloud-dataplex==1.1.0 +google-cloud-dataproc==5.0.0 +google-cloud-dataproc-metastore==1.6.0 +google-cloud-dlp==1.0.2 +google-cloud-kms==2.12.0 +google-cloud-language==1.3.2 +google-cloud-logging==3.2.1 +google-cloud-memcache==1.4.1 +google-cloud-monitoring==2.11.0 +google-cloud-orchestration-airflow==1.4.1 +google-cloud-os-login==2.7.1 +google-cloud-pubsub==2.13.5 +google-cloud-redis==2.9.0 +google-cloud-resource-manager==1.6.0 +google-cloud-secret-manager==1.0.2 +google-cloud-spanner==1.19.3 +google-cloud-speech==1.3.4 +google-cloud-storage==1.44.0 +google-cloud-tasks==2.10.1 +google-cloud-texttospeech==1.0.3 +google-cloud-translate==1.7.2 +google-cloud-videointelligence==1.16.3 +google-cloud-vision==1.0.2 +google-cloud-workflows==1.7.1 +google-crc32c==1.3.0 +google-resumable-media==2.3.3 +googleapis-common-protos==1.56.4 +graphviz==0.20.1 +greenlet==1.1.2 +grpc-google-iam-v1==0.12.4 +grpcio==1.47.0 +grpcio-gcp==0.2.2 +grpcio-status==1.47.0 +gunicorn==20.1.0 +h11==0.12.0 +HeapDict==1.0.1 +httpcore==0.15.0 +httplib2==0.20.4 +httpx==0.23.0 +humanize==4.3.0 +hvac==1.1.1 +idna==3.3 +inflection==0.5.1 +isodate==0.6.1 +itsdangerous==2.1.2 +Jinja2==3.1.2 +jmespath==0.10.0 +json-merge-patch==0.2 +jsonpath-ng==1.5.3 +jsonschema==4.13.0 +kombu==5.2.4 +kubernetes==23.6.0 +lazy-object-proxy==1.7.1 +ldap3==2.9.1 +linkify-it-py==2.0.0 +locket==1.0.0 +lockfile==0.12.2 +looker-sdk==22.10.0 +lxml==4.9.1 +Mako==1.2.1 +Markdown==3.4.1 +markdown-it-py==2.1.0 +MarkupSafe==2.1.1 +marshmallow==3.17.0 +marshmallow-enum==1.5.1 +marshmallow-oneofschema==3.0.1 +marshmallow-sqlalchemy==0.26.1 +mdit-py-plugins==0.3.0 +mdurl==0.1.2 +msal==1.18.0 +msal-extensions==1.0.0 +msgpack==1.0.4 +msrest==0.7.1 +msrestazure==0.6.4 +mypy-boto3-appflow==1.24.36.post1 +mypy-boto3-rds==1.24.54 +mypy-boto3-redshift-data==1.24.36.post1 +mysql-connector-python==8.0.30 +mysqlclient==2.1.1 +numpy==1.22.4 +oauthlib==3.2.0 +packaging==21.3 +pandas==1.4.3 +pandas-gbq==0.17.8 +paramiko==2.11.0 +partd==1.3.0 +pathspec==0.9.0 +pendulum==2.1.2 +platformdirs==2.5.2 +pluggy==1.0.0 +ply==3.11 +portalocker==2.5.1 +prison==0.2.1 +prometheus-client==0.14.1 +prompt-toolkit==3.0.30 +proto-plus==1.19.6 +protobuf==3.20.0 +psutil==5.9.1 +psycopg2-binary==2.9.3 +pyarrow==6.0.1 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycparser==2.21 +pydata-google-auth==1.4.0 +Pygments==2.13.0 +PyJWT==2.4.0 +PyNaCl==1.5.0 +pyodbc==4.0.34 +pyOpenSSL==22.0.0 +pyparsing==3.0.9 +pyrsistent==0.18.1 +python-daemon==2.3.1 +python-dateutil==2.8.2 +python-http-client==3.3.7 +python-ldap==3.4.2 +python-nvd3==0.15.0 +python-slugify==6.1.2 +pytz==2022.1 +pytzdata==2020.1 +PyYAML==6.0 +redis==3.5.3 +redshift-connector==2.0.908 +requests==2.28.0 +requests-oauthlib==1.3.1 +requests-toolbelt==0.9.1 +rfc3986==1.5.0 +rich==12.5.1 +rsa==4.9 +s3transfer==0.6.0 +scramp==1.4.1 +sendgrid==6.9.7 +setproctitle==1.3.2 +six==1.16.0 +slack-sdk==3.18.1 +sniffio==1.2.0 +sortedcontainers==2.4.0 +soupsieve==2.3.2.post1 +SQLAlchemy==1.4.27 +sqlalchemy-bigquery==1.4.4 +SQLAlchemy-JSONField==1.0.0 +sqlalchemy-redshift==0.8.11 +SQLAlchemy-Utils==0.38.3 +sqlparse==0.4.2 +sshtunnel==0.4.0 +starkbank-ecdsa==2.0.3 +statsd==3.3.0 +swagger-ui-bundle==0.0.9 +tabulate==0.8.10 +tblib==1.7.0 +tenacity==8.0.1 +termcolor==1.1.0 +text-unidecode==1.3 +toolz==0.12.0 +tornado==6.1 +typing_extensions==4.3.0 +uamqp==1.6.0 +uc-micro-py==1.0.1 +unicodecsv==0.14.1 +uritemplate==3.0.1 +urllib3==1.26.11 +vine==5.0.0 +virtualenv==20.16.3 +watchtower==2.0.1 +wcwidth==0.2.5 +websocket-client==1.3.3 +Werkzeug==2.2.2 +wrapt==1.14.1 +WTForms==2.3.3 +zict==2.2.0 +zope.event==4.5.0 +zope.interface==5.4.0 diff --git a/scripts/requirements/airflow2-req.in b/scripts/requirements/airflow2-req.in new file mode 100644 index 000000000000..70ed2851a9d8 --- /dev/null +++ b/scripts/requirements/airflow2-req.in @@ -0,0 +1,7 @@ +# Run with: +# ``` +# uv pip compile scripts/requirements/airflow2-req.in -c scripts/requirements/airflow2-constraints.txt --universal --python-version 3.8 +# ``` +apache-airflow[amazon,async,google,google_auth,grpc,hashicorp,http,ldap,mysql,odbc,pandas,postgres,redis,sftp,slack,ssh,statsd,virtualenv]==2.3.4 +alembic>=1.8.1 +ipython>=8.4.0 From 503f9a97af39abbe05ce47ac8d57397ed8bbf274 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 10 Jan 2025 11:23:19 -0500 Subject: [PATCH 102/135] uv-resolver: pre-compute PEP 508 markers from universal markers (#10472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It turns out that we use `UniversalMarker::pep508` quite a bit. To the point that it makes sense to pre-compute it when constructing a `UniversalMarker`. This still isn't necessarily the fastest thing we can do, but this results in a major speed-up and `without_extras` no longer shows up for me in a profile. Motivating benchmarks. First, from #10430: ``` $ hyperfine 'rm -f uv.lock && uv lock' 'rm -f uv.lock && uv-ag-optimize-without-extras lock' Benchmark 1: rm -f uv.lock && uv lock Time (mean ± σ): 408.3 ms ± 276.6 ms [User: 333.6 ms, System: 111.1 ms] Range (min … max): 316.9 ms … 1195.3 ms 10 runs Warning: The first benchmarking run for this command was significantly slower than the rest (1.195 s). This could be caused by (filesystem) caches that were not filled until after the first run. You should consider using the '--warmup' option to fill those caches before the actual benchmark. Alternatively, use the '--prepare' option to clear the caches before each timing run. Benchmark 2: rm -f uv.lock && uv-ag-optimize-without-extras lock Time (mean ± σ): 209.4 ms ± 2.2 ms [User: 209.8 ms, System: 103.8 ms] Range (min … max): 206.1 ms … 213.4 ms 14 runs Summary rm -f uv.lock && uv-ag-optimize-without-extras lock ran 1.95 ± 1.32 times faster than rm -f uv.lock && uv lock ``` And now from #10438: ``` $ hyperfine 'uv pip compile requirements.in -c constraints.txt --universal --no-progress --python-version 3.8 --offline > /dev/null' 'uv-ag-optimize-without-extras pip compile requirements.in -c constraints.txt --universal --no-progress --python-version 3.8 --offline > /dev/null' Benchmark 1: uv pip compile requirements.in -c constraints.txt --universal --no-progress --python-version 3.8 --offline > /dev/null Time (mean ± σ): 12.718 s ± 0.052 s [User: 12.818 s, System: 0.140 s] Range (min … max): 12.650 s … 12.815 s 10 runs Benchmark 2: uv-ag-optimize-without-extras pip compile requirements.in -c constraints.txt --universal --no-progress --python-version 3.8 --offline > /dev/null Time (mean ± σ): 419.5 ms ± 6.7 ms [User: 434.7 ms, System: 100.6 ms] Range (min … max): 412.7 ms … 434.3 ms 10 runs Summary uv-ag-optimize-without-extras pip compile requirements.in -c constraints.txt --universal --no-progress --python-version 3.8 --offline > /dev/null ran 30.32 ± 0.50 times faster than uv pip compile requirements.in -c constraints.txt --universal --no-progress --python-version 3.8 --offline > /dev/null ``` Fixes #10430, Fixes #10438 --- crates/uv-resolver/src/universal_marker.rs | 23 ++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/uv-resolver/src/universal_marker.rs b/crates/uv-resolver/src/universal_marker.rs index 00673e95ae2b..5724f19ab5b1 100644 --- a/crates/uv-resolver/src/universal_marker.rs +++ b/crates/uv-resolver/src/universal_marker.rs @@ -69,17 +69,24 @@ pub struct UniversalMarker { /// stand-point, evaluating it requires uv's custom encoding of extras (and /// groups). marker: MarkerTree, + /// The strictly PEP 508 version of `marker`. Basically, `marker`, but + /// without any extras in it. This could be computed on demand (and + /// that's what we used to do), but we do it enough that it was causing a + /// regression in some cases. + pep508: MarkerTree, } impl UniversalMarker { /// A constant universal marker that always evaluates to `true`. pub(crate) const TRUE: UniversalMarker = UniversalMarker { marker: MarkerTree::TRUE, + pep508: MarkerTree::TRUE, }; /// A constant universal marker that always evaluates to `false`. pub(crate) const FALSE: UniversalMarker = UniversalMarker { marker: MarkerTree::FALSE, + pep508: MarkerTree::FALSE, }; /// Creates a new universal marker from its constituent pieces. @@ -94,7 +101,10 @@ impl UniversalMarker { /// Creates a new universal marker from a marker that has already been /// combined from a PEP 508 and conflict marker. pub(crate) fn from_combined(marker: MarkerTree) -> UniversalMarker { - UniversalMarker { marker } + UniversalMarker { + marker, + pep508: marker.without_extras(), + } } /// Combine this universal marker with the one given in a way that unions @@ -102,6 +112,7 @@ impl UniversalMarker { /// `other` evaluate to `true`. pub(crate) fn or(&mut self, other: UniversalMarker) { self.marker.or(other.marker); + self.pep508.or(other.pep508); } /// Combine this universal marker with the one given in a way that @@ -109,6 +120,7 @@ impl UniversalMarker { /// `self` and `other` evaluate to `true`. pub(crate) fn and(&mut self, other: UniversalMarker) { self.marker.and(other.marker); + self.pep508.and(other.pep508); } /// Imbibes the world knowledge expressed by `conflicts` into this marker. @@ -121,6 +133,7 @@ impl UniversalMarker { let self_marker = self.marker; self.marker = conflicts.marker; self.marker.implies(self_marker); + self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra/group for the given package is activated. @@ -132,6 +145,7 @@ impl UniversalMarker { ConflictPackage::Extra(ref extra) => self.assume_extra(item.package(), extra), ConflictPackage::Group(ref group) => self.assume_group(item.package(), group), } + self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra/group for the given package is not @@ -144,6 +158,7 @@ impl UniversalMarker { ConflictPackage::Extra(ref extra) => self.assume_not_extra(item.package(), extra), ConflictPackage::Group(ref group) => self.assume_not_group(item.package(), group), } + self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra for the given package is activated. @@ -155,6 +170,7 @@ impl UniversalMarker { self.marker = self .marker .simplify_extras_with(|candidate| *candidate == extra); + self.pep508 = self.marker.without_extras(); } /// Assumes that a given extra for the given package is not activated. @@ -166,6 +182,7 @@ impl UniversalMarker { self.marker = self .marker .simplify_not_extras_with(|candidate| *candidate == extra); + self.pep508 = self.marker.without_extras(); } /// Assumes that a given group for the given package is activated. @@ -177,6 +194,7 @@ impl UniversalMarker { self.marker = self .marker .simplify_extras_with(|candidate| *candidate == extra); + self.pep508 = self.marker.without_extras(); } /// Assumes that a given group for the given package is not activated. @@ -188,6 +206,7 @@ impl UniversalMarker { self.marker = self .marker .simplify_not_extras_with(|candidate| *candidate == extra); + self.pep508 = self.marker.without_extras(); } /// Returns true if this universal marker will always evaluate to `true`. @@ -259,7 +278,7 @@ impl UniversalMarker { /// always use a universal marker since it accounts for all possible ways /// for a package to be installed. pub fn pep508(self) -> MarkerTree { - self.marker.without_extras() + self.pep508 } /// Returns the non-PEP 508 marker expression that represents conflicting From b3d7beb1a00637639b3dff4d9f9e897a4dd90128 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 10 Jan 2025 14:46:36 -0500 Subject: [PATCH 103/135] Use `arcstr` for package, extra, and group names (#10475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This appears to be a consistent 1% performance improvement and should also reduce memory quite a bit. We've also decided to use these for markers, so it's nice to use the same optimization here. ``` ❯ hyperfine "./uv pip compile --universal scripts/requirements/airflow.in" "./arcstr pip compile --universal scripts/requirements/airflow.in" --min-runs 50 --warmup 20 Benchmark 1: ./uv pip compile --universal scripts/requirements/airflow.in Time (mean ± σ): 136.3 ms ± 4.0 ms [User: 139.1 ms, System: 241.9 ms] Range (min … max): 131.5 ms … 149.5 ms 50 runs Benchmark 2: ./arcstr pip compile --universal scripts/requirements/airflow.in Time (mean ± σ): 134.9 ms ± 3.2 ms [User: 137.6 ms, System: 239.0 ms] Range (min … max): 130.1 ms … 151.8 ms 50 runs Summary ./arcstr pip compile --universal scripts/requirements/airflow.in ran 1.01 ± 0.04 times faster than ./uv pip compile --universal scripts/requirements/airflow.in ``` --- Cargo.lock | 7 ++ Cargo.toml | 1 + crates/uv-normalize/Cargo.toml | 1 + crates/uv-normalize/src/extra_name.rs | 5 +- crates/uv-normalize/src/group_name.rs | 5 +- crates/uv-normalize/src/lib.rs | 34 ++++-- crates/uv-normalize/src/package_name.rs | 7 +- crates/uv-normalize/src/small_string.rs | 119 +++++++++++++++++++++ crates/uv-resolver/src/universal_marker.rs | 3 +- 9 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 crates/uv-normalize/src/small_string.rs diff --git a/Cargo.lock b/Cargo.lock index 5bdaed13303f..4e07002b0479 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" + [[package]] name = "arrayref" version = "0.3.9" @@ -5227,6 +5233,7 @@ dependencies = [ name = "uv-normalize" version = "0.0.1" dependencies = [ + "arcstr", "rkyv", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 6bba2c93e93b..ae7315f428e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ uv-workspace = { path = "crates/uv-workspace" } anstream = { version = "0.6.15" } anyhow = { version = "1.0.89" } +arcstr = { version = "1.2.0" } async-channel = { version = "2.3.1" } async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] } async-trait = { version = "0.1.82" } diff --git a/crates/uv-normalize/Cargo.toml b/crates/uv-normalize/Cargo.toml index 8f4db6a15d82..8d9c1c23e512 100644 --- a/crates/uv-normalize/Cargo.toml +++ b/crates/uv-normalize/Cargo.toml @@ -11,6 +11,7 @@ doctest = false workspace = true [dependencies] +arcstr = { workspace = true } rkyv = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/uv-normalize/src/extra_name.rs b/crates/uv-normalize/src/extra_name.rs index f23431178d2c..bc3c5f737bb8 100644 --- a/crates/uv-normalize/src/extra_name.rs +++ b/crates/uv-normalize/src/extra_name.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; +use crate::small_string::SmallString; use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of an extra dependency. @@ -14,9 +15,9 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam /// See: /// - /// - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ExtraName(String); +pub struct ExtraName(SmallString); impl ExtraName { /// Create a validated, normalized extra name. diff --git a/crates/uv-normalize/src/group_name.rs b/crates/uv-normalize/src/group_name.rs index 72aa898ec729..a3ecb74c8c24 100644 --- a/crates/uv-normalize/src/group_name.rs +++ b/crates/uv-normalize/src/group_name.rs @@ -5,6 +5,7 @@ use std::sync::LazyLock; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::small_string::SmallString; use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of a dependency group. @@ -12,9 +13,9 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam /// See: /// - /// - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct GroupName(String); +pub struct GroupName(SmallString); impl GroupName { /// Create a validated, normalized group name. diff --git a/crates/uv-normalize/src/lib.rs b/crates/uv-normalize/src/lib.rs index 583b17bcc6af..17beee6b9995 100644 --- a/crates/uv-normalize/src/lib.rs +++ b/crates/uv-normalize/src/lib.rs @@ -5,26 +5,37 @@ pub use dist_info_name::DistInfoName; pub use extra_name::ExtraName; pub use group_name::{GroupName, DEV_DEPENDENCIES}; pub use package_name::PackageName; +use small_string::SmallString; mod dist_info_name; mod extra_name; mod group_name; mod package_name; +mod small_string; /// Validate and normalize an owned package or extra name. -pub(crate) fn validate_and_normalize_owned(name: String) -> Result { +pub(crate) fn validate_and_normalize_owned(name: String) -> Result { if is_normalized(&name)? { - Ok(name) + Ok(SmallString::from(name)) } else { - validate_and_normalize_ref(name) + Ok(SmallString::from(normalize(&name)?)) } } /// Validate and normalize an unowned package or extra name. pub(crate) fn validate_and_normalize_ref( name: impl AsRef, -) -> Result { +) -> Result { let name = name.as_ref(); + if is_normalized(name)? { + Ok(SmallString::from(name)) + } else { + Ok(SmallString::from(normalize(name)?)) + } +} + +/// Normalize an unowned package or extra name. +fn normalize(name: &str) -> Result { let mut normalized = String::with_capacity(name.len()); let mut last = None; @@ -136,9 +147,14 @@ mod tests { "FrIeNdLy-._.-bArD", ]; for input in inputs { - assert_eq!(validate_and_normalize_ref(input).unwrap(), "friendly-bard"); assert_eq!( - validate_and_normalize_owned(input.to_string()).unwrap(), + validate_and_normalize_ref(input).unwrap().as_ref(), + "friendly-bard" + ); + assert_eq!( + validate_and_normalize_owned(input.to_string()) + .unwrap() + .as_ref(), "friendly-bard" ); } @@ -169,9 +185,11 @@ mod tests { // Unchanged let unchanged = ["friendly-bard", "1okay", "okay2"]; for input in unchanged { - assert_eq!(validate_and_normalize_ref(input).unwrap(), input); + assert_eq!(validate_and_normalize_ref(input).unwrap().as_ref(), input); assert_eq!( - validate_and_normalize_owned(input.to_string()).unwrap(), + validate_and_normalize_owned(input.to_string()) + .unwrap() + .as_ref(), input ); assert!(is_normalized(input).unwrap()); diff --git a/crates/uv-normalize/src/package_name.rs b/crates/uv-normalize/src/package_name.rs index d3067f17aec7..742867e7f51b 100644 --- a/crates/uv-normalize/src/package_name.rs +++ b/crates/uv-normalize/src/package_name.rs @@ -1,8 +1,10 @@ use std::borrow::Cow; +use std::cmp::PartialEq; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; +use crate::small_string::SmallString; use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of a package. @@ -13,7 +15,6 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam /// See: #[derive( Debug, - Default, Clone, PartialEq, Eq, @@ -27,7 +28,7 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[rkyv(derive(Debug))] -pub struct PackageName(String); +pub struct PackageName(SmallString); impl PackageName { /// Create a validated, normalized package name. @@ -56,7 +57,7 @@ impl PackageName { Cow::Owned(owned_string) } else { - Cow::Borrowed(self.0.as_str()) + Cow::Borrowed(self.0.as_ref()) } } diff --git a/crates/uv-normalize/src/small_string.rs b/crates/uv-normalize/src/small_string.rs new file mode 100644 index 000000000000..ca6d3ea6dadb --- /dev/null +++ b/crates/uv-normalize/src/small_string.rs @@ -0,0 +1,119 @@ +use std::cmp::PartialEq; +use std::ops::Deref; + +/// An optimized small string type for short identifiers, like package names. +/// +/// Represented as an [`arcstr::ArcStr`] internally. +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct SmallString(arcstr::ArcStr); + +impl From<&str> for SmallString { + #[inline] + fn from(s: &str) -> Self { + Self(s.into()) + } +} + +impl From for SmallString { + #[inline] + fn from(s: String) -> Self { + Self(s.into()) + } +} + +impl AsRef for SmallString { + #[inline] + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for SmallString { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::fmt::Debug for SmallString { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self.0, f) + } +} + +impl core::fmt::Display for SmallString { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.0, f) + } +} + +/// A [`serde::Serialize`] implementation for [`SmallString`]. +impl serde::Serialize for SmallString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +/// An [`rkyv`] implementation for [`SmallString`]. +impl rkyv::Archive for SmallString { + type Archived = rkyv::string::ArchivedString; + type Resolver = rkyv::string::StringResolver; + + #[inline] + fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place) { + rkyv::string::ArchivedString::resolve_from_str(&self.0, resolver, out); + } +} + +impl rkyv::Serialize for SmallString +where + S: rkyv::rancor::Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized, + S::Error: rkyv::rancor::Source, +{ + fn serialize(&self, serializer: &mut S) -> Result { + rkyv::string::ArchivedString::serialize_from_str(&self.0, serializer) + } +} + +impl rkyv::Deserialize + for rkyv::string::ArchivedString +{ + fn deserialize(&self, _deserializer: &mut D) -> Result { + Ok(SmallString::from(self.as_str())) + } +} + +impl PartialEq for rkyv::string::ArchivedString { + fn eq(&self, other: &SmallString) -> bool { + **other == **self + } +} + +impl PartialOrd for rkyv::string::ArchivedString { + fn partial_cmp(&self, other: &SmallString) -> Option<::core::cmp::Ordering> { + Some(self.as_str().cmp(other)) + } +} + +/// An [`schemars::JsonSchema`] implementation for [`SmallString`]. +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for SmallString { + fn is_referenceable() -> bool { + String::is_referenceable() + } + + fn schema_name() -> String { + String::schema_name() + } + + fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(_gen) + } +} diff --git a/crates/uv-resolver/src/universal_marker.rs b/crates/uv-resolver/src/universal_marker.rs index 5724f19ab5b1..0789ad264a61 100644 --- a/crates/uv-resolver/src/universal_marker.rs +++ b/crates/uv-resolver/src/universal_marker.rs @@ -488,6 +488,7 @@ fn encode_package_group(package: &PackageName, group: &GroupName) -> ExtraName { #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; use uv_pypi_types::ConflictSet; @@ -516,7 +517,7 @@ mod tests { /// Shortcut for creating a package name. fn create_package(name: &str) -> PackageName { - PackageName::new(name.to_string()).unwrap() + PackageName::from_str(name).unwrap() } /// Shortcut for creating an extra name. From 2982c2074cba9af442d509a917cef742804a40cc Mon Sep 17 00:00:00 2001 From: Jeremy Foxcroft Date: Fri, 10 Jan 2025 15:04:35 -0500 Subject: [PATCH 104/135] Fix `UV_FIND_LINKS` delimiter to split on commas (#10477) #8061 incorrectly claims to change the delimiter for `UV_FIND_LINKS` from spaces to commas. In reality, it prevents `UV_FIND_LINKS` from being split. This commit fixes that. --- crates/uv-cli/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 8c0dc6f8cee9..ec38129b1d1b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4593,6 +4593,7 @@ pub struct IndexArgs { long, short, env = EnvVars::UV_FIND_LINKS, + value_delimiter = ',', value_parser = parse_find_links, help_heading = "Index options" )] From d44affaac0a16af52775b63dba76935f155f31e4 Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 10 Jan 2025 21:10:54 +0100 Subject: [PATCH 105/135] Read publish username from URL (#10469) --- crates/uv/src/commands/publish.rs | 245 ++++++++++++++++++++++++------ 1 file changed, 201 insertions(+), 44 deletions(-) diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 88dcdf684245..b036dc1444e3 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -11,7 +11,9 @@ use std::time::Duration; use tracing::{debug, info}; use url::Url; use uv_cache::Cache; -use uv_client::{AuthIntegration, BaseClientBuilder, Connectivity, RegistryClientBuilder}; +use uv_client::{ + AuthIntegration, BaseClient, BaseClientBuilder, Connectivity, RegistryClientBuilder, +}; use uv_configuration::{KeyringProviderType, TrustedHost, TrustedPublishing}; use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl}; use uv_publish::{ @@ -68,6 +70,18 @@ pub(crate) async fn publish( .auth_integration(AuthIntegration::NoAuthMiddleware) .wrap_existing(&upload_client); + let (publish_url, username, password) = gather_credentials( + publish_url, + username, + password, + trusted_publishing, + keyring_provider, + &oidc_client, + check_url.as_ref(), + printer, + ) + .await?; + // Initialize the registry client. let check_url_client = if let Some(index_url) = &check_url { let index_urls = IndexLocations::new( @@ -93,6 +107,83 @@ pub(crate) async fn publish( None }; + for (file, raw_filename, filename) in files { + if let Some(check_url_client) = &check_url_client { + if uv_publish::check_url(check_url_client, &file, &filename).await? { + writeln!(printer.stderr(), "File {filename} already exists, skipping")?; + continue; + } + } + + let size = fs_err::metadata(&file)?.len(); + let (bytes, unit) = human_readable_bytes(size); + writeln!( + printer.stderr(), + "{} {filename} {}", + "Uploading".bold().green(), + format!("({bytes:.1}{unit})").dimmed() + )?; + let reporter = PublishReporter::single(printer); + let uploaded = upload( + &file, + &raw_filename, + &filename, + &publish_url, + &upload_client, + username.as_deref(), + password.as_deref(), + check_url_client.as_ref(), + // Needs to be an `Arc` because the reqwest `Body` static lifetime requirement + Arc::new(reporter), + ) + .await?; // Filename and/or URL are already attached, if applicable. + info!("Upload succeeded"); + if !uploaded { + writeln!( + printer.stderr(), + "{}", + "File already exists, skipping".dimmed() + )?; + } + } + + Ok(ExitStatus::Success) +} + +/// Unify the different possible source for username and password information. +/// +/// Returns the publish URL, the username and the password. +async fn gather_credentials( + mut publish_url: Url, + mut username: Option, + mut password: Option, + trusted_publishing: TrustedPublishing, + keyring_provider: KeyringProviderType, + oidc_client: &BaseClient, + check_url: Option<&IndexUrl>, + printer: Printer, +) -> Result<(Url, Option, Option)> { + // Support reading username and password from the URL, for symmetry with the index API. + if let Some(url_password) = publish_url.password() { + if password.is_some_and(|password| password != url_password) { + bail!("The password can't be set both in the publish URL and in the CLI"); + } + password = Some(url_password.to_string()); + publish_url + .set_password(None) + .expect("Failed to clear publish URL password"); + } + + if !publish_url.username().is_empty() { + if username.is_some_and(|username| username != publish_url.username()) { + bail!("The username can't be set both in the publish URL and in the CLI"); + } + username = Some(publish_url.username().to_string()); + publish_url + .set_username("") + .expect("Failed to clear publish URL username"); + } + // If applicable, attempt obtaining a token for trusted publishing. let trusted_publishing_token = check_trusted_publishing( username.as_deref(), @@ -100,7 +191,7 @@ pub(crate) async fn publish( keyring_provider, trusted_publishing, &publish_url, - &oidc_client, + oidc_client, ) .await?; @@ -179,48 +270,7 @@ pub(crate) async fn publish( // We may be using the keyring for the simple index. } } - - for (file, raw_filename, filename) in files { - if let Some(check_url_client) = &check_url_client { - if uv_publish::check_url(check_url_client, &file, &filename).await? { - writeln!(printer.stderr(), "File {filename} already exists, skipping")?; - continue; - } - } - - let size = fs_err::metadata(&file)?.len(); - let (bytes, unit) = human_readable_bytes(size); - writeln!( - printer.stderr(), - "{} {filename} {}", - "Uploading".bold().green(), - format!("({bytes:.1}{unit})").dimmed() - )?; - let reporter = PublishReporter::single(printer); - let uploaded = upload( - &file, - &raw_filename, - &filename, - &publish_url, - &upload_client, - username.as_deref(), - password.as_deref(), - check_url_client.as_ref(), - // Needs to be an `Arc` because the reqwest `Body` static lifetime requirement - Arc::new(reporter), - ) - .await?; // Filename and/or URL are already attached, if applicable. - info!("Upload succeeded"); - if !uploaded { - writeln!( - printer.stderr(), - "{}", - "File already exists, skipping".dimmed() - )?; - } - } - - Ok(ExitStatus::Success) + Ok((publish_url, username, password)) } fn prompt_username_and_password() -> Result<(Option, Option)> { @@ -235,3 +285,110 @@ fn prompt_username_and_password() -> Result<(Option, Option)> { uv_console::password(password_prompt, &term).context("Failed to read password")?; Ok((Some(username), Some(password))) } + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_snapshot; + use std::str::FromStr; + use url::Url; + + async fn credentials( + url: Url, + username: Option, + password: Option, + ) -> Result<(Url, Option, Option)> { + let client = BaseClientBuilder::new().build(); + gather_credentials( + url, + username, + password, + TrustedPublishing::Never, + KeyringProviderType::Disabled, + &client, + None, + Printer::Quiet, + ) + .await + } + + #[tokio::test] + async fn username_password_sources() { + let example_url = Url::from_str("https://example.com").unwrap(); + let example_url_username = Url::from_str("https://ferris@example.com").unwrap(); + let example_url_username_password = + Url::from_str("https://ferris:f3rr1s@example.com").unwrap(); + + let (publish_url, username, password) = + credentials(example_url.clone(), None, None).await.unwrap(); + assert_eq!(publish_url, example_url); + assert_eq!(username, None); + assert_eq!(password, None); + + let (publish_url, username, password) = + credentials(example_url_username.clone(), None, None) + .await + .unwrap(); + assert_eq!(publish_url, example_url); + assert_eq!(username.as_deref(), Some("ferris")); + assert_eq!(password, None); + + let (publish_url, username, password) = + credentials(example_url_username_password.clone(), None, None) + .await + .unwrap(); + assert_eq!(publish_url, example_url); + assert_eq!(username.as_deref(), Some("ferris")); + assert_eq!(password.as_deref(), Some("f3rr1s")); + + // Ok: The username is the same between CLI/env vars and URL + let (publish_url, username, password) = credentials( + example_url_username_password.clone(), + Some("ferris".to_string()), + None, + ) + .await + .unwrap(); + assert_eq!(publish_url, example_url); + assert_eq!(username.as_deref(), Some("ferris")); + assert_eq!(password.as_deref(), Some("f3rr1s")); + + // Err: There are two different usernames between CLI/env vars and URL + let err = credentials( + example_url_username_password.clone(), + Some("packaging-platypus".to_string()), + None, + ) + .await + .unwrap_err(); + assert_snapshot!( + err.to_string(), + @"The username can't be set both in the publish URL and in the CLI" + ); + + // Ok: The username and password are the same between CLI/env vars and URL + let (publish_url, username, password) = credentials( + example_url_username_password.clone(), + Some("ferris".to_string()), + Some("f3rr1s".to_string()), + ) + .await + .unwrap(); + assert_eq!(publish_url, example_url); + assert_eq!(username.as_deref(), Some("ferris")); + assert_eq!(password.as_deref(), Some("f3rr1s")); + + // Err: There are two different passwords between CLI/env vars and URL + let err = credentials( + example_url_username_password.clone(), + Some("ferris".to_string()), + Some("secret".to_string()), + ) + .await + .unwrap_err(); + assert_snapshot!( + err.to_string(), + @"The password can't be set both in the publish URL and in the CLI" + ); + } +} From 7a21b713b4701048b7c60378bb1f4668a8461129 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 10 Jan 2025 15:12:23 -0500 Subject: [PATCH 106/135] Avoid allocating for names in the PEP 508 parser (#10476) ## Summary We can read from the slice directly. I don't think this will affect performance today, because `from_str` will then allocate, but it _should_ be a speedup once #10475 merges, since we can then avoid allocating a `String` and go straight from `str` to `ArcStr`. --- crates/uv-pep508/src/lib.rs | 43 ++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index 95f4a4774c4e..c4b1193a1845 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -394,12 +394,9 @@ fn parse_name(cursor: &mut Cursor) -> Result(cursor: &mut Cursor) -> Result { - name.push(char); - cursor.next(); - // [.-_] can't be the final character - if cursor.peek().is_none() && matches!(char, '.' | '-' | '_') { - return Err(Pep508Error { - message: Pep508ErrorSource::String(format!( - "Package name must end with an alphanumeric character, not '{char}'" - )), - start: index, - len: char.len_utf8(), - input: cursor.to_string(), - }); - } - } - Some(_) | None => { - return Ok(PackageName::new(name) - .expect("`PackageName` validation should match PEP 508 parsing")); + if let Some((index, char @ ('A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '-' | '_'))) = + cursor.peek() + { + cursor.next(); + // [.-_] can't be the final character + if cursor.peek().is_none() && matches!(char, '.' | '-' | '_') { + return Err(Pep508Error { + message: Pep508ErrorSource::String(format!( + "Package name must end with an alphanumeric character, not `{char}`" + )), + start: index, + len: char.len_utf8(), + input: cursor.to_string(), + }); } + } else { + let len = cursor.pos() - start; + return Ok(PackageName::from_str(cursor.slice(start, len)).unwrap()); } } } @@ -1033,7 +1028,7 @@ mod tests { assert_snapshot!( parse_pep508_err("name_"), @" - Package name must end with an alphanumeric character, not '_' + Package name must end with an alphanumeric character, not `_` name_ ^" ); From 8420195aa75821f33d7024c500ec2624c68ab669 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 10 Jan 2025 15:15:12 -0500 Subject: [PATCH 107/135] Use `ArcStr` for marker values (#10453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N.B. After fixing #10430, `ArcStr` became the fastest implementation (and the gains were significantly reduced, down to 1-2%). See: https://github.com/astral-sh/uv/pull/10453#issuecomment-2583344414. ## Summary I tried out a variety of small string crates, but `Arc` outperformed them, giving a ~10% speed-up: ```console ❯ hyperfine "../arcstr lock" "../flexstr lock" "uv lock" "../arc lock" "../compact_str lock" --prepare "rm -f uv.lock" --min-runs 50 --warmup 20 Benchmark 1: ../arcstr lock Time (mean ± σ): 304.6 ms ± 2.3 ms [User: 302.9 ms, System: 117.8 ms] Range (min … max): 299.0 ms … 311.3 ms 50 runs Benchmark 2: ../flexstr lock Time (mean ± σ): 319.2 ms ± 1.7 ms [User: 317.7 ms, System: 118.2 ms] Range (min … max): 316.8 ms … 323.3 ms 50 runs Benchmark 3: uv lock Time (mean ± σ): 330.6 ms ± 1.5 ms [User: 328.1 ms, System: 139.3 ms] Range (min … max): 326.6 ms … 334.2 ms 50 runs Benchmark 4: ../arc lock Time (mean ± σ): 303.0 ms ± 1.2 ms [User: 301.6 ms, System: 118.4 ms] Range (min … max): 300.3 ms … 305.3 ms 50 runs Benchmark 5: ../compact_str lock Time (mean ± σ): 320.4 ms ± 2.0 ms [User: 318.7 ms, System: 120.8 ms] Range (min … max): 317.3 ms … 326.7 ms 50 runs Summary ../arc lock ran 1.01 ± 0.01 times faster than ../arcstr lock 1.05 ± 0.01 times faster than ../flexstr lock 1.06 ± 0.01 times faster than ../compact_str lock 1.09 ± 0.01 times faster than uv lock ``` --- Cargo.lock | 3 + crates/uv-distribution-types/Cargo.toml | 1 + .../src/prioritized_distribution.rs | 21 +++---- crates/uv-pep508/Cargo.toml | 1 + crates/uv-pep508/src/lib.rs | 6 +- crates/uv-pep508/src/marker/algebra.rs | 60 ++++++++++--------- crates/uv-pep508/src/marker/parse.rs | 4 +- crates/uv-pep508/src/marker/simplify.rs | 8 ++- crates/uv-pep508/src/marker/tree.rs | 24 ++++---- crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/resolution/output.rs | 3 +- crates/uv-resolver/src/resolver/mod.rs | 8 ++- 12 files changed, 80 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e07002b0479..7d894b5e005a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5013,6 +5013,7 @@ name = "uv-distribution-types" version = "0.0.1" dependencies = [ "anyhow", + "arcstr", "bitflags 2.6.0", "fs-err 3.0.0", "itertools 0.14.0", @@ -5272,6 +5273,7 @@ dependencies = [ name = "uv-pep508" version = "0.6.0" dependencies = [ + "arcstr", "boxcar", "indexmap", "insta", @@ -5508,6 +5510,7 @@ dependencies = [ name = "uv-resolver" version = "0.0.1" dependencies = [ + "arcstr", "clap", "dashmap", "either", diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index 4a501926b7ec..d4c5309f5e05 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -29,6 +29,7 @@ uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } anyhow = { workspace = true } +arcstr = { workspace = true } bitflags = { workspace = true } fs-err = { workspace = true } itertools = { workspace = true } diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index 61b010df26b5..d5bfc4d5ecd2 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -1,5 +1,6 @@ use std::fmt::{Display, Formatter}; +use arcstr::ArcStr; use tracing::debug; use uv_distribution_filename::{BuildTag, WheelFilename}; @@ -662,12 +663,12 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "win32".to_string(), + value: arcstr::literal!("win32"), }); tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, - value: "x86".to_string(), + value: arcstr::literal!("x86"), })); marker.or(tag_marker); } @@ -675,12 +676,12 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "win32".to_string(), + value: arcstr::literal!("win32"), }); tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, - value: "x86_64".to_string(), + value: arcstr::literal!("x86_64"), })); marker.or(tag_marker); } @@ -688,12 +689,12 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "win32".to_string(), + value: arcstr::literal!("win32"), }); tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, - value: "arm64".to_string(), + value: arcstr::literal!("arm64"), })); marker.or(tag_marker); } @@ -703,7 +704,7 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "darwin".to_string(), + value: arcstr::literal!("darwin"), }); // Parse the macOS version from the tag. @@ -787,7 +788,7 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { arch_marker.or(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, - value: (*arch).to_string(), + value: ArcStr::from(*arch), })); } tag_marker.and(arch_marker); @@ -800,7 +801,7 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "linux".to_string(), + value: arcstr::literal!("linux"), }); // Parse the architecture from the tag. @@ -866,7 +867,7 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, - value: arch.to_string(), + value: ArcStr::from(arch), })); marker.or(tag_marker); diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index 912a226f7c0d..1fff96287684 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -23,6 +23,7 @@ uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } +arcstr = { workspace = true} boxcar = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index c4b1193a1845..087f69d316f4 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -1334,17 +1334,17 @@ mod tests { let mut b = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "win32".to_string(), + value: arcstr::literal!("win32"), }); let mut c = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::OsName, operator: MarkerOperator::Equal, - value: "linux".to_string(), + value: arcstr::literal!("linux"), }); let d = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::ImplementationName, operator: MarkerOperator::Equal, - value: "cpython".to_string(), + value: arcstr::literal!("cpython"), }); c.and(d); diff --git a/crates/uv-pep508/src/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs index 892c38f8ddc1..303d2696f494 100644 --- a/crates/uv-pep508/src/marker/algebra.rs +++ b/crates/uv-pep508/src/marker/algebra.rs @@ -51,6 +51,7 @@ use std::ops::Bound; use std::sync::Mutex; use std::sync::MutexGuard; +use arcstr::ArcStr; use itertools::{Either, Itertools}; use rustc_hash::FxHashMap; use std::sync::LazyLock; @@ -287,28 +288,31 @@ impl InternerGuard<'_> { // values in `exclusions`. // // See: https://discuss.python.org/t/clarify-usage-of-platform-system/70900 - let (key, value) = match (key, value.as_str()) { - (MarkerValueString::PlatformSystem, "Windows") => { - (CanonicalMarkerValueString::SysPlatform, "win32".to_string()) - } + let (key, value) = match (key, value.as_ref()) { + (MarkerValueString::PlatformSystem, "Windows") => ( + CanonicalMarkerValueString::SysPlatform, + arcstr::literal!("win32"), + ), (MarkerValueString::PlatformSystem, "Darwin") => ( CanonicalMarkerValueString::SysPlatform, - "darwin".to_string(), + arcstr::literal!("darwin"), + ), + (MarkerValueString::PlatformSystem, "Linux") => ( + CanonicalMarkerValueString::SysPlatform, + arcstr::literal!("linux"), + ), + (MarkerValueString::PlatformSystem, "AIX") => ( + CanonicalMarkerValueString::SysPlatform, + arcstr::literal!("aix"), ), - (MarkerValueString::PlatformSystem, "Linux") => { - (CanonicalMarkerValueString::SysPlatform, "linux".to_string()) - } - (MarkerValueString::PlatformSystem, "AIX") => { - (CanonicalMarkerValueString::SysPlatform, "aix".to_string()) - } (MarkerValueString::PlatformSystem, "Emscripten") => ( CanonicalMarkerValueString::SysPlatform, - "emscripten".to_string(), + arcstr::literal!("emscripten"), ), // See: https://peps.python.org/pep-0738/#sys (MarkerValueString::PlatformSystem, "Android") => ( CanonicalMarkerValueString::SysPlatform, - "android".to_string(), + arcstr::literal!("android"), ), _ => (key.into(), value), }; @@ -869,48 +873,48 @@ impl InternerGuard<'_> { MarkerExpression::String { key: MarkerValueString::OsName, operator: MarkerOperator::Equal, - value: "nt".to_string(), + value: arcstr::literal!("nt"), }, MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "linux".to_string(), + value: arcstr::literal!("linux"), }, ), ( MarkerExpression::String { key: MarkerValueString::OsName, operator: MarkerOperator::Equal, - value: "nt".to_string(), + value: arcstr::literal!("nt"), }, MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "darwin".to_string(), + value: arcstr::literal!("darwin"), }, ), ( MarkerExpression::String { key: MarkerValueString::OsName, operator: MarkerOperator::Equal, - value: "nt".to_string(), + value: arcstr::literal!("nt"), }, MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "ios".to_string(), + value: arcstr::literal!("ios"), }, ), ( MarkerExpression::String { key: MarkerValueString::OsName, operator: MarkerOperator::Equal, - value: "posix".to_string(), + value: arcstr::literal!("posix"), }, MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: "win32".to_string(), + value: arcstr::literal!("win32"), }, ), ]; @@ -950,12 +954,12 @@ impl InternerGuard<'_> { MarkerExpression::String { key: MarkerValueString::PlatformSystem, operator: MarkerOperator::Equal, - value: platform_system.to_string(), + value: ArcStr::from(platform_system), }, MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, - value: sys_platform.to_string(), + value: ArcStr::from(sys_platform), }, )); } @@ -996,13 +1000,13 @@ pub(crate) enum Variable { /// string marker and value. In { key: CanonicalMarkerValueString, - value: String, + value: ArcStr, }, /// A variable representing a ` in ` expression for a particular /// string marker and value. Contains { key: CanonicalMarkerValueString, - value: String, + value: ArcStr, }, /// A variable representing the existence or absence of a given extra. /// @@ -1128,7 +1132,7 @@ pub(crate) enum Edges { // Invariant: All ranges are simple, meaning they can be represented by a bounded // interval without gaps. Additionally, there are at least two edges in the set. String { - edges: SmallVec<(Ranges, NodeId)>, + edges: SmallVec<(Ranges, NodeId)>, }, // The edges of a boolean variable, representing the values `true` (the `high` child) // and `false` (the `low` child). @@ -1158,8 +1162,8 @@ impl Edges { /// /// This function will panic for the `In` and `Contains` marker operators, which /// should be represented as separate boolean variables. - fn from_string(operator: MarkerOperator, value: String) -> Edges { - let range: Ranges = match operator { + fn from_string(operator: MarkerOperator, value: ArcStr) -> Edges { + let range: Ranges = match operator { MarkerOperator::Equal => Ranges::singleton(value), MarkerOperator::NotEqual => Ranges::singleton(value).complement(), MarkerOperator::GreaterThan => Ranges::strictly_higher_than(value), diff --git a/crates/uv-pep508/src/marker/parse.rs b/crates/uv-pep508/src/marker/parse.rs index d85af429f8b2..bae032d9b270 100644 --- a/crates/uv-pep508/src/marker/parse.rs +++ b/crates/uv-pep508/src/marker/parse.rs @@ -1,5 +1,5 @@ +use arcstr::ArcStr; use std::str::FromStr; - use uv_normalize::ExtraName; use uv_pep440::{Version, VersionPattern, VersionSpecifier}; @@ -92,7 +92,7 @@ pub(crate) fn parse_marker_value( Some((start_pos, quotation_mark @ ('"' | '\''))) => { cursor.next(); let (start, len) = cursor.take_while(|c| c != quotation_mark); - let value = cursor.slice(start, len).to_string(); + let value = ArcStr::from(cursor.slice(start, len)); cursor.next_expect_char(quotation_mark, start_pos)?; Ok(MarkerValue::QuotedString(value)) } diff --git a/crates/uv-pep508/src/marker/simplify.rs b/crates/uv-pep508/src/marker/simplify.rs index ea868c3cfe4f..34c095b09efc 100644 --- a/crates/uv-pep508/src/marker/simplify.rs +++ b/crates/uv-pep508/src/marker/simplify.rs @@ -1,12 +1,14 @@ use std::fmt; use std::ops::Bound; +use arcstr::ArcStr; use indexmap::IndexMap; use itertools::Itertools; use rustc_hash::FxBuildHasher; -use uv_pep440::{Version, VersionSpecifier}; use version_ranges::Ranges; +use uv_pep440::{Version, VersionSpecifier}; + use crate::{ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerTreeKind}; /// Returns a simplified DNF expression for a given marker tree. @@ -131,7 +133,7 @@ fn collect_dnf( let expr = MarkerExpression::String { key: marker.key().into(), - value: marker.value().to_owned(), + value: ArcStr::from(marker.value()), operator, }; @@ -150,7 +152,7 @@ fn collect_dnf( let expr = MarkerExpression::String { key: marker.key().into(), - value: marker.value().to_owned(), + value: ArcStr::from(marker.value()), operator, }; diff --git a/crates/uv-pep508/src/marker/tree.rs b/crates/uv-pep508/src/marker/tree.rs index 55e8b5d7d06c..099fdb7e66d9 100644 --- a/crates/uv-pep508/src/marker/tree.rs +++ b/crates/uv-pep508/src/marker/tree.rs @@ -3,6 +3,7 @@ use std::fmt::{self, Display, Formatter}; use std::ops::{Bound, Deref}; use std::str::FromStr; +use arcstr::ArcStr; use itertools::Itertools; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use version_ranges::Ranges; @@ -129,7 +130,7 @@ pub enum MarkerValue { /// `extra`. This one is special because it's a list and not env but user given Extra, /// Not a constant, but a user given quoted string with a value inside such as '3.8' or "windows" - QuotedString(String), + QuotedString(ArcStr), } impl FromStr for MarkerValue { @@ -272,8 +273,8 @@ impl MarkerOperator { /// Returns the marker operator and value whose union represents the given range. pub fn from_bounds( - bounds: (&Bound, &Bound), - ) -> impl Iterator { + bounds: (&Bound, &Bound), + ) -> impl Iterator { let (b1, b2) = match bounds { (Bound::Included(v1), Bound::Included(v2)) if v1 == v2 => { (Some((MarkerOperator::Equal, v1.clone())), None) @@ -291,7 +292,7 @@ impl MarkerOperator { } /// Returns a value specifier representing the given lower bound. - pub fn from_lower_bound(bound: &Bound) -> Option<(MarkerOperator, String)> { + pub fn from_lower_bound(bound: &Bound) -> Option<(MarkerOperator, ArcStr)> { match bound { Bound::Included(value) => Some((MarkerOperator::GreaterEqual, value.clone())), Bound::Excluded(value) => Some((MarkerOperator::GreaterThan, value.clone())), @@ -300,7 +301,7 @@ impl MarkerOperator { } /// Returns a value specifier representing the given upper bound. - pub fn from_upper_bound(bound: &Bound) -> Option<(MarkerOperator, String)> { + pub fn from_upper_bound(bound: &Bound) -> Option<(MarkerOperator, ArcStr)> { match bound { Bound::Included(value) => Some((MarkerOperator::LessEqual, value.clone())), Bound::Excluded(value) => Some((MarkerOperator::LessThan, value.clone())), @@ -485,7 +486,7 @@ pub enum MarkerExpression { String { key: MarkerValueString, operator: MarkerOperator, - value: String, + value: ArcStr, }, /// `extra '...'` or `'...' extra`. Extra { @@ -1383,7 +1384,7 @@ impl Ord for VersionMarkerTree<'_> { pub struct StringMarkerTree<'a> { id: NodeId, key: CanonicalMarkerValueString, - map: &'a [(Ranges, NodeId)], + map: &'a [(Ranges, NodeId)], } impl StringMarkerTree<'_> { @@ -1393,7 +1394,7 @@ impl StringMarkerTree<'_> { } /// The edges of this node, corresponding to possible output ranges of the given variable. - pub fn children(&self) -> impl ExactSizeIterator, MarkerTree)> { + pub fn children(&self) -> impl ExactSizeIterator, MarkerTree)> { self.map .iter() .map(|(range, node)| (range, MarkerTree(node.negate(self.id)))) @@ -1418,7 +1419,7 @@ impl Ord for StringMarkerTree<'_> { #[derive(PartialEq, Eq, Clone, Debug)] pub struct InMarkerTree<'a> { key: CanonicalMarkerValueString, - value: &'a str, + value: &'a ArcStr, high: NodeId, low: NodeId, } @@ -1430,7 +1431,7 @@ impl InMarkerTree<'_> { } /// The value (RHS) for this expression. - pub fn value(&self) -> &str { + pub fn value(&self) -> &ArcStr { self.value } @@ -1654,6 +1655,7 @@ mod test { use std::str::FromStr; use insta::assert_snapshot; + use uv_normalize::ExtraName; use uv_pep440::Version; @@ -2041,7 +2043,7 @@ mod test { MarkerExpression::String { key: MarkerValueString::OsName, operator: MarkerOperator::Equal, - value: "nt".to_string(), + value: arcstr::literal!("nt") } ); } diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 62706a4e6c89..4f0a4687a7dd 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -38,6 +38,7 @@ uv-types = { workspace = true } uv-warnings = { workspace = true } uv-workspace = { workspace = true } +arcstr = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } dashmap = { workspace = true } either = { workspace = true } diff --git a/crates/uv-resolver/src/resolution/output.rs b/crates/uv-resolver/src/resolution/output.rs index aa9385d71ed2..deec8ace1730 100644 --- a/crates/uv-resolver/src/resolution/output.rs +++ b/crates/uv-resolver/src/resolution/output.rs @@ -7,6 +7,7 @@ use petgraph::{ Directed, Direction, }; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; + use uv_configuration::{Constraints, Overrides}; use uv_distribution::Metadata; use uv_distribution_types::{ @@ -726,7 +727,7 @@ impl ResolverOutput { MarkerExpression::String { key: value_string.into(), operator: MarkerOperator::Equal, - value: from_env.to_string(), + value: from_env.into(), } } }; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 60d309bb8194..c1343a0f44e7 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1424,11 +1424,15 @@ impl ResolverState Date: Fri, 10 Jan 2025 15:52:56 -0500 Subject: [PATCH 108/135] Bump version to v0.5.17 (#10480) --- CHANGELOG.md | 56 +++++++++++++++++++++++++++ Cargo.lock | 4 +- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/getting-started/installation.md | 4 +- docs/guides/integration/aws-lambda.md | 4 +- docs/guides/integration/docker.md | 8 ++-- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 6 +-- pyproject.toml | 2 +- 10 files changed, 73 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4af1bff5e8..4c308ca9e796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,61 @@ # Changelog +## 0.5.17 + +This release includes support for generating lockfiles from scripts based on inline metadata, as defined in PEP 723. + +By default, scripts remain unlocked, and must be locked explicitly with `uv lock --script /path/to/script.py`, which +will generate a lockfile adjacent to the script (e.g., `script.py.lock`). Once generated, the lockfile will be +respected (and updated, if necessary) across `uv run --script`, `uv add --script`, and `uv remove --script` invocations. + +This release also includes support for `uv export --script` and `uv tree --script`. Both commands support PEP 723 +scripts with and without accompanying lockfiles. + +### Enhancements + +- Add support for locking PEP 723 scripts ([#10135](https://github.com/astral-sh/uv/pull/10135)) +- Respect PEP 723 script lockfiles in `uv run` ([#10136](https://github.com/astral-sh/uv/pull/10136)) +- Update PEP 723 lockfile in `uv add --script` ([#10145](https://github.com/astral-sh/uv/pull/10145)) +- Update PEP 723 lockfile in `uv remove --script` ([#10162](https://github.com/astral-sh/uv/pull/10162)) +- Add `--script` support to `uv export` for PEP 723 scripts ([#10160](https://github.com/astral-sh/uv/pull/10160)) +- Add `--script` support to `uv tree` for PEP 723 scripts ([#10159](https://github.com/astral-sh/uv/pull/10159)) +- Add `ls` alias to `uv {tool, python, pip} list` ([#10240](https://github.com/astral-sh/uv/pull/10240)) +- Allow reading `--with-requirements` from stdin in `uv add` and `uv run` ([#10447](https://github.com/astral-sh/uv/pull/10447)) +- Warn-and-ignore for unsupported `requirements.txt` options ([#10420](https://github.com/astral-sh/uv/pull/10420)) + +### Preview features + +- Add remaining Python type annotations to build backend ([#10434](https://github.com/astral-sh/uv/pull/10434)) + +### Performance + +- Avoid allocating for names in the PEP 508 parser ([#10476](https://github.com/astral-sh/uv/pull/10476)) +- Fetch concurrently for non-first-match index strategies ([#10432](https://github.com/astral-sh/uv/pull/10432)) +- Remove unnecessary `.to_string()` call ([#10419](https://github.com/astral-sh/uv/pull/10419)) +- Respect sentinels in package prioritization ([#10443](https://github.com/astral-sh/uv/pull/10443)) +- Use `ArcStr` for marker values ([#10453](https://github.com/astral-sh/uv/pull/10453)) +- Use `ArcStr` for package, extra, and group names ([#10475](https://github.com/astral-sh/uv/pull/10475)) +- Use `matches!` rather than `contains` in `requirements.txt` parsing ([#10423](https://github.com/astral-sh/uv/pull/10423)) +- Use faster disjointness check for markers ([#10439](https://github.com/astral-sh/uv/pull/10439)) +- Pre-compute PEP 508 markers from universal markers ([#10472](https://github.com/astral-sh/uv/pull/10472)) + +### Bug fixes + +- Fix `UV_FIND_LINKS` delimiter to split on commas ([#10477](https://github.com/astral-sh/uv/pull/10477)) +- Improve `uv tool list` output when tool environment is broken ([#10409](https://github.com/astral-sh/uv/pull/10409)) +- Only track markers for compatible versions ([#10457](https://github.com/astral-sh/uv/pull/10457)) +- Respect `requires-python` when installing tools ([#10401](https://github.com/astral-sh/uv/pull/10401)) +- Visit proxy packages eagerly ([#10441](https://github.com/astral-sh/uv/pull/10441)) +- Improve shell compatibility of `venv` activate scripts ([#10397](https://github.com/astral-sh/uv/pull/10397)) +- Read publish username from URL ([#10469](https://github.com/astral-sh/uv/pull/10469)) + +### Documentation + +- Add Lambda layer instructions to AWS Lambda guide ([#10411](https://github.com/astral-sh/uv/pull/10411)) +- Add `uv lock --script` to the docs ([#10414](https://github.com/astral-sh/uv/pull/10414)) +- Use Windows-specific instructions in Jupyter guide ([#10446](https://github.com/astral-sh/uv/pull/10446)) + + ## 0.5.16 ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index 7d894b5e005a..794fe515d071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4489,7 +4489,7 @@ checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "uv" -version = "0.5.16" +version = "0.5.17" dependencies = [ "anstream", "anyhow", @@ -5701,7 +5701,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.5.16" +version = "0.5.17" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index bfc8226167fd..36f44dc72c2c 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.5.16" +version = "0.5.17" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 5516afcdb635..2149039f8323 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.5.16" +version = "0.5.17" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d631f330479d..66e4b2dd68ba 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.5.16/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.5.17/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.5.16/install.ps1 | iex" + $ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.5.17/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 11ed1d7ea50a..7121c02f10ae 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.5.16 AS uv +FROM ghcr.io/astral-sh/uv:0.5.17 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -294,7 +294,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.5.16 AS uv +FROM ghcr.io/astral-sh/uv:0.5.17 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index fee04193f3d1..dcdb73e9144f 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -28,7 +28,7 @@ $ docker run ghcr.io/astral-sh/uv --help uv provides a distroless Docker image including the `uv` binary. The following tags are published: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.5.16` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.5.17` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.5` (the latest patch version) @@ -69,7 +69,7 @@ In addition, uv publishes the following images: As with the distroless image, each image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.5.16-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.5.17-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -107,13 +107,13 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.5.16 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.5.17 /uv /uvx /bin/ ``` Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.5.16/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.5.17/install.sh /uv-installer.sh ``` ### Installing a project diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index f8543c7750cc..b46635c5d0ec 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.5.16" + version: "0.5.17" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index e60c1785465b..5168c21f48be 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -36,7 +36,7 @@ To compile requirements via pre-commit, add the following to the `.pre-commit-co ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.16 + rev: 0.5.17 hooks: # Compile requirements - id: pip-compile @@ -48,7 +48,7 @@ To compile alternative files, modify `args` and `files`: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.16 + rev: 0.5.17 hooks: # Compile requirements - id: pip-compile @@ -61,7 +61,7 @@ To run the hook over multiple files at the same time: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.16 + rev: 0.5.17 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 843193109c26..7387c616ae3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.5.16" +version = "0.5.17" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From 918ddef090b36da7bd3a3117e59ea1a657d29795 Mon Sep 17 00:00:00 2001 From: bnorick Date: Fri, 10 Jan 2025 17:07:03 -0800 Subject: [PATCH 109/135] Fixes bug in `uv remove` when only comments exist (#10484) ## Summary Fixes a bug when there are only comments in the dependencies section. Basically, after one removes all dependencies, if there are remaining comments then the value unwrapped here https://github.com/astral-sh/uv/blob/c198e2233efaa037a9154fd7fe2625e4c78d976b/crates/uv-workspace/src/pyproject_mut.rs#L1309 is never properly initialized. It's initialized to `None`, here https://github.com/astral-sh/uv/blob/c198e2233efaa037a9154fd7fe2625e4c78d976b/crates/uv-workspace/src/pyproject_mut.rs#L1256, but doesn't get set to `Some(...)` until the first dependency here https://github.com/astral-sh/uv/blob/c198e2233efaa037a9154fd7fe2625e4c78d976b/crates/uv-workspace/src/pyproject_mut.rs#L1276 and since we remove them all... there are none. ## Test Plan Manually induced bug with ``` [project] name = "t1" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [ "duct>=0.6.4", "minilog>=2.3.1", # comment ] ``` Then running ``` $ RUST_LOG=trace RUST_BACKTRACE=full uv remove duct minilog DEBUG uv 0.5.8 DEBUG Found project root: `/home/bnorick/dev/workspace/t1` DEBUG No workspace root found, using project root thread 'main' panicked at crates/uv-workspace/src/pyproject_mut.rs:1294:73: called `Option::unwrap()` on a `None` value stack backtrace: 0: 0x5638d7bed6ba - 1: 0x5638d783760b - 2: 0x5638d7bae232 - 3: 0x5638d7bf0f07 - 4: 0x5638d7bf215c - 5: 0x5638d7bf1972 - 6: 0x5638d7bf1909 - 7: 0x5638d7bf18f4 - 8: 0x5638d75087d2 - 9: 0x5638d750896b - 10: 0x5638d7508d68 - 11: 0x5638d8dcf1bb - 12: 0x5638d76be271 - 13: 0x5638d75ef1f9 - 14: 0x5638d75fc3cd - 15: 0x5638d772d9de - 16: 0x5638d8476812 - 17: 0x5638d83e1894 - 18: 0x5638d84722d3 - 19: 0x5638d83e1372 - 20: 0x7f851cfc7d90 - 21: 0x7f851cfc7e40 - __libc_start_main 22: 0x5638d758e992 - 23: 0x0 - ``` --- crates/uv-workspace/src/pyproject_mut.rs | 12 ++--- crates/uv/tests/it/edit.rs | 61 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 248bcc1035b5..10d7591ed19b 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -1272,15 +1272,11 @@ fn reformat_array_multiline(deps: &mut Array) { .map(|(s, _)| s) .unwrap_or(decor_prefix); - // If there is no indentation, use four-space. - indentation_prefix = Some(if decor_prefix.is_empty() { - " ".to_string() - } else { - decor_prefix.to_string() - }); + indentation_prefix = (!decor_prefix.is_empty()).then_some(decor_prefix.to_string()); } - let indentation_prefix_str = format!("\n{}", indentation_prefix.as_ref().unwrap()); + let indentation_prefix_str = + format!("\n{}", indentation_prefix.as_deref().unwrap_or(" ")); for comment in find_comments(decor.prefix()).chain(find_comments(decor.suffix())) { match comment.comment_type { @@ -1306,7 +1302,7 @@ fn reformat_array_multiline(deps: &mut Array) { match comment.comment_type { CommentType::OwnLine => { let indentation_prefix_str = - format!("\n{}", indentation_prefix.as_ref().unwrap()); + format!("\n{}", indentation_prefix.as_deref().unwrap_or(" ")); rv.push_str(&indentation_prefix_str); } CommentType::EndOfLine => { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 537f2f83d454..84f8cd2cef51 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9024,3 +9024,64 @@ fn remove_requirement() -> Result<()> { Ok(()) } + +/// Remove all dependencies with remaining comments +#[test] +fn remove_all_with_comments() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "duct", + "minilog", + # foo + # bar + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.remove().arg("duct").arg("minilog"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + # foo + # bar + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +} From 54b3a438d03483c6e76f05168bd417657b693e4b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 10 Jan 2025 22:30:04 -0500 Subject: [PATCH 110/135] Avoid forking for identical markers (#10490) ## Summary If you have a dependency with a marker, and you add a constraint, it causes us to _always_ fork, because we represent the constraint as a second dependency with the marker repeated (and, therefore, we have two requirements of the same name, both with markers). I don't think we should fork here -- and in the end it's leading to this undesirable resolution: #10481. I tried to change constraints such that we just _reuse_ and augment the initial requirement, but that has a fairly negative effect on error messages: #10489. So this fix seems a bit better to me. Closes https://github.com/astral-sh/uv/issues/10481. --- crates/uv-resolver/src/resolver/mod.rs | 22 ++++++++++++++++++++++ crates/uv/tests/it/lock.rs | 26 ++++++++++---------------- crates/uv/tests/it/lock_scenarios.rs | 2 +- crates/uv/tests/it/pip_compile.rs | 14 +++++++++----- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index c1343a0f44e7..ced06cf26c9e 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -3302,6 +3302,28 @@ impl Forks { } continue; } + } else { + // If all dependencies have the same markers, we should also avoid forking. + if let Some(dep) = deps.first() { + let marker = dep.package.marker(); + if deps.iter().all(|dep| marker == dep.package.marker()) { + // Unless that "same marker" is a Python requirement that is stricter than + // the current Python requirement. In that case, we need to fork to respect + // the stricter requirement. + if marker::requires_python(marker) + .is_none_or(|bound| !python_requirement.raises(&bound)) + { + for dep in deps { + for fork in &mut forks { + if fork.env.included_by_marker(marker) { + fork.add_dependency(dep.clone()); + } + } + } + continue; + } + } + } } for dep in deps { let mut forker = match ForkingPossibility::new(env, &dep) { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index d4039ab4bf5c..6a67f8273de2 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -21706,9 +21706,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = 1 requires-python = ">=3.12.[X]" resolution-markers = [ - "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "(platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", + "platform_machine != 'aarch64' or sys_platform != 'linux'", "platform_machine == 'aarch64' and sys_platform == 'linux'", "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')", "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'", @@ -21915,7 +21913,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "9.1.0.70" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 }, @@ -21927,7 +21925,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "11.2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 }, @@ -21950,9 +21948,9 @@ fn lock_pytorch_cpu() -> Result<()> { version = "11.6.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 }, @@ -21965,7 +21963,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "12.3.1.170" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 }, @@ -22161,9 +22159,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "2.5.1+cu124" source = { registry = "https://download.pytorch.org/whl/cu124" } resolution-markers = [ - "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "(platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", + "platform_machine != 'aarch64' or sys_platform != 'linux'", ] dependencies = [ { name = "filelock", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, @@ -22248,9 +22244,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "0.20.1+cu124" source = { registry = "https://download.pytorch.org/whl/cu124" } resolution-markers = [ - "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "(platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", + "platform_machine != 'aarch64' or sys_platform != 'linux'", ] dependencies = [ { name = "numpy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, @@ -22267,7 +22261,7 @@ fn lock_pytorch_cpu() -> Result<()> { version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444 }, diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index 4ac688b7f457..377ab064139c 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -1320,7 +1320,7 @@ fn fork_marker_disjoint() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (sys_platform == 'linux'): + × No solution found when resolving dependencies: ╰─▶ Because your project depends on package-a{sys_platform == 'linux'}>=2 and package-a{sys_platform == 'linux'}<2, we can conclude that your project's requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 9fc609c5675e..c222d619f436 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -13270,7 +13270,9 @@ exceptiongroup==1.0.0rc8 # uv pip compile --cache-dir [CACHE_DIR] requirements.in -c constraints.txt --universal -p 3.10 alembic==1.8.1 # via -r requirements.in - astroid==2.13.5 + astroid==2.13.5 ; python_full_version >= '3.11' + # via pylint + astroid==3.1.0 ; python_full_version < '3.11' # via pylint asttokens==2.4.1 # via stack-data @@ -13298,7 +13300,7 @@ exceptiongroup==1.0.0rc8 # via pylint jedi==0.19.1 # via ipython - lazy-object-proxy==1.10.0 + lazy-object-proxy==1.10.0 ; python_full_version >= '3.11' # via astroid mako==1.3.2 # via alembic @@ -13322,7 +13324,9 @@ exceptiongroup==1.0.0rc8 # via stack-data pygments==2.17.2 # via ipython - pylint==2.15.8 + pylint==2.15.8 ; python_full_version >= '3.11' + # via -r requirements.in + pylint==3.1.0 ; python_full_version < '3.11' # via -r requirements.in six==1.16.0 # via asttokens @@ -13344,11 +13348,11 @@ exceptiongroup==1.0.0rc8 # sqlalchemy wcwidth==0.2.13 # via prompt-toolkit - wrapt==1.16.0 + wrapt==1.16.0 ; python_full_version >= '3.11' # via astroid ----- stderr ----- - Resolved 34 packages in [TIME] + Resolved 36 packages in [TIME] "###); Ok(()) From e57acc55518b19c177c48d8d9d449e8976fb1555 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 11 Jan 2025 09:06:26 -0500 Subject: [PATCH 111/135] Avoid prompting on terminals during publish tests (#10496) ## Summary Closes https://github.com/astral-sh/uv/issues/10493. ## Test Plan Run `cargo test --profile fast-build --no-fail-fast -p uv username_password_sources` from a terminal. --- crates/uv/src/commands/publish.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index b036dc1444e3..b3405ccd9d07 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -78,6 +78,7 @@ pub(crate) async fn publish( keyring_provider, &oidc_client, check_url.as_ref(), + Prompt::Enabled, printer, ) .await?; @@ -150,6 +151,14 @@ pub(crate) async fn publish( Ok(ExitStatus::Success) } +/// Whether to allow prompting for username and password. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Prompt { + Enabled, + #[allow(dead_code)] + Disabled, +} + /// Unify the different possible source for username and password information. /// /// Returns the publish URL, the username and the password. @@ -161,6 +170,7 @@ async fn gather_credentials( keyring_provider: KeyringProviderType, oidc_client: &BaseClient, check_url: Option<&IndexUrl>, + prompt: Prompt, printer: Printer, ) -> Result<(Url, Option, Option)> { // Support reading username and password from the URL, for symmetry with the index API. @@ -200,7 +210,10 @@ async fn gather_credentials( (Some("__token__".to_string()), Some(password.to_string())) } else { if username.is_none() && password.is_none() { - prompt_username_and_password()? + match prompt { + Prompt::Enabled => prompt_username_and_password()?, + Prompt::Disabled => (None, None), + } } else { (username, password) } @@ -289,8 +302,10 @@ fn prompt_username_and_password() -> Result<(Option, Option)> { #[cfg(test)] mod tests { use super::*; - use insta::assert_snapshot; + use std::str::FromStr; + + use insta::assert_snapshot; use url::Url; async fn credentials( @@ -307,6 +322,7 @@ mod tests { KeyringProviderType::Disabled, &client, None, + Prompt::Disabled, Printer::Quiet, ) .await From 5bc09a1e9e9cc1b2ac8ad2c968401e4e36f47085 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 11 Jan 2025 09:23:07 -0500 Subject: [PATCH 112/135] Revert "improve shell compatibility of venv activate scripts (#10397)" (#10497) ## Summary This reverts commit 2f7f9ea571f4fa8d2eefbfa4a95f6bc8fd18175c (https://github.com/astral-sh/uv/pull/10397). We're seeing some user-reported failures, so we need to investigate further before re-shipping. Re-opens https://github.com/astral-sh/uv/issues/7480. Closes https://github.com/astral-sh/uv/issues/10487. --- .github/workflows/ci.yml | 1 - crates/uv-virtualenv/src/activator/activate | 21 ++++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 736aaffd2833..fce3bceaa495 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,6 @@ jobs: version: ${{ env.SHELLCHECK_VERSION }} severity: style check_together: "yes" - additional_files: activate cargo-clippy: timeout-minutes: 10 diff --git a/crates/uv-virtualenv/src/activator/activate b/crates/uv-virtualenv/src/activator/activate index 011eac5abb82..5a49d9e48596 100644 --- a/crates/uv-virtualenv/src/activator/activate +++ b/crates/uv-virtualenv/src/activator/activate @@ -31,12 +31,9 @@ if [ -n "${BASH_VERSION:+x}" ] ; then exit 33 fi elif [ -n "${ZSH_VERSION:+x}" ] ; then - # we use eval in these paths to "hide" the fact that these branches - # genuinely don't work when run in the wrong shell. Any shell or - # linter that eagerly checks them would be right to complain! - eval 'SCRIPT_PATH="${(%):-%x}"' + SCRIPT_PATH="${(%):-%x}" elif [ -n "${KSH_VERSION:+x}" ] ; then - eval 'SCRIPT_PATH="${.sh.file}"' + SCRIPT_PATH="${.sh.file}" fi deactivate () { @@ -44,12 +41,12 @@ deactivate () { # reset old environment variables # ! [ -z ${VAR+_} ] returns true if VAR is declared at all - if [ -n "${_OLD_VIRTUAL_PATH:+_}" ] ; then + if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then PATH="$_OLD_VIRTUAL_PATH" export PATH unset _OLD_VIRTUAL_PATH fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then + if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" export PYTHONHOME unset _OLD_VIRTUAL_PYTHONHOME @@ -60,7 +57,7 @@ deactivate () { # we made may not be respected hash -r 2>/dev/null - if [ -n "${_OLD_VIRTUAL_PS1+_}" ] ; then + if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then PS1="$_OLD_VIRTUAL_PS1" export PS1 unset _OLD_VIRTUAL_PS1 @@ -78,7 +75,7 @@ deactivate () { deactivate nondestructive VIRTUAL_ENV='{{ VIRTUAL_ENV_DIR }}' -if { [ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]; } && command -v cygpath &> /dev/null ; then +if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV") fi export VIRTUAL_ENV @@ -87,8 +84,6 @@ _OLD_VIRTUAL_PATH="$PATH" PATH="$VIRTUAL_ENV/{{ BIN_NAME }}:$PATH" export PATH -# this file is templated, so the constant comparison here actually varies -# shellcheck disable=SC2050 if [ "x{{ VIRTUAL_PROMPT }}" != x ] ; then VIRTUAL_ENV_PROMPT="({{ VIRTUAL_PROMPT }}) " else @@ -97,7 +92,7 @@ fi export VIRTUAL_ENV_PROMPT # unset PYTHONHOME if set -if [ -n "${PYTHONHOME+_}" ] ; then +if ! [ -z "${PYTHONHOME+_}" ] ; then _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME" unset PYTHONHOME fi @@ -109,7 +104,7 @@ if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then fi # Make sure to unalias pydoc if it's already there -{ alias pydoc 2>/dev/null >/dev/null && unalias pydoc; } || true +alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true pydoc () { python -m pydoc "$@" From 27d1bad55076ebcad1fb536f0f1949de03cb7c5a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 11 Jan 2025 09:38:26 -0500 Subject: [PATCH 113/135] Bump version to v0.5.18 (#10499) --- CHANGELOG.md | 9 ++++++++- Cargo.lock | 4 ++-- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- docs/getting-started/installation.md | 4 ++-- docs/guides/integration/aws-lambda.md | 4 ++-- docs/guides/integration/docker.md | 8 ++++---- docs/guides/integration/github.md | 2 +- docs/guides/integration/pre-commit.md | 6 +++--- pyproject.toml | 2 +- 10 files changed, 25 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c308ca9e796..f1ca6fca8fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.18 + +### Bug fixes + +- Avoid forking for identical markers ([#10490](https://github.com/astral-sh/uv/pull/10490)) +- Avoid panic in `uv remove` when only comments exist ([#10484](https://github.com/astral-sh/uv/pull/10484)) +- Revert "improve shell compatibility of venv activate scripts (#10397)" ([#10497](https://github.com/astral-sh/uv/pull/10497)) + ## 0.5.17 This release includes support for generating lockfiles from scripts based on inline metadata, as defined in PEP 723. @@ -55,7 +63,6 @@ scripts with and without accompanying lockfiles. - Add `uv lock --script` to the docs ([#10414](https://github.com/astral-sh/uv/pull/10414)) - Use Windows-specific instructions in Jupyter guide ([#10446](https://github.com/astral-sh/uv/pull/10446)) - ## 0.5.16 ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index 794fe515d071..b4b7daefc07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4489,7 +4489,7 @@ checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "uv" -version = "0.5.17" +version = "0.5.18" dependencies = [ "anstream", "anyhow", @@ -5701,7 +5701,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.5.17" +version = "0.5.18" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 36f44dc72c2c..876e2d578ab0 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.5.17" +version = "0.5.18" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 2149039f8323..de27506f98d4 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.5.17" +version = "0.5.18" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 66e4b2dd68ba..1eec38086336 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.5.17/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.5.18/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.5.17/install.ps1 | iex" + $ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.5.18/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 7121c02f10ae..16cdab8e9691 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.5.17 AS uv +FROM ghcr.io/astral-sh/uv:0.5.18 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -294,7 +294,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.5.17 AS uv +FROM ghcr.io/astral-sh/uv:0.5.18 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index dcdb73e9144f..6d0ca0fb651d 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -28,7 +28,7 @@ $ docker run ghcr.io/astral-sh/uv --help uv provides a distroless Docker image including the `uv` binary. The following tags are published: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.5.17` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.5.18` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.5` (the latest patch version) @@ -69,7 +69,7 @@ In addition, uv publishes the following images: As with the distroless image, each image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.5.17-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.5.18-alpine`. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. @@ -107,13 +107,13 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.5.17 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.5.18 /uv /uvx /bin/ ``` Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.5.17/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.5.18/install.sh /uv-installer.sh ``` ### Installing a project diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index b46635c5d0ec..1f3c4aa510d9 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. - version: "0.5.17" + version: "0.5.18" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 5168c21f48be..8e7f623e7c39 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -36,7 +36,7 @@ To compile requirements via pre-commit, add the following to the `.pre-commit-co ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.17 + rev: 0.5.18 hooks: # Compile requirements - id: pip-compile @@ -48,7 +48,7 @@ To compile alternative files, modify `args` and `files`: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.17 + rev: 0.5.18 hooks: # Compile requirements - id: pip-compile @@ -61,7 +61,7 @@ To run the hook over multiple files at the same time: ```yaml title=".pre-commit-config.yaml" - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.5.17 + rev: 0.5.18 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index 7387c616ae3f..88b0e3802edf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.5.17" +version = "0.5.18" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From aa1fd76a15f5e776753480b059dce8352e6451f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:26:36 -0500 Subject: [PATCH 114/135] Update Rust crate async-trait to v0.1.85 (#10501) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4b7daefc07f..99933e0dae48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.84" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", From 59eb60b81992235999143172fd17d7e559aa929c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:26:44 -0500 Subject: [PATCH 115/135] Update Rust crate cargo-util to v0.2.17 (#10502) --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99933e0dae48..41e7b4c7af78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,9 +476,9 @@ dependencies = [ [[package]] name = "cargo-util" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b15bbe49616ee353fadadf6de5a24136f3fe8fdbd5eb0894be9f8a42c905674" +checksum = "7cccd15f96a29696e13e1d5fa10dd1dbed2e172f58b6e6124a9a4fa695363fdd" dependencies = [ "anyhow", "core-foundation", @@ -684,7 +684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1038,7 +1038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1912,7 +1912,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2800,7 +2800,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3237,7 +3237,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3806,7 +3806,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5992,7 +5992,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] From 716f3ec9ebc2b84710b8d683e107a2fd77181863 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:27:01 -0500 Subject: [PATCH 116/135] Update Rust crate goblin to v0.9.3 (#10504) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41e7b4c7af78..fe9b8445c916 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1389,9 +1389,9 @@ dependencies = [ [[package]] name = "goblin" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53ab3f32d1d77146981dea5d6b1e8fe31eedcb7013e5e00d6ccd1259a4b4d923" +checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745" dependencies = [ "log", "plain", From 94d4babe1a3792d1f354cdfc740c7cdf2b66e849 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:27:53 -0500 Subject: [PATCH 117/135] Update Rust crate proc-macro2 to v1.0.93 (#10506) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe9b8445c916..94f2c61fcb6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2688,9 +2688,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] From bee6eff23546b6c2be0cac2bf8a9525b14102afb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:28:23 -0500 Subject: [PATCH 118/135] Update Rust crate serde_json to v1.0.135 (#10509) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94f2c61fcb6b..0d3f6751bb58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3476,9 +3476,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", "memchr", From 3e2a8cbd04c96c399d900ba6c98c2ad72026633a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:28:56 -0500 Subject: [PATCH 119/135] Update Rust crate syn to v2.0.96 (#10510) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d3f6751bb58..a57923b38513 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3724,9 +3724,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", From ed5a53f01b571328ab28d1612cf7f69a3f262fcf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:29:03 -0500 Subject: [PATCH 120/135] Update Rust crate thiserror to v2.0.11 (#10511) --- Cargo.lock | 84 ++++++++++++++++----------------- crates/uv-trampoline/Cargo.lock | 8 ++-- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a57923b38513..900b598a7dcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,7 +253,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.11", "url", "walkdir", ] @@ -295,7 +295,7 @@ dependencies = [ "self-replace", "serde", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "url", ] @@ -2475,7 +2475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.9", + "thiserror 2.0.11", "ucd-trie", ] @@ -2747,7 +2747,7 @@ dependencies = [ "log", "priority-queue", "rustc-hash", - "thiserror 2.0.9", + "thiserror 2.0.11", "version-ranges", ] @@ -2764,7 +2764,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", ] @@ -2783,7 +2783,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.9", + "thiserror 2.0.11", "tinyvec", "tracing", "web-time", @@ -3911,11 +3911,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.11", ] [[package]] @@ -3931,9 +3931,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -4530,7 +4530,7 @@ dependencies = [ "tar", "tempfile", "textwrap", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "toml", "toml_edit", @@ -4655,7 +4655,7 @@ dependencies = [ "spdx", "tar", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml", "tracing", "uv-distribution-filename", @@ -4687,7 +4687,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "toml_edit", "tracing", @@ -4736,7 +4736,7 @@ dependencies = [ "globwalk", "schemars", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml", "tracing", ] @@ -4804,7 +4804,7 @@ dependencies = [ "serde", "serde_json", "sys-info", - "thiserror 2.0.9", + "thiserror 2.0.11", "tl", "tokio", "tokio-util", @@ -4843,7 +4843,7 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "url", "uv-auth", @@ -4926,7 +4926,7 @@ dependencies = [ "futures", "itertools 0.14.0", "rustc-hash", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", "uv-build-backend", @@ -4966,7 +4966,7 @@ dependencies = [ "rustc-hash", "serde", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-util", "tracing", @@ -5001,7 +5001,7 @@ dependencies = [ "insta", "rkyv", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "url", "uv-normalize", "uv-pep440", @@ -5024,7 +5024,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "url", "urlencoding", @@ -5056,7 +5056,7 @@ dependencies = [ "reqwest", "rustc-hash", "sha2", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-util", "tracing", @@ -5101,7 +5101,7 @@ dependencies = [ "reqwest", "reqwest-middleware", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", "url", @@ -5122,7 +5122,7 @@ dependencies = [ "regex", "regex-automata 0.4.9", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "walkdir", ] @@ -5152,7 +5152,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "uv-cache-info", "uv-distribution-filename", @@ -5180,7 +5180,7 @@ dependencies = [ "rustc-hash", "same-file", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", "url", @@ -5221,7 +5221,7 @@ dependencies = [ "async_zip", "fs-err 3.0.0", "futures", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-util", "uv-distribution-filename", @@ -5284,7 +5284,7 @@ dependencies = [ "serde", "serde_json", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "tracing-test", "unicode-width 0.1.14", @@ -5318,7 +5318,7 @@ dependencies = [ "insta", "rustc-hash", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -5339,7 +5339,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-util", "tracing", @@ -5372,7 +5372,7 @@ dependencies = [ "schemars", "serde", "serde-untagged", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml", "toml_edit", "tracing", @@ -5414,7 +5414,7 @@ dependencies = [ "temp-env", "tempfile", "test-log", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-util", "tracing", @@ -5453,7 +5453,7 @@ dependencies = [ "futures", "rustc-hash", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml", "tracing", "url", @@ -5491,7 +5491,7 @@ dependencies = [ "reqwest-middleware", "tempfile", "test-case", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", "unscanny", @@ -5529,7 +5529,7 @@ dependencies = [ "schemars", "serde", "textwrap", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-stream", "toml", @@ -5567,7 +5567,7 @@ dependencies = [ "indoc", "memchr", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml", "uv-pep440", "uv-pep508", @@ -5588,7 +5588,7 @@ dependencies = [ "schemars", "serde", "textwrap", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml", "tracing", "url", @@ -5645,7 +5645,7 @@ dependencies = [ "pathdiff", "self-replace", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml", "toml_edit", "tracing", @@ -5672,7 +5672,7 @@ dependencies = [ "assert_cmd", "assert_fs", "fs-err 3.0.0", - "thiserror 2.0.9", + "thiserror 2.0.11", "uv-fs", "which", "zip", @@ -5684,7 +5684,7 @@ version = "0.0.1" dependencies = [ "anyhow", "rustc-hash", - "thiserror 2.0.9", + "thiserror 2.0.11", "url", "uv-cache", "uv-configuration", @@ -5711,7 +5711,7 @@ dependencies = [ "itertools 0.14.0", "pathdiff", "self-replace", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "uv-fs", "uv-platform-tags", @@ -5747,7 +5747,7 @@ dependencies = [ "schemars", "serde", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "toml", "toml_edit", diff --git a/crates/uv-trampoline/Cargo.lock b/crates/uv-trampoline/Cargo.lock index eaf671b0e217..ca87ceb981e0 100644 --- a/crates/uv-trampoline/Cargo.lock +++ b/crates/uv-trampoline/Cargo.lock @@ -426,18 +426,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", From 08569667cb2080312ff0cc6dcf73d6b352534725 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:29:13 -0500 Subject: [PATCH 121/135] Update Rust crate bitflags to v2.7.0 (#10512) --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 900b598a7dcc..c5c604fbc44e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,9 +352,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" [[package]] name = "block-buffer" @@ -1370,7 +1370,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "ignore", "walkdir", ] @@ -2027,7 +2027,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "libc", "redox_syscall 0.5.8", ] @@ -2280,7 +2280,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cfg-if", "cfg_aliases", "libc", @@ -2701,7 +2701,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "flate2", "hex", "procfs-core", @@ -2714,7 +2714,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "hex", ] @@ -2907,7 +2907,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", ] [[package]] @@ -3233,7 +3233,7 @@ version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "errno", "libc", "linux-raw-sys", @@ -3398,7 +3398,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "core-foundation", "core-foundation-sys", "libc", @@ -5014,7 +5014,7 @@ version = "0.0.1" dependencies = [ "anyhow", "arcstr", - "bitflags 2.6.0", + "bitflags 2.7.0", "fs-err 3.0.0", "itertools 0.14.0", "jiff", From 69102e056399d4e6c9f93ee21a0427fc472f4fbc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:55:31 +0000 Subject: [PATCH 122/135] Update Rust crate tokio to v1.43.0 (#10514) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5c604fbc44e..fde624b563c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4037,9 +4037,9 @@ source = "git+https://github.com/astral-sh/tl.git?rev=6e25b2ee2513d75385101a8ff9 [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -4055,9 +4055,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", From 2c021e2f7d5ed8fab047969495737487a69c1508 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:08:13 +0000 Subject: [PATCH 123/135] Update Rust crate winreg to 0.53.0 (#10518) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fde624b563c1..d5e5fec81f8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6308,9 +6308,9 @@ dependencies = [ [[package]] name = "winreg" -version = "0.52.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +checksum = "89a47b489f8fc5b949477e89dca4d1617f162c6c53fbcbefde553ab17b342ff9" dependencies = [ "cfg-if", "windows-sys 0.48.0", diff --git a/Cargo.toml b/Cargo.toml index ae7315f428e5..f26f2de4c7b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,7 +182,7 @@ which = { version = "7.0.0", features = ["regex"] } windows-registry = { version = "0.3.0" } windows-result = { version = "0.2.0" } windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO"] } -winreg = { version = "0.52.0" } +winreg = { version = "0.53.0" } winsafe = { version = "0.0.22", features = ["kernel"] } wiremock = { version = "0.6.2" } xz2 = { version = "0.1.7" } From 0650e178bfbd70f3b5e343a96833525b6e517ee4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:16:20 +0000 Subject: [PATCH 124/135] Update pre-commit hook astral-sh/ruff-pre-commit to v0.9.1 (#10519) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c4cff397a42..ebd051fec2f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: types_or: [yaml, json5] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.9.1 hooks: - id: ruff-format - id: ruff From 8e0c7cfd76a744a2367e85078a128a9bb894c076 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:24:45 +0000 Subject: [PATCH 125/135] Update Rust crate clap to v4.5.26 (#10503) --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5e5fec81f8e..69e7ec7e889a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,9 +565,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -575,9 +575,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", @@ -618,9 +618,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", @@ -684,7 +684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -1038,7 +1038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1912,7 +1912,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2800,7 +2800,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3237,7 +3237,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3806,7 +3806,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5992,7 +5992,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] From 5aa53863a77a008b7dcabaef0f57b47839ba464d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:26:21 +0000 Subject: [PATCH 126/135] Update Rust crate rustix to v0.38.43 (#10508) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69e7ec7e889a..d2ba39b1a9fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3229,9 +3229,9 @@ checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags 2.7.0", "errno", From 9e948b73638c3c9a164a222fdc23dafe21bff2df Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 11 Jan 2025 18:12:11 -0500 Subject: [PATCH 127/135] Remove resolved build tag TODO (#10526) --- crates/uv-distribution-filename/src/wheel.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/uv-distribution-filename/src/wheel.rs b/crates/uv-distribution-filename/src/wheel.rs index 0d476213578b..fda2de853d4b 100644 --- a/crates/uv-distribution-filename/src/wheel.rs +++ b/crates/uv-distribution-filename/src/wheel.rs @@ -102,12 +102,6 @@ impl WheelFilename { // The wheel filename should contain either five or six entries. If six, then the third // entry is the build tag. If five, then the third entry is the Python tag. // https://www.python.org/dev/peps/pep-0427/#file-name-convention - // - // 2023-11-08(burntsushi): It looks like the code below actually drops - // the build tag if one is found. According to PEP 0427, the build tag - // is used to break ties. This might mean that we generate identical - // `WheelName` values for multiple distinct wheels, but it's not clear - // if this is a problem in practice. let mut parts = stem.split('-'); let name = parts From 5788cd2b18605d02402a7bb5b568736b435ff535 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 11 Jan 2025 18:39:46 -0500 Subject: [PATCH 128/135] Fix typo in `version_map.rs` (#10528) --- crates/uv-resolver/src/version_map.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 5056916738fa..60d822f20ad6 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -524,7 +524,7 @@ impl VersionMapLazy { let priority = if let Some(tags) = &self.tags { match filename.compatibility(tags) { TagCompatibility::Incompatible(tag) => { - return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag)) + return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag)); } TagCompatibility::Compatible(priority) => Some(priority), } @@ -565,7 +565,7 @@ impl VersionMapLazy { /// a single version of a package. #[derive(Debug)] enum LazyPrioritizedDist { - /// Represents a eagerly constructed distribution from a + /// Represents an eagerly constructed distribution from a /// `FlatDistributions`. OnlyFlat(PrioritizedDist), /// Represents a lazily constructed distribution from an index into a From 72692734583a648c93ddc855f40f192db3c0dfbd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 11 Jan 2025 20:37:57 -0500 Subject: [PATCH 129/135] Update packse to include `--python-platform` (#10531) ## Summary Relevant for: https://github.com/astral-sh/uv/pull/10527. --- crates/uv/tests/it/common/mod.rs | 2 +- crates/uv/tests/it/lock_scenarios.rs | 2 +- crates/uv/tests/it/pip_compile_scenarios.rs | 2 +- crates/uv/tests/it/pip_install_scenarios.rs | 5 ++++- scripts/scenarios/requirements.txt | 2 +- scripts/scenarios/templates/install.mustache | 3 +++ 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index b34a6faaf585..7a25115f1440 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -32,7 +32,7 @@ use uv_static::EnvVars; // Exclude any packages uploaded after this date. static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z"; -pub const PACKSE_VERSION: &str = "0.3.42"; +pub const PACKSE_VERSION: &str = "0.3.43"; /// Using a find links url allows using `--index-url` instead of `--extra-index-url` in tests /// to prevent dependency confusion attacks against our test suite. diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index 377ab064139c..831d9bf2a9b2 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::needless_raw_string_hashes)] diff --git a/crates/uv/tests/it/pip_compile_scenarios.rs b/crates/uv/tests/it/pip_compile_scenarios.rs index 722d3f0e001c..93157dc32c76 100644 --- a/crates/uv/tests/it/pip_compile_scenarios.rs +++ b/crates/uv/tests/it/pip_compile_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index efd65ae30cca..1965a4f596f7 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] @@ -4079,6 +4079,7 @@ fn no_sdist_no_wheels_with_matching_abi() { filters.push((r"no-sdist-no-wheels-with-matching-abi-", "package-")); uv_snapshot!(filters, command(&context) + .arg("--python-platform=x86_64-manylinux2014") .arg("no-sdist-no-wheels-with-matching-abi-a") , @r###" success: false @@ -4119,6 +4120,7 @@ fn no_sdist_no_wheels_with_matching_platform() { filters.push((r"no-sdist-no-wheels-with-matching-platform-", "package-")); uv_snapshot!(filters, command(&context) + .arg("--python-platform=x86_64-manylinux2014") .arg("no-sdist-no-wheels-with-matching-platform-a") , @r###" success: false @@ -4159,6 +4161,7 @@ fn no_sdist_no_wheels_with_matching_python() { filters.push((r"no-sdist-no-wheels-with-matching-python-", "package-")); uv_snapshot!(filters, command(&context) + .arg("--python-platform=x86_64-manylinux2014") .arg("no-sdist-no-wheels-with-matching-python-a") , @r###" success: false diff --git a/scripts/scenarios/requirements.txt b/scripts/scenarios/requirements.txt index d6c9203d0956..0a6f0cbcd5fd 100644 --- a/scripts/scenarios/requirements.txt +++ b/scripts/scenarios/requirements.txt @@ -10,7 +10,7 @@ msgspec==0.18.6 # via packse packaging==24.2 # via hatchling -packse==0.3.42 +packse==0.3.43 # via -r scripts/scenarios/requirements.in pathspec==0.12.1 # via hatchling diff --git a/scripts/scenarios/templates/install.mustache b/scripts/scenarios/templates/install.mustache index 48f86e7ffe1a..be425f92948c 100644 --- a/scripts/scenarios/templates/install.mustache +++ b/scripts/scenarios/templates/install.mustache @@ -88,6 +88,9 @@ fn {{module_name}}() { .arg("--no-binary") .arg("{{.}}") {{/resolver_options.no_binary}} + {{#resolver_options.python_platform}} + .arg("--python-platform={{.}}") + {{/resolver_options.python_platform}} {{#root.requires}} .arg("{{requirement}}") {{/root.requires}}, @r###" From 051aaa5fe57de3a5e461693c4c75d5631382d1df Mon Sep 17 00:00:00 2001 From: Sergei Nizovtsev Date: Sun, 12 Jan 2025 05:30:46 +0300 Subject: [PATCH 130/135] Fix git-tag cache-key reader in case of slashes (#10467) (#10500) ## Summary The assumption that all tags are listed under a flat `.git/ref/tags` structure was wrong. Git creates a hierarchy of directories for tags containing slashes. To fix the cache key calculation, we need to recursively traverse all files under that folder instead. ## Test Plan 1. Create an `uv` project with git-tag cache-keys; 2. Add any tag with slash; 3. Run `uv sync` and see uv_cache_info error in verbose log; 4. `uv sync` doesn't trigger reinstall on next tag addition or removal; 5. With fix applied, reinstall triggers on every tag update and there are no errors in the log. Fixes #10467 --------- Co-authored-by: Sergei Nizovtsev Co-authored-by: Charlie Marsh --- Cargo.lock | 1 + crates/uv-cache-info/Cargo.toml | 1 + crates/uv-cache-info/src/git_info.rs | 32 +++++++++++++++++----------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2ba39b1a9fc..c6696c60679b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4739,6 +4739,7 @@ dependencies = [ "thiserror 2.0.11", "toml", "tracing", + "walkdir", ] [[package]] diff --git a/crates/uv-cache-info/Cargo.toml b/crates/uv-cache-info/Cargo.toml index ec17a4416a0d..6b10bbebe853 100644 --- a/crates/uv-cache-info/Cargo.toml +++ b/crates/uv-cache-info/Cargo.toml @@ -23,3 +23,4 @@ serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } +walkdir = { workspace = true } diff --git a/crates/uv-cache-info/src/git_info.rs b/crates/uv-cache-info/src/git_info.rs index 9df098e6959e..ad566d13bdf9 100644 --- a/crates/uv-cache-info/src/git_info.rs +++ b/crates/uv-cache-info/src/git_info.rs @@ -1,6 +1,9 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use tracing::warn; +use walkdir::WalkDir; + #[derive(Debug, thiserror::Error)] pub(crate) enum GitInfoError { #[error("The repository at {0} is missing a `.git` directory")] @@ -80,24 +83,27 @@ impl Tags { .find(|git_dir| git_dir.exists()) .ok_or_else(|| GitInfoError::MissingGitDir(path.to_path_buf()))?; - let git_refs_path = - git_refs(&git_dir).ok_or_else(|| GitInfoError::MissingRefs(git_dir.clone()))?; + let git_tags_path = git_refs(&git_dir) + .ok_or_else(|| GitInfoError::MissingRefs(git_dir.clone()))? + .join("tags"); let mut tags = BTreeMap::new(); // Map each tag to its commit. - let read_dir = match fs_err::read_dir(git_refs_path.join("tags")) { - Ok(read_dir) => read_dir, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Ok(Self(tags)); - } - Err(err) => return Err(err.into()), - }; - for entry in read_dir { - let entry = entry?; + for entry in WalkDir::new(&git_tags_path).contents_first(true) { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + warn!("Failed to read Git tags: {err}"); + continue; + } + }; let path = entry.path(); - if let Some(tag) = path.file_name().and_then(|name| name.to_str()) { - let commit = fs_err::read_to_string(&path)?.trim().to_string(); + if !entry.file_type().is_file() { + continue; + } + if let Ok(Some(tag)) = path.strip_prefix(&git_tags_path).map(|name| name.to_str()) { + let commit = fs_err::read_to_string(path)?.trim().to_string(); // The commit should be 40 hexadecimal characters. if commit.len() != 40 { From 4d3809cc6b28d71f26a782929b994abfebd7973c Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:19:33 -0500 Subject: [PATCH 131/135] Upgrade Rust toolchain to 1.84.0 (#10533) ## Summary Upgrade the rust toolchain to 1.84.0. This PR does not bump the MSRV. --- Cargo.toml | 3 +++ crates/uv-auth/src/middleware.rs | 6 +++--- crates/uv-configuration/src/dev.rs | 2 +- crates/uv-dispatch/src/lib.rs | 2 +- crates/uv-extract/src/stream.rs | 4 ++-- crates/uv-fs/src/lib.rs | 4 ++-- crates/uv-install-wheel/src/wheel.rs | 4 ++-- crates/uv-pep508/src/lib.rs | 6 +++--- crates/uv-pep508/src/unnamed.rs | 6 +++--- crates/uv-pep508/src/verbatim_url.rs | 2 +- crates/uv-python/src/discovery.rs | 2 +- crates/uv-python/src/installation.rs | 8 ++++---- crates/uv-python/src/interpreter.rs | 8 ++++---- crates/uv-python/src/lib.rs | 12 ++++++------ crates/uv-resolver/src/lock/mod.rs | 4 ++-- crates/uv-resolver/src/resolver/provider.rs | 2 +- crates/uv-resolver/src/yanks.rs | 2 +- crates/uv-scripts/src/lib.rs | 4 ++-- crates/uv-trampoline-builder/src/lib.rs | 12 ++++++------ crates/uv/src/commands/pip/latest.rs | 2 +- crates/uv/src/commands/project/init.rs | 12 ++++++------ crates/uv/src/commands/project/mod.rs | 2 +- crates/uv/src/commands/project/run.rs | 8 +++----- crates/uv/tests/it/common/mod.rs | 2 +- crates/uv/tests/it/pip_compile.rs | 4 ++-- crates/uv/tests/it/pip_install.rs | 4 ++-- rust-toolchain.toml | 2 +- 27 files changed, 65 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f26f2de4c7b2..e97d1ae6657c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -225,6 +225,9 @@ rc_mutex = "warn" rest_pat_in_fully_bound_structs = "warn" if_not_else = "allow" +# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved. +large_stack_arrays = "allow" + [profile.release] strip = true lto = "fat" diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 3c78600d865b..16c4e542b72b 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -741,7 +741,7 @@ mod tests { let mut netrc_file = NamedTempFile::new()?; writeln!( netrc_file, - r#"machine {} login {username} password {password}"#, + r"machine {} login {username} password {password}", base_url.host_str().unwrap() )?; @@ -788,7 +788,7 @@ mod tests { let mut netrc_file = NamedTempFile::new()?; writeln!( netrc_file, - r#"machine example.com login {username} password {password}"#, + r"machine example.com login {username} password {password}", )?; let client = test_client_builder() @@ -829,7 +829,7 @@ mod tests { let mut netrc_file = NamedTempFile::new()?; writeln!( netrc_file, - r#"machine {} login {username} password {password}"#, + r"machine {} login {username} password {password}", base_url.host_str().unwrap() )?; diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index cce967ba1f89..3a94c6f4be16 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -292,7 +292,7 @@ impl DevGroupsSpecification { self.groups .as_ref() - .map_or(false, |groups| groups.contains(group)) + .is_some_and(|groups| groups.contains(group)) } } diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index c84e81271c0a..28a0a5ba0564 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -163,7 +163,7 @@ impl<'a> BuildDispatch<'a> { } #[allow(refining_impl_trait)] -impl<'a> BuildContext for BuildDispatch<'a> { +impl BuildContext for BuildDispatch<'_> { type SourceDistBuilder = SourceBuild; fn interpreter(&self) -> &Interpreter { diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index 483be2f50588..49ae138c451d 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -139,8 +139,8 @@ pub async fn unzip( /// Unpack the given tar archive into the destination directory. /// /// This is equivalent to `archive.unpack_in(dst)`, but it also preserves the executable bit. -async fn untar_in<'a>( - mut archive: tokio_tar::Archive<&'a mut (dyn tokio::io::AsyncRead + Unpin)>, +async fn untar_in( + mut archive: tokio_tar::Archive<&'_ mut (dyn tokio::io::AsyncRead + Unpin)>, dst: &Path, ) -> std::io::Result<()> { let mut entries = archive.entries()?; diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index f7dadb6aeddb..8f6cbc9377e4 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -516,7 +516,7 @@ pub fn is_temporary(path: impl AsRef) -> bool { path.as_ref() .file_name() .and_then(|name| name.to_str()) - .map_or(false, |name| name.starts_with(".tmp")) + .is_some_and(|name| name.starts_with(".tmp")) } /// A file lock that is automatically released when dropped. @@ -588,7 +588,7 @@ impl LockedFile { impl Drop for LockedFile { fn drop(&mut self) { - if let Err(err) = self.0.file().unlock() { + if let Err(err) = fs2::FileExt::unlock(self.0.file()) { error!( "Failed to unlock {}; program may be stuck: {}", self.0.path().display(), diff --git a/crates/uv-install-wheel/src/wheel.rs b/crates/uv-install-wheel/src/wheel.rs index 8218f75ae758..9e5698e0db8a 100644 --- a/crates/uv-install-wheel/src/wheel.rs +++ b/crates/uv-install-wheel/src/wheel.rs @@ -34,7 +34,7 @@ fn get_script_launcher(entry_point: &Script, shebang: &str) -> String { let import_name = entry_point.import_name(); format!( - r##"{shebang} + r#"{shebang} # -*- coding: utf-8 -*- import re import sys @@ -42,7 +42,7 @@ from {module} import {import_name} if __name__ == "__main__": sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit({function}()) -"## +"# ) } diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index 087f69d316f4..b30bbc670a97 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -930,12 +930,12 @@ fn parse_pep508_requirement( if let Some((pos, char)) = cursor.next().filter(|(_, c)| *c != '#') { let message = if char == '#' { format!( - r#"Expected end of input or `;`, found `{char}`; comments must be preceded by a leading space"# + r"Expected end of input or `;`, found `{char}`; comments must be preceded by a leading space" ) } else if marker.is_none() { - format!(r#"Expected end of input or `;`, found `{char}`"#) + format!(r"Expected end of input or `;`, found `{char}`") } else { - format!(r#"Expected end of input, found `{char}`"#) + format!(r"Expected end of input, found `{char}`") }; return Err(Pep508Error { message: Pep508ErrorSource::String(message), diff --git a/crates/uv-pep508/src/unnamed.rs b/crates/uv-pep508/src/unnamed.rs index 2e4361b940cc..0893cc989b04 100644 --- a/crates/uv-pep508/src/unnamed.rs +++ b/crates/uv-pep508/src/unnamed.rs @@ -176,12 +176,12 @@ fn parse_unnamed_requirement( if let Some((pos, char)) = cursor.next() { let message = if char == '#' { format!( - r#"Expected end of input or `;`, found `{char}`; comments must be preceded by a leading space"# + r"Expected end of input or `;`, found `{char}`; comments must be preceded by a leading space" ) } else if marker.is_none() { - format!(r#"Expected end of input or `;`, found `{char}`"#) + format!(r"Expected end of input or `;`, found `{char}`") } else { - format!(r#"Expected end of input, found `{char}`"#) + format!(r"Expected end of input, found `{char}`") }; return Err(Pep508Error { message: Pep508ErrorSource::String(message), diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index 73b06158a916..4d715b389989 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -406,7 +406,7 @@ pub fn looks_like_git_repository(url: &Url) -> bool { .map_or(true, |ext| ext.eq_ignore_ascii_case("git")) && url .path_segments() - .map_or(false, |segments| segments.count() == 2) + .is_some_and(|segments| segments.count() == 2) } /// Split the fragment from a URL. diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 879bb8b240ef..77fb6acf64be 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -1154,7 +1154,7 @@ pub(crate) fn is_windows_store_shim(path: &Path) -> bool { component.starts_with("python") && std::path::Path::new(component) .extension() - .map_or(false, |ext| ext.eq_ignore_ascii_case("exe")) + .is_some_and(|ext| ext.eq_ignore_ascii_case("exe")) }) { return false; diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 2733a14a0885..e7fc3994056c 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -78,12 +78,12 @@ impl PythonInstallation { /// Find or fetch a [`PythonInstallation`]. /// /// Unlike [`PythonInstallation::find`], if the required Python is not installed it will be installed automatically. - pub async fn find_or_download<'a>( + pub async fn find_or_download( request: Option<&PythonRequest>, environments: EnvironmentPreference, preference: PythonPreference, python_downloads: PythonDownloads, - client_builder: &BaseClientBuilder<'a>, + client_builder: &BaseClientBuilder<'_>, cache: &Cache, reporter: Option<&dyn Reporter>, python_install_mirror: Option<&str>, @@ -127,9 +127,9 @@ impl PythonInstallation { } /// Download and install the requested installation. - pub async fn fetch<'a>( + pub async fn fetch( request: PythonDownloadRequest, - client_builder: &BaseClientBuilder<'a>, + client_builder: &BaseClientBuilder<'_>, cache: &Cache, reporter: Option<&dyn Reporter>, python_install_mirror: Option<&str>, diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index e725fbccd0f0..13a12be575f0 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -894,10 +894,10 @@ mod tests { fs::write( &mocked_interpreter, - formatdoc! {r##" + formatdoc! {r" #!/bin/bash echo '{json}' - "##}, + "}, ) .unwrap(); @@ -913,10 +913,10 @@ mod tests { ); fs::write( &mocked_interpreter, - formatdoc! {r##" + formatdoc! {r" #!/bin/bash echo '{}' - "##, json.replace("3.12", "3.13")}, + ", json.replace("3.12", "3.13")}, ) .unwrap(); let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap(); diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 982878ff3d56..ea5b3e57275a 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -282,10 +282,10 @@ mod tests { fs_err::create_dir_all(path.parent().unwrap())?; fs_err::write( path, - formatdoc! {r##" + formatdoc! {r" #!/bin/bash echo '{json}' - "##}, + "}, )?; fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?; @@ -304,10 +304,10 @@ mod tests { fs_err::write( path, - formatdoc! {r##" + formatdoc! {r" #!/bin/bash echo '{output}' 1>&2 - "##}, + "}, )?; fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?; @@ -525,10 +525,10 @@ mod tests { #[cfg(unix)] fs_err::write( children[0].join(format!("python{}", env::consts::EXE_SUFFIX)), - formatdoc! {r##" + formatdoc! {r" #!/bin/bash echo 'foo' - "##}, + "}, )?; fs_err::set_permissions( children[0].join(format!("python{}", env::consts::EXE_SUFFIX)), diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index c0255804641c..283889b91eca 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -953,7 +953,7 @@ impl Lock { .ok() .flatten() .map(|package| matches!(package.id.source, Source::Virtual(_))); - if actual.map_or(true, |actual| actual != expected) { + if actual != Some(expected) { return Ok(SatisfiesResult::MismatchedSources(name.clone(), expected)); } } @@ -973,7 +973,7 @@ impl Lock { .ok() .flatten() .map(|package| &package.id.version); - if actual.map_or(true, |actual| actual != expected) { + if actual != Some(expected) { return Ok(SatisfiesResult::MismatchedVersion( name.clone(), expected.clone(), diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 2d6173a8c6ba..dc9b38e08ec9 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -145,7 +145,7 @@ impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> { } } -impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, Context> { +impl ResolverProvider for DefaultResolverProvider<'_, Context> { /// Make a "Simple API" request for the package and convert the result to a [`VersionMap`]. async fn get_package_versions<'io>( &'io self, diff --git a/crates/uv-resolver/src/yanks.rs b/crates/uv-resolver/src/yanks.rs index 6ed70a57e93f..6c43f2ed9802 100644 --- a/crates/uv-resolver/src/yanks.rs +++ b/crates/uv-resolver/src/yanks.rs @@ -55,6 +55,6 @@ impl AllowedYanks { pub fn contains(&self, package_name: &PackageName, version: &Version) -> bool { self.0 .get(package_name) - .map_or(false, |versions| versions.contains(version)) + .is_some_and(|versions| versions.contains(version)) } } diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index a17398e278a0..ad25ac131c9d 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -205,10 +205,10 @@ impl Pep723Script { let metadata = serialize_metadata(&default_metadata); let script = if let Some(existing_contents) = existing_contents { - indoc::formatdoc! {r#" + indoc::formatdoc! {r" {metadata} {content} - "#, + ", content = String::from_utf8(existing_contents).map_err(|err| Pep723Error::Utf8(err.utf8_error()))?} } else { indoc::formatdoc! {r#" diff --git a/crates/uv-trampoline-builder/src/lib.rs b/crates/uv-trampoline-builder/src/lib.rs index a5920fd2d054..2fb461fc8e5b 100644 --- a/crates/uv-trampoline-builder/src/lib.rs +++ b/crates/uv-trampoline-builder/src/lib.rs @@ -268,7 +268,7 @@ pub fn windows_script_launcher( launcher.extend_from_slice(&payload); launcher.extend_from_slice(python_path.as_bytes()); launcher.extend_from_slice( - &u32::try_from(python_path.as_bytes().len()) + &u32::try_from(python_path.len()) .expect("file path should be smaller than 4GB") .to_le_bytes(), ); @@ -300,7 +300,7 @@ pub fn windows_python_launcher( launcher.extend_from_slice(launcher_bin); launcher.extend_from_slice(python_path.as_bytes()); launcher.extend_from_slice( - &u32::try_from(python_path.as_bytes().len()) + &u32::try_from(python_path.len()) .expect("file path should be smaller than 4GB") .to_le_bytes(), ); @@ -377,7 +377,7 @@ mod test { fn get_script_launcher(shebang: &str, is_gui: bool) -> String { if is_gui { format!( - r##"{shebang} + r#"{shebang} # -*- coding: utf-8 -*- import re import sys @@ -394,11 +394,11 @@ def make_gui() -> None: if __name__ == "__main__": sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(make_gui()) -"## +"# ) } else { format!( - r##"{shebang} + r#"{shebang} # -*- coding: utf-8 -*- import re import sys @@ -412,7 +412,7 @@ def main_console() -> None: if __name__ == "__main__": sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(main_console()) -"## +"# ) } } diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index 1331c29f5bc1..abc028a0076b 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -21,7 +21,7 @@ pub(crate) struct LatestClient<'env> { pub(crate) requires_python: &'env RequiresPython, } -impl<'env> LatestClient<'env> { +impl LatestClient<'_> { /// Find the latest version of a package from an index. pub(crate) async fn find_latest( &self, diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index faa3302493d3..75f8008a6a88 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -1006,7 +1006,7 @@ fn pyproject_build_backend_prerequisites( if !build_file.try_exists()? { fs_err::write( build_file, - indoc::formatdoc! {r#" + indoc::formatdoc! {r" cmake_minimum_required(VERSION 3.15) project(${{SKBUILD_PROJECT_NAME}} LANGUAGES CXX) @@ -1015,7 +1015,7 @@ fn pyproject_build_backend_prerequisites( pybind11_add_module(_core MODULE src/main.cpp) install(TARGETS _core DESTINATION ${{SKBUILD_PROJECT_NAME}}) - "#}, + "}, )?; } } @@ -1052,21 +1052,21 @@ fn generate_package_scripts( // Python script for binary-based packaged apps or libs let binary_call_script = if is_lib { - indoc::formatdoc! {r#" + indoc::formatdoc! {r" from {module_name}._core import hello_from_bin def hello() -> str: return hello_from_bin() - "#} + "} } else { - indoc::formatdoc! {r#" + indoc::formatdoc! {r" from {module_name}._core import hello_from_bin def main() -> None: print(hello_from_bin()) - "#} + "} }; // .pyi file for binary script diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 4f3c1416efcb..74cfbe5ffc09 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1117,7 +1117,7 @@ impl<'lock> EnvironmentSpecification<'lock> { } /// Run dependency resolution for an interpreter, returning the [`ResolverOutput`]. -pub(crate) async fn resolve_environment<'a>( +pub(crate) async fn resolve_environment( spec: EnvironmentSpecification<'_>, interpreter: &Interpreter, settings: ResolverSettingsRef<'_>, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index dd327ede6c91..16cd4b8f01ed 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -1535,8 +1535,8 @@ impl RunCommand { } let metadata = target_path.metadata(); - let is_file = metadata.as_ref().map_or(false, std::fs::Metadata::is_file); - let is_dir = metadata.as_ref().map_or(false, std::fs::Metadata::is_dir); + let is_file = metadata.as_ref().is_ok_and(std::fs::Metadata::is_file); + let is_dir = metadata.as_ref().is_ok_and(std::fs::Metadata::is_dir); if target.eq_ignore_ascii_case("python") { Ok(Self::Python(args.to_vec())) @@ -1569,9 +1569,7 @@ impl RunCommand { fn is_python_zipapp(target: &Path) -> bool { if let Ok(file) = fs_err::File::open(target) { if let Ok(mut archive) = zip::ZipArchive::new(file) { - return archive - .by_name("__main__.py") - .map_or(false, |f| f.is_file()); + return archive.by_name("__main__.py").is_ok_and(|f| f.is_file()); } } false diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 7a25115f1440..7dd8ac935dfd 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -922,7 +922,7 @@ impl TestContext { /// For when we add pypy to the test suite. #[allow(clippy::unused_self)] - pub fn python_kind(&self) -> &str { + pub fn python_kind(&self) -> &'static str { "python" } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index c222d619f436..7adf4a635f0e 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -6859,10 +6859,10 @@ dependencies = [ // Write to a requirements file. let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str(&indoc::formatdoc! {r#" + requirements_in.write_str(&indoc::formatdoc! {r" -e {} -e {} - "#, + ", editable_dir1.path().display(), editable_dir2.path().display() })?; diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 93d410560a9c..14e7dee5008b 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -7077,10 +7077,10 @@ fn local_index_requirements_txt_absolute() -> Result<()> { "#, Url::from_directory_path(context.workspace_root.join("scripts/links/")).unwrap().as_str()})?; let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.write_str(&indoc::formatdoc! {r#" + requirements_txt.write_str(&indoc::formatdoc! {r" --index-url {} tqdm - "#, Url::from_directory_path(root).unwrap().as_str()})?; + ", Url::from_directory_path(root).unwrap().as_str()})?; uv_snapshot!(context.filters(), context.pip_install() .env_remove(EnvVars::UV_EXCLUDE_NEWER) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a718dc2fc84c..9db33c0e40d1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.83" +channel = "1.84" From 15e20501d1e12431341336821012872fa11a36b8 Mon Sep 17 00:00:00 2001 From: sergue1 Date: Sun, 12 Jan 2025 18:07:04 +0300 Subject: [PATCH 132/135] typo (#10538) fix typo in docs --- docs/guides/tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/tools.md b/docs/guides/tools.md index 8300d1d5db33..e1419f0e60f2 100644 --- a/docs/guides/tools.md +++ b/docs/guides/tools.md @@ -235,4 +235,4 @@ $ uv tool upgrade --all To learn more about managing tools with uv, see the [Tools concept](../concepts/tools.md) page and the [command reference](../reference/cli.md#uv-tool). -Or, read on to learn how to to [work on projects](./projects.md). +Or, read on to learn how to [work on projects](./projects.md). From 4ca5e048ccfa35477353429f9b68b1770510be44 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 12 Jan 2025 13:45:16 -0500 Subject: [PATCH 133/135] Upgrade packse to v0.3.44 (#10544) --- crates/uv/tests/it/common/mod.rs | 2 +- crates/uv/tests/it/lock_scenarios.rs | 2 +- crates/uv/tests/it/pip_compile_scenarios.rs | 2 +- crates/uv/tests/it/pip_install_scenarios.rs | 2 +- scripts/scenarios/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 7dd8ac935dfd..6a20c930e1a0 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -32,7 +32,7 @@ use uv_static::EnvVars; // Exclude any packages uploaded after this date. static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z"; -pub const PACKSE_VERSION: &str = "0.3.43"; +pub const PACKSE_VERSION: &str = "0.3.44"; /// Using a find links url allows using `--index-url` instead of `--extra-index-url` in tests /// to prevent dependency confusion attacks against our test suite. diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index 831d9bf2a9b2..a645f4f0f9fd 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::needless_raw_string_hashes)] diff --git a/crates/uv/tests/it/pip_compile_scenarios.rs b/crates/uv/tests/it/pip_compile_scenarios.rs index 93157dc32c76..a49792c8782d 100644 --- a/crates/uv/tests/it/pip_compile_scenarios.rs +++ b/crates/uv/tests/it/pip_compile_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index 1965a4f596f7..aa16c7941d85 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] diff --git a/scripts/scenarios/requirements.txt b/scripts/scenarios/requirements.txt index 0a6f0cbcd5fd..733e9ea67a49 100644 --- a/scripts/scenarios/requirements.txt +++ b/scripts/scenarios/requirements.txt @@ -10,7 +10,7 @@ msgspec==0.18.6 # via packse packaging==24.2 # via hatchling -packse==0.3.43 +packse==0.3.44 # via -r scripts/scenarios/requirements.in pathspec==0.12.1 # via hatchling From 1e48c1283709919af10174bb8a5094142b0da07f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 12 Jan 2025 15:23:18 -0500 Subject: [PATCH 134/135] Add a shared `uv-small-str` crate (#10545) ## Summary I want to use `SmallString` elsewhere. --- Cargo.lock | 12 +++++++++- Cargo.toml | 3 ++- crates/uv-normalize/Cargo.toml | 6 ++++- crates/uv-normalize/src/extra_name.rs | 3 ++- crates/uv-normalize/src/group_name.rs | 3 ++- crates/uv-normalize/src/lib.rs | 4 ++-- crates/uv-normalize/src/package_name.rs | 3 ++- crates/uv-small-str/Cargo.toml | 22 +++++++++++++++++++ .../src/lib.rs} | 22 ++++++++++++++----- 9 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 crates/uv-small-str/Cargo.toml rename crates/{uv-normalize/src/small_string.rs => uv-small-str/src/lib.rs} (86%) diff --git a/Cargo.lock b/Cargo.lock index c6696c60679b..2e3f383bd6bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5235,10 +5235,10 @@ dependencies = [ name = "uv-normalize" version = "0.0.1" dependencies = [ - "arcstr", "rkyv", "schemars", "serde", + "uv-small-str", ] [[package]] @@ -5622,6 +5622,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "uv-small-str" +version = "0.0.1" +dependencies = [ + "arcstr", + "rkyv", + "schemars", + "serde", +] + [[package]] name = "uv-state" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index e97d1ae6657c..ad3717a20177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,10 +59,11 @@ uv-resolver = { path = "crates/uv-resolver" } uv-scripts = { path = "crates/uv-scripts" } uv-settings = { path = "crates/uv-settings" } uv-shell = { path = "crates/uv-shell" } +uv-small-str = { path = "crates/uv-small-str" } uv-state = { path = "crates/uv-state" } uv-static = { path = "crates/uv-static" } -uv-trampoline-builder = { path = "crates/uv-trampoline-builder" } uv-tool = { path = "crates/uv-tool" } +uv-trampoline-builder = { path = "crates/uv-trampoline-builder" } uv-types = { path = "crates/uv-types" } uv-version = { path = "crates/uv-version" } uv-virtualenv = { path = "crates/uv-virtualenv" } diff --git a/crates/uv-normalize/Cargo.toml b/crates/uv-normalize/Cargo.toml index 8d9c1c23e512..d40faf3d72b9 100644 --- a/crates/uv-normalize/Cargo.toml +++ b/crates/uv-normalize/Cargo.toml @@ -11,7 +11,11 @@ doctest = false workspace = true [dependencies] -arcstr = { workspace = true } +uv-small-str = { workspace = true } + rkyv = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } + +[features] +schemars = ["dep:schemars", "uv-small-str/schemars"] diff --git a/crates/uv-normalize/src/extra_name.rs b/crates/uv-normalize/src/extra_name.rs index bc3c5f737bb8..11a6707ef127 100644 --- a/crates/uv-normalize/src/extra_name.rs +++ b/crates/uv-normalize/src/extra_name.rs @@ -4,7 +4,8 @@ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; -use crate::small_string::SmallString; +use uv_small_str::SmallString; + use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of an extra dependency. diff --git a/crates/uv-normalize/src/group_name.rs b/crates/uv-normalize/src/group_name.rs index a3ecb74c8c24..a11ed01bb73a 100644 --- a/crates/uv-normalize/src/group_name.rs +++ b/crates/uv-normalize/src/group_name.rs @@ -5,7 +5,8 @@ use std::sync::LazyLock; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::small_string::SmallString; +use uv_small_str::SmallString; + use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of a dependency group. diff --git a/crates/uv-normalize/src/lib.rs b/crates/uv-normalize/src/lib.rs index 17beee6b9995..06234db96a71 100644 --- a/crates/uv-normalize/src/lib.rs +++ b/crates/uv-normalize/src/lib.rs @@ -5,13 +5,13 @@ pub use dist_info_name::DistInfoName; pub use extra_name::ExtraName; pub use group_name::{GroupName, DEV_DEPENDENCIES}; pub use package_name::PackageName; -use small_string::SmallString; + +use uv_small_str::SmallString; mod dist_info_name; mod extra_name; mod group_name; mod package_name; -mod small_string; /// Validate and normalize an owned package or extra name. pub(crate) fn validate_and_normalize_owned(name: String) -> Result { diff --git a/crates/uv-normalize/src/package_name.rs b/crates/uv-normalize/src/package_name.rs index 742867e7f51b..59d7ea912e5f 100644 --- a/crates/uv-normalize/src/package_name.rs +++ b/crates/uv-normalize/src/package_name.rs @@ -4,7 +4,8 @@ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; -use crate::small_string::SmallString; +use uv_small_str::SmallString; + use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of a package. diff --git a/crates/uv-small-str/Cargo.toml b/crates/uv-small-str/Cargo.toml new file mode 100644 index 000000000000..c8e6cf18baf6 --- /dev/null +++ b/crates/uv-small-str/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "uv-small-str" +version = "0.0.1" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +arcstr = { workspace = true } +rkyv = { workspace = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true } diff --git a/crates/uv-normalize/src/small_string.rs b/crates/uv-small-str/src/lib.rs similarity index 86% rename from crates/uv-normalize/src/small_string.rs rename to crates/uv-small-str/src/lib.rs index ca6d3ea6dadb..20d66190ef7d 100644 --- a/crates/uv-normalize/src/small_string.rs +++ b/crates/uv-small-str/src/lib.rs @@ -1,11 +1,16 @@ use std::cmp::PartialEq; use std::ops::Deref; -/// An optimized small string type for short identifiers, like package names. -/// -/// Represented as an [`arcstr::ArcStr`] internally. -#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct SmallString(arcstr::ArcStr); +/// An optimized type for immutable identifiers. Represented as an [`arcstr::ArcStr`] internally. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SmallString(arcstr::ArcStr); + +impl From for SmallString { + #[inline] + fn from(s: arcstr::ArcStr) -> Self { + Self(s) + } +} impl From<&str> for SmallString { #[inline] @@ -28,6 +33,13 @@ impl AsRef for SmallString { } } +impl core::borrow::Borrow for SmallString { + #[inline] + fn borrow(&self) -> &str { + self + } +} + impl Deref for SmallString { type Target = str; From b38d3fec64c03f299cf203e4a7dd9ababdb3c377 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:33:01 -0500 Subject: [PATCH 135/135] Update Rust crate jiff to v0.1.22 (#10550) --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e3f383bd6bf..c3fe0acc4b3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1038,7 +1038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1903,16 +1903,16 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0ce60560149333a8e41ca7dc78799c47c5fd435e2bc18faf6a054382eec037" +checksum = "5c258647f65892e500c2478ef2c71ba008e7dc1774a8289345adbbb502a4def1" dependencies = [ "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2800,7 +2800,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3237,7 +3237,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3806,7 +3806,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]]