Show tag hints when failing to find a compatible wheel in pylock.toml (#13136)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / build binary | linux libc (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

## Summary

Closes #13135.
This commit is contained in:
Charlie Marsh 2025-04-27 12:56:50 -04:00 committed by GitHub
parent 78756de027
commit dc5b3762f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 294 additions and 184 deletions

View file

@ -7,7 +7,7 @@ use uv_configuration::Upgrade;
use uv_fs::CWD;
use uv_git::ResolvedRepositoryReference;
use uv_requirements_txt::RequirementsTxt;
use uv_resolver::{Lock, LockError, Preference, PreferenceError, PylockToml, PylockTomlError};
use uv_resolver::{Lock, LockError, Preference, PreferenceError, PylockToml, PylockTomlErrorKind};
#[derive(Debug, Default)]
pub struct LockedRequirements {
@ -105,7 +105,7 @@ pub fn read_lock_requirements(
pub async fn read_pylock_toml_requirements(
output_file: &Path,
upgrade: &Upgrade,
) -> Result<LockedRequirements, PylockTomlError> {
) -> Result<LockedRequirements, PylockTomlErrorKind> {
// As an optimization, skip iterating over the lockfile is we're upgrading all packages anyway.
if upgrade.is_all() {
return Ok(LockedRequirements::default());

View file

@ -5,8 +5,9 @@ pub use exclusions::Exclusions;
pub use flat_index::{FlatDistributions, FlatIndex};
pub use fork_strategy::ForkStrategy;
pub use lock::{
Installable, Lock, LockError, LockVersion, Package, PackageMap, PylockToml, PylockTomlError,
RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay, VERSION,
Installable, Lock, LockError, LockVersion, Package, PackageMap, PylockToml,
PylockTomlErrorKind, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay,
VERSION,
};
pub use manifest::Manifest;
pub use options::{Flexibility, Options, OptionsBuilder};

View file

@ -15,7 +15,7 @@ use uv_pypi_types::ConflictItem;
use crate::graph_ops::{marker_reachability, Reachable};
pub(crate) use crate::lock::export::pylock_toml::PylockTomlPackage;
pub use crate::lock::export::pylock_toml::{PylockToml, PylockTomlError};
pub use crate::lock::export::pylock_toml::{PylockToml, PylockTomlErrorKind};
pub use crate::lock::export::requirements_txt::RequirementsTxtExport;
use crate::universal_marker::resolve_conflicts;
use crate::{Installable, Package};

View file

@ -36,12 +36,12 @@ use uv_pypi_types::{HashDigests, Hashes, ParsedGitUrl, VcsKind};
use uv_small_str::SmallString;
use crate::lock::export::ExportableRequirements;
use crate::lock::{each_element_on_its_line_array, Source};
use crate::lock::{each_element_on_its_line_array, Source, WheelTagHint};
use crate::resolution::ResolutionGraphNode;
use crate::{Installable, LockError, RequiresPython, ResolverOutput};
#[derive(Debug, thiserror::Error)]
pub enum PylockTomlError {
pub enum PylockTomlErrorKind {
#[error("Package `{0}` includes both a registry (`packages.wheels`) and a directory source (`packages.directory`)")]
WheelWithDirectory(PackageName),
#[error("Package `{0}` includes both a registry (`packages.wheels`) and a VCS source (`packages.vcs`)")]
@ -114,6 +114,40 @@ pub enum PylockTomlError {
Deserialize(#[from] toml::de::Error),
}
#[derive(Debug)]
pub struct PylockTomlError {
kind: Box<PylockTomlErrorKind>,
hint: Option<WheelTagHint>,
}
impl std::error::Error for PylockTomlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.kind.source()
}
}
impl std::fmt::Display for PylockTomlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.kind)?;
if let Some(hint) = &self.hint {
write!(f, "\n\n{hint}")?;
}
Ok(())
}
}
impl<E> From<E> for PylockTomlError
where
PylockTomlErrorKind: From<E>,
{
fn from(err: E) -> Self {
PylockTomlError {
kind: Box::new(PylockTomlErrorKind::from(err)),
hint: None,
}
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct PylockToml {
@ -267,7 +301,7 @@ impl<'lock> PylockToml {
resolution: &ResolverOutput,
omit: &[PackageName],
install_path: &Path,
) -> Result<Self, PylockTomlError> {
) -> Result<Self, PylockTomlErrorKind> {
// The lock version is always `1.0` at time of writing.
let lock_version = Version::new([1, 0]);
@ -354,8 +388,11 @@ impl<'lock> PylockToml {
dist.wheels
.iter()
.map(|wheel| {
let url =
wheel.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
let url = wheel
.file
.url
.to_url()
.map_err(PylockTomlErrorKind::ToUrl)?;
Ok(PylockTomlWheel {
// Optional "when the last component of path/ url would be the same value".
name: if url
@ -372,18 +409,26 @@ impl<'lock> PylockToml {
.map(Timestamp::from_millisecond)
.transpose()?,
url: Some(
wheel.file.url.to_url().map_err(PylockTomlError::ToUrl)?,
wheel
.file
.url
.to_url()
.map_err(PylockTomlErrorKind::ToUrl)?,
),
path: None,
size: wheel.file.size,
hashes: Hashes::from(wheel.file.hashes.clone()),
})
})
.collect::<Result<Vec<_>, PylockTomlError>>()?,
.collect::<Result<Vec<_>, PylockTomlErrorKind>>()?,
);
if let Some(sdist) = dist.sdist.as_ref() {
let url = sdist.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
let url = sdist
.file
.url
.to_url()
.map_err(PylockTomlErrorKind::ToUrl)?;
package.sdist = Some(PylockTomlSdist {
// Optional "when the last component of path/ url would be the same value".
name: if url
@ -456,8 +501,11 @@ impl<'lock> PylockToml {
dist.wheels
.iter()
.map(|wheel| {
let url =
wheel.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
let url = wheel
.file
.url
.to_url()
.map_err(PylockTomlErrorKind::ToUrl)?;
Ok(PylockTomlWheel {
// Optional "when the last component of path/ url would be the same value".
name: if url
@ -474,17 +522,21 @@ impl<'lock> PylockToml {
.map(Timestamp::from_millisecond)
.transpose()?,
url: Some(
wheel.file.url.to_url().map_err(PylockTomlError::ToUrl)?,
wheel
.file
.url
.to_url()
.map_err(PylockTomlErrorKind::ToUrl)?,
),
path: None,
size: wheel.file.size,
hashes: Hashes::from(wheel.file.hashes.clone()),
})
})
.collect::<Result<Vec<_>, PylockTomlError>>()?,
.collect::<Result<Vec<_>, PylockTomlErrorKind>>()?,
);
let url = dist.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
let url = dist.file.url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
package.sdist = Some(PylockTomlSdist {
// Optional "when the last component of path/ url would be the same value".
name: if url
@ -536,7 +588,7 @@ impl<'lock> PylockToml {
dev: &DependencyGroupsWithDefaults,
annotate: bool,
install_options: &'lock InstallOptions,
) -> Result<Self, PylockTomlError> {
) -> Result<Self, PylockTomlErrorKind> {
// Extract the packages from the lock file.
let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(
target,
@ -591,8 +643,11 @@ impl<'lock> PylockToml {
wheels
.into_iter()
.map(|wheel| {
let url =
wheel.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
let url = wheel
.file
.url
.to_url()
.map_err(PylockTomlErrorKind::ToUrl)?;
Ok(PylockTomlWheel {
// Optional "when the last component of path/ url would be the same value".
name: if url
@ -614,7 +669,7 @@ impl<'lock> PylockToml {
hashes: Hashes::from(wheel.file.hashes),
})
})
.collect::<Result<Vec<_>, PylockTomlError>>()?,
.collect::<Result<Vec<_>, PylockTomlErrorKind>>()?,
)
}
Source::Path(..) => None,
@ -728,7 +783,11 @@ impl<'lock> PylockToml {
// Extract the `packages.sdist` field.
let sdist = match &sdist {
Some(SourceDist::Registry(sdist)) => {
let url = sdist.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
let url = sdist
.file
.url
.to_url()
.map_err(PylockTomlErrorKind::ToUrl)?;
Some(PylockTomlSdist {
// Optional "when the last component of path/ url would be the same value".
name: if url
@ -894,37 +953,43 @@ impl<'lock> PylockToml {
) {
// `packages.wheels` is mutually exclusive with `packages.directory`, `packages.vcs`, and `packages.archive`.
(true, _, true, _, _) => {
return Err(PylockTomlError::WheelWithDirectory(package.name.clone()));
return Err(
PylockTomlErrorKind::WheelWithDirectory(package.name.clone()).into(),
);
}
(true, _, _, true, _) => {
return Err(PylockTomlError::WheelWithVcs(package.name.clone()));
return Err(PylockTomlErrorKind::WheelWithVcs(package.name.clone()).into());
}
(true, _, _, _, true) => {
return Err(PylockTomlError::WheelWithArchive(package.name.clone()));
return Err(PylockTomlErrorKind::WheelWithArchive(package.name.clone()).into());
}
// `packages.sdist` is mutually exclusive with `packages.directory`, `packages.vcs`, and `packages.archive`.
(_, true, true, _, _) => {
return Err(PylockTomlError::SdistWithDirectory(package.name.clone()));
return Err(
PylockTomlErrorKind::SdistWithDirectory(package.name.clone()).into(),
);
}
(_, true, _, true, _) => {
return Err(PylockTomlError::SdistWithVcs(package.name.clone()));
return Err(PylockTomlErrorKind::SdistWithVcs(package.name.clone()).into());
}
(_, true, _, _, true) => {
return Err(PylockTomlError::SdistWithArchive(package.name.clone()));
return Err(PylockTomlErrorKind::SdistWithArchive(package.name.clone()).into());
}
// `packages.directory` is mutually exclusive with `packages.vcs`, and `packages.archive`.
(_, _, true, true, _) => {
return Err(PylockTomlError::DirectoryWithVcs(package.name.clone()));
return Err(PylockTomlErrorKind::DirectoryWithVcs(package.name.clone()).into());
}
(_, _, true, _, true) => {
return Err(PylockTomlError::DirectoryWithArchive(package.name.clone()));
return Err(
PylockTomlErrorKind::DirectoryWithArchive(package.name.clone()).into(),
);
}
// `packages.vcs` is mutually exclusive with `packages.archive`.
(_, _, _, true, true) => {
return Err(PylockTomlError::VcsWithArchive(package.name.clone()));
return Err(PylockTomlErrorKind::VcsWithArchive(package.name.clone()).into());
}
(false, false, false, false, false) => {
return Err(PylockTomlError::MissingSource(package.name.clone()));
return Err(PylockTomlErrorKind::MissingSource(package.name.clone()).into());
}
_ => {}
}
@ -1025,18 +1090,28 @@ impl<'lock> PylockToml {
}
} else {
return match (no_binary, no_build) {
(true, true) => Err(PylockTomlError::NoBinaryNoBuild(package.name.clone())),
(true, true) => {
Err(PylockTomlErrorKind::NoBinaryNoBuild(package.name.clone()).into())
}
(true, false) if is_wheel => {
Err(PylockTomlError::NoBinaryWheelOnly(package.name.clone()))
Err(PylockTomlErrorKind::NoBinaryWheelOnly(package.name.clone()).into())
}
(true, false) => Err(PylockTomlError::NoBinary(package.name.clone())),
(false, true) => Err(PylockTomlError::NoBuild(package.name.clone())),
(false, false) if is_wheel => {
Err(PylockTomlError::IncompatibleWheelOnly(package.name.clone()))
(true, false) => {
Err(PylockTomlErrorKind::NoBinary(package.name.clone()).into())
}
(false, false) => Err(PylockTomlError::NeitherSourceDistNorWheel(
package.name.clone(),
)),
(false, true) => Err(PylockTomlErrorKind::NoBuild(package.name.clone()).into()),
(false, false) if is_wheel => Err(PylockTomlError {
kind: Box::new(PylockTomlErrorKind::IncompatibleWheelOnly(
package.name.clone(),
)),
hint: package.tag_hint(tags),
}),
(false, false) => Err(PylockTomlError {
kind: Box::new(PylockTomlErrorKind::NeitherSourceDistNorWheel(
package.name.clone(),
)),
hint: package.tag_hint(tags),
}),
};
};
@ -1163,6 +1238,18 @@ impl PylockTomlPackage {
best.map(|(_, i)| i)
}
/// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities.
fn tag_hint(&self, tags: &Tags) -> Option<WheelTagHint> {
let filenames = self
.wheels
.iter()
.flatten()
.filter_map(|wheel| wheel.filename(&self.name).ok())
.collect::<Vec<_>>();
let filenames = filenames.iter().map(Cow::as_ref).collect::<Vec<_>>();
WheelTagHint::from_wheels(&self.name, self.version.as_ref(), &filenames, tags)
}
/// Returns the [`ResolvedRepositoryReference`] for the package, if it is a Git source.
pub fn as_git_ref(&self) -> Option<ResolvedRepositoryReference> {
let vcs = self.vcs.as_ref()?;
@ -1180,12 +1267,12 @@ impl PylockTomlPackage {
impl PylockTomlWheel {
/// Return the [`WheelFilename`] for this wheel.
fn filename(&self, name: &PackageName) -> Result<Cow<'_, WheelFilename>, PylockTomlError> {
fn filename(&self, name: &PackageName) -> Result<Cow<WheelFilename>, PylockTomlErrorKind> {
if let Some(name) = self.name.as_ref() {
Ok(Cow::Borrowed(name))
} else if let Some(path) = self.path.as_ref() {
let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
return Err(PylockTomlError::PathMissingFilename(Box::<Path>::from(
return Err(PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(
path.clone(),
)));
};
@ -1193,12 +1280,12 @@ impl PylockTomlWheel {
Ok(filename)
} else if let Some(url) = self.url.as_ref() {
let Some(filename) = url.filename().ok() else {
return Err(PylockTomlError::UrlMissingFilename(url.clone()));
return Err(PylockTomlErrorKind::UrlMissingFilename(url.clone()));
};
let filename = WheelFilename::from_str(&filename).map(Cow::Owned)?;
Ok(filename)
} else {
Err(PylockTomlError::WheelMissingPathUrl(name.clone()))
Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone()))
}
}
@ -1208,17 +1295,17 @@ impl PylockTomlWheel {
install_path: &Path,
name: &PackageName,
index: Option<&Url>,
) -> Result<RegistryBuiltWheel, PylockTomlError> {
) -> Result<RegistryBuiltWheel, PylockTomlErrorKind> {
let filename = self.filename(name)?.into_owned();
let file_url = if let Some(url) = self.url.as_ref() {
UrlString::from(url)
} else if let Some(path) = self.path.as_ref() {
let path = install_path.join(path);
let url = Url::from_file_path(path).map_err(|()| PylockTomlError::PathToUrl)?;
let url = Url::from_file_path(path).map_err(|()| PylockTomlErrorKind::PathToUrl)?;
UrlString::from(url)
} else {
return Err(PylockTomlError::WheelMissingPathUrl(name.clone()));
return Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone()));
};
let index = if let Some(index) = index {
@ -1228,7 +1315,7 @@ impl PylockTomlWheel {
// URL (less the filename) as the index. This isn't correct, but it's the best we can
// do. In practice, the only effect here should be that we cache the wheel under a hash
// of this URL (since we cache under the hash of the index).
let mut index = file_url.to_url().map_err(PylockTomlError::ToUrl)?;
let mut index = file_url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
index.path_segments_mut().unwrap().pop();
IndexUrl::from(VerbatimUrl::from_url(index))
};
@ -1258,7 +1345,7 @@ impl PylockTomlDirectory {
&self,
install_path: &Path,
name: &PackageName,
) -> Result<DirectorySourceDist, PylockTomlError> {
) -> Result<DirectorySourceDist, PylockTomlErrorKind> {
let path = if let Some(subdirectory) = self.subdirectory.as_ref() {
install_path.join(&self.path).join(subdirectory)
} else {
@ -1266,7 +1353,7 @@ impl PylockTomlDirectory {
};
let path = uv_fs::normalize_path_buf(path);
let url =
VerbatimUrl::from_normalized_path(&path).map_err(|_| PylockTomlError::PathToUrl)?;
VerbatimUrl::from_normalized_path(&path).map_err(|_| PylockTomlErrorKind::PathToUrl)?;
Ok(DirectorySourceDist {
name: name.clone(),
install_path: path.into_boxed_path(),
@ -1283,7 +1370,7 @@ impl PylockTomlVcs {
&self,
install_path: &Path,
name: &PackageName,
) -> Result<GitSourceDist, PylockTomlError> {
) -> Result<GitSourceDist, PylockTomlErrorKind> {
let subdirectory = self.subdirectory.clone().map(Box::<Path>::from);
// Reconstruct the `GitUrl` from the individual fields.
@ -1292,9 +1379,9 @@ impl PylockTomlVcs {
url.clone()
} else if let Some(path) = self.path.as_ref() {
Url::from_directory_path(install_path.join(path))
.map_err(|()| PylockTomlError::PathToUrl)?
.map_err(|()| PylockTomlErrorKind::PathToUrl)?
} else {
return Err(PylockTomlError::VcsMissingPathUrl(name.clone()));
return Err(PylockTomlErrorKind::VcsMissingPathUrl(name.clone()));
};
url.set_fragment(None);
url.set_query(None);
@ -1326,23 +1413,23 @@ impl PylockTomlVcs {
impl PylockTomlSdist {
/// Return the filename for this sdist.
fn filename(&self, name: &PackageName) -> Result<Cow<'_, SmallString>, PylockTomlError> {
fn filename(&self, name: &PackageName) -> Result<Cow<'_, SmallString>, PylockTomlErrorKind> {
if let Some(name) = self.name.as_ref() {
Ok(Cow::Borrowed(name))
} else if let Some(path) = self.path.as_ref() {
let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
return Err(PylockTomlError::PathMissingFilename(Box::<Path>::from(
return Err(PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(
path.clone(),
)));
};
Ok(Cow::Owned(SmallString::from(filename)))
} else if let Some(url) = self.url.as_ref() {
let Some(filename) = url.filename().ok() else {
return Err(PylockTomlError::UrlMissingFilename(url.clone()));
return Err(PylockTomlErrorKind::UrlMissingFilename(url.clone()));
};
Ok(Cow::Owned(SmallString::from(filename)))
} else {
Err(PylockTomlError::SdistMissingPathUrl(name.clone()))
Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone()))
}
}
@ -1353,7 +1440,7 @@ impl PylockTomlSdist {
name: &PackageName,
version: Option<&Version>,
index: Option<&Url>,
) -> Result<RegistrySourceDist, PylockTomlError> {
) -> Result<RegistrySourceDist, PylockTomlErrorKind> {
let filename = self.filename(name)?.into_owned();
let ext = SourceDistExtension::from_path(filename.as_ref())?;
@ -1368,10 +1455,10 @@ impl PylockTomlSdist {
UrlString::from(url)
} else if let Some(path) = self.path.as_ref() {
let path = install_path.join(path);
let url = Url::from_file_path(path).map_err(|()| PylockTomlError::PathToUrl)?;
let url = Url::from_file_path(path).map_err(|()| PylockTomlErrorKind::PathToUrl)?;
UrlString::from(url)
} else {
return Err(PylockTomlError::SdistMissingPathUrl(name.clone()));
return Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone()));
};
let index = if let Some(index) = index {
@ -1381,7 +1468,7 @@ impl PylockTomlSdist {
// URL (less the filename) as the index. This isn't correct, but it's the best we can
// do. In practice, the only effect here should be that we cache the sdist under a hash
// of this URL (since we cache under the hash of the index).
let mut index = file_url.to_url().map_err(PylockTomlError::ToUrl)?;
let mut index = file_url.to_url().map_err(PylockTomlErrorKind::ToUrl)?;
index.path_segments_mut().unwrap().pop();
IndexUrl::from(VerbatimUrl::from_url(index))
};
@ -1414,11 +1501,11 @@ impl PylockTomlArchive {
install_path: &Path,
name: &PackageName,
version: Option<&Version>,
) -> Result<Dist, PylockTomlError> {
) -> Result<Dist, PylockTomlErrorKind> {
if let Some(url) = self.url.as_ref() {
let filename = url
.filename()
.map_err(|_| PylockTomlError::UrlMissingFilename(url.clone()))?;
.map_err(|_| PylockTomlErrorKind::UrlMissingFilename(url.clone()))?;
let ext = DistExtension::from_path(filename.as_ref())?;
match ext {
@ -1446,7 +1533,7 @@ impl PylockTomlArchive {
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| {
PylockTomlError::PathMissingFilename(Box::<Path>::from(path.clone()))
PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(path.clone()))
})?;
let ext = DistExtension::from_path(filename)?;
@ -1455,7 +1542,7 @@ impl PylockTomlArchive {
let filename = WheelFilename::from_str(filename)?;
let install_path = install_path.join(path);
let url = VerbatimUrl::from_absolute_path(&install_path)
.map_err(|_| PylockTomlError::PathToUrl)?;
.map_err(|_| PylockTomlErrorKind::PathToUrl)?;
Ok(Dist::Built(BuiltDist::Path(PathBuiltDist {
filename,
install_path: install_path.into_boxed_path(),
@ -1465,7 +1552,7 @@ impl PylockTomlArchive {
DistExtension::Source(ext) => {
let install_path = install_path.join(path);
let url = VerbatimUrl::from_absolute_path(&install_path)
.map_err(|_| PylockTomlError::PathToUrl)?;
.map_err(|_| PylockTomlErrorKind::PathToUrl)?;
Ok(Dist::Source(SourceDist::Path(PathSourceDist {
name: name.clone(),
version: version.cloned(),
@ -1476,16 +1563,16 @@ impl PylockTomlArchive {
}
}
} else {
return Err(PylockTomlError::ArchiveMissingPathUrl(name.clone()));
return Err(PylockTomlErrorKind::ArchiveMissingPathUrl(name.clone()));
}
}
/// Returns `true` if the [`PylockTomlArchive`] is a wheel.
fn is_wheel(&self, name: &PackageName) -> Result<bool, PylockTomlError> {
fn is_wheel(&self, name: &PackageName) -> Result<bool, PylockTomlErrorKind> {
if let Some(url) = self.url.as_ref() {
let filename = url
.filename()
.map_err(|_| PylockTomlError::UrlMissingFilename(url.clone()))?;
.map_err(|_| PylockTomlErrorKind::UrlMissingFilename(url.clone()))?;
let ext = DistExtension::from_path(filename.as_ref())?;
Ok(matches!(ext, DistExtension::Wheel))
@ -1495,13 +1582,13 @@ impl PylockTomlArchive {
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| {
PylockTomlError::PathMissingFilename(Box::<Path>::from(path.clone()))
PylockTomlErrorKind::PathMissingFilename(Box::<Path>::from(path.clone()))
})?;
let ext = DistExtension::from_path(filename)?;
Ok(matches!(ext, DistExtension::Wheel))
} else {
return Err(PylockTomlError::ArchiveMissingPathUrl(name.clone()));
return Err(PylockTomlErrorKind::ArchiveMissingPathUrl(name.clone()));
}
}
}

View file

@ -52,7 +52,7 @@ use uv_workspace::WorkspaceMember;
use crate::fork_strategy::ForkStrategy;
pub(crate) use crate::lock::export::PylockTomlPackage;
pub use crate::lock::export::RequirementsTxtExport;
pub use crate::lock::export::{PylockToml, PylockTomlError};
pub use crate::lock::export::{PylockToml, PylockTomlErrorKind};
pub use crate::lock::installable::Installable;
pub use crate::lock::map::PackageMap;
pub use crate::lock::tree::TreeDisplay;
@ -2305,78 +2305,19 @@ impl Package {
}
}
/// Generate a [`LockErrorHint`] based on wheel-tag incompatibilities.
fn tag_hint(&self, tag_policy: TagPolicy<'_>) -> Option<LockErrorHint> {
let incompatibility = self
/// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities.
fn tag_hint(&self, tag_policy: TagPolicy<'_>) -> Option<WheelTagHint> {
let filenames = self
.wheels
.iter()
.map(|wheel| {
tag_policy.tags().compatibility(
wheel.filename.python_tags(),
wheel.filename.abi_tags(),
wheel.filename.platform_tags(),
)
})
.max()?;
match incompatibility {
TagCompatibility::Incompatible(IncompatibleTag::Python) => {
let best = tag_policy.tags().python_tag();
let tags = self.python_tags().collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(LockErrorHint::LanguageTags {
package: self.id.name.clone(),
version: self.id.version.clone(),
tags,
best,
})
}
}
TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
let best = tag_policy.tags().abi_tag();
let tags = self
.abi_tags()
// Ignore `none`, which is universally compatible.
//
// As an example, `none` can appear here if we're solving for Python 3.13, and
// the distribution includes a wheel for `cp312-none-macosx_11_0_arm64`.
//
// In that case, the wheel isn't compatible, but when solving for Python 3.13,
// the `cp312` Python tag _can_ be compatible (e.g., for `cp312-abi3-macosx_11_0_arm64.whl`),
// so this is considered an ABI incompatibility rather than Python incompatibility.
.filter(|tag| *tag != AbiTag::None)
.collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(LockErrorHint::AbiTags {
package: self.id.name.clone(),
version: self.id.version.clone(),
tags,
best,
})
}
}
TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
let best = tag_policy.tags().platform_tag().cloned();
let tags = self
.platform_tags(tag_policy.tags())
.cloned()
.collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(LockErrorHint::PlatformTags {
package: self.id.name.clone(),
version: self.id.version.clone(),
tags,
best,
})
}
}
_ => None,
}
.map(|wheel| &wheel.filename)
.collect::<Vec<_>>();
WheelTagHint::from_wheels(
&self.id.name,
self.id.version.as_ref(),
&filenames,
tag_policy.tags(),
)
}
/// Convert the source of this [`Package`] to a [`SourceDist`] that can be used in installation.
@ -2729,43 +2670,6 @@ impl Package {
Ok(table)
}
/// Returns an iterator over the compatible Python tags of the available wheels.
fn python_tags(&self) -> impl Iterator<Item = LanguageTag> + '_ {
self.wheels
.iter()
.flat_map(|wheel| wheel.filename.python_tags())
.copied()
}
/// Returns an iterator over the compatible Python tags of the available wheels.
fn abi_tags(&self) -> impl Iterator<Item = AbiTag> + '_ {
self.wheels
.iter()
.flat_map(|wheel| wheel.filename.abi_tags())
.copied()
}
/// Returns the set of platform tags for the distribution that are ABI-compatible with the given
/// tags.
pub fn platform_tags<'a>(
&'a self,
tags: &'a Tags,
) -> impl Iterator<Item = &'a PlatformTag> + 'a {
self.wheels.iter().flat_map(move |wheel| {
if wheel.filename.python_tags().iter().any(|wheel_py| {
wheel
.filename
.abi_tags()
.iter()
.any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
}) {
wheel.filename.platform_tags().iter()
} else {
[].iter()
}
})
}
fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
@ -4782,7 +4686,7 @@ fn normalize_requirement(
#[derive(Debug)]
pub struct LockError {
kind: Box<LockErrorKind>,
hint: Option<LockErrorHint>,
hint: Option<WheelTagHint>,
}
impl std::error::Error for LockError {
@ -4822,7 +4726,7 @@ where
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
enum LockErrorHint {
enum WheelTagHint {
/// None of the available wheels for a package have a compatible Python language tag (e.g.,
/// `cp310` in `cp310-abi3-manylinux_2_17_x86_64.whl`).
LanguageTags {
@ -4849,7 +4753,123 @@ enum LockErrorHint {
},
}
impl std::fmt::Display for LockErrorHint {
impl WheelTagHint {
/// Generate a [`WheelTagHint`] from the given (incompatible) wheels.
fn from_wheels(
name: &PackageName,
version: Option<&Version>,
filenames: &[&WheelFilename],
tags: &Tags,
) -> Option<WheelTagHint> {
let incompatibility = filenames
.iter()
.map(|filename| {
tags.compatibility(
filename.python_tags(),
filename.abi_tags(),
filename.platform_tags(),
)
})
.max()?;
match incompatibility {
TagCompatibility::Incompatible(IncompatibleTag::Python) => {
let best = tags.python_tag();
let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(WheelTagHint::LanguageTags {
package: name.clone(),
version: version.cloned(),
tags,
best,
})
}
}
TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
let best = tags.abi_tag();
let tags = Self::abi_tags(filenames.iter().copied())
// Ignore `none`, which is universally compatible.
//
// As an example, `none` can appear here if we're solving for Python 3.13, and
// the distribution includes a wheel for `cp312-none-macosx_11_0_arm64`.
//
// In that case, the wheel isn't compatible, but when solving for Python 3.13,
// the `cp312` Python tag _can_ be compatible (e.g., for `cp312-abi3-macosx_11_0_arm64.whl`),
// so this is considered an ABI incompatibility rather than Python incompatibility.
.filter(|tag| *tag != AbiTag::None)
.collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(WheelTagHint::AbiTags {
package: name.clone(),
version: version.cloned(),
tags,
best,
})
}
}
TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
let best = tags.platform_tag().cloned();
let tags = Self::platform_tags(filenames.iter().copied(), tags)
.cloned()
.collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(WheelTagHint::PlatformTags {
package: name.clone(),
version: version.cloned(),
tags,
best,
})
}
}
_ => None,
}
}
/// Returns an iterator over the compatible Python tags of the available wheels.
fn python_tags<'a>(
filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
) -> impl Iterator<Item = LanguageTag> + 'a {
filenames
.flat_map(uv_distribution_filename::WheelFilename::python_tags)
.copied()
}
/// Returns an iterator over the compatible Python tags of the available wheels.
fn abi_tags<'a>(
filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
) -> impl Iterator<Item = AbiTag> + 'a {
filenames
.flat_map(uv_distribution_filename::WheelFilename::abi_tags)
.copied()
}
/// Returns the set of platform tags for the distribution that are ABI-compatible with the given
/// tags.
fn platform_tags<'a>(
filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
tags: &'a Tags,
) -> impl Iterator<Item = &'a PlatformTag> + 'a {
filenames.flat_map(move |filename| {
if filename.python_tags().iter().any(|wheel_py| {
filename
.abi_tags()
.iter()
.any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
}) {
filename.platform_tags().iter()
} else {
[].iter()
}
})
}
}
impl std::fmt::Display for WheelTagHint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LanguageTags {

View file

@ -5892,6 +5892,8 @@ fn pep_751_wheel_only() -> Result<()> {
----- stderr -----
error: Package `torch` can't be installed because it doesn't have a source distribution or wheel for the current platform
hint: You're using CPython 3.8 (`cp38`), but `torch` (v2.2.1) only has wheels with the following Python implementation tag: `cp312`
"
);