mirror of https://github.com/astral-sh/uv
Use custom incompatibility
This commit is contained in:
parent
d962db6fba
commit
24ed6d02f0
|
|
@ -1149,14 +1149,6 @@ impl SimpleMetadata {
|
||||||
warn!("Skipping file for {package_name}: {}", file.filename);
|
warn!("Skipping file for {package_name}: {}", file.filename);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if filename.name() != package_name {
|
|
||||||
warn!(
|
|
||||||
"Skipping file with mismatched package name: `{}` vs. `{}`",
|
|
||||||
filename.name(),
|
|
||||||
package_name
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let file = match File::try_from(file, &base) {
|
let file = match File::try_from(file, &base) {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use crate::resolver::{MetadataUnavailable, VersionFork};
|
use crate::resolver::{MetadataUnavailable, VersionFork};
|
||||||
use uv_distribution_types::IncompatibleDist;
|
use uv_distribution_types::IncompatibleDist;
|
||||||
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{Version, VersionSpecifiers};
|
use uv_pep440::{Version, VersionSpecifiers};
|
||||||
use uv_platform_tags::{AbiTag, Tags};
|
use uv_platform_tags::{AbiTag, Tags};
|
||||||
|
|
||||||
|
|
@ -38,6 +39,8 @@ pub enum UnavailableVersion {
|
||||||
InconsistentMetadata,
|
InconsistentMetadata,
|
||||||
/// The wheel has an invalid structure.
|
/// The wheel has an invalid structure.
|
||||||
InvalidStructure,
|
InvalidStructure,
|
||||||
|
/// A package with the wrong name is on an index page.
|
||||||
|
PackageNameMismatch { dist_name: PackageName },
|
||||||
/// The wheel metadata was not found in the cache and the network is not available.
|
/// The wheel metadata was not found in the cache and the network is not available.
|
||||||
Offline,
|
Offline,
|
||||||
/// The source distribution has a `requires-python` requirement that is not met by the installed
|
/// The source distribution has a `requires-python` requirement that is not met by the installed
|
||||||
|
|
@ -52,6 +55,11 @@ impl UnavailableVersion {
|
||||||
Self::InvalidMetadata => "invalid metadata".into(),
|
Self::InvalidMetadata => "invalid metadata".into(),
|
||||||
Self::InconsistentMetadata => "inconsistent metadata".into(),
|
Self::InconsistentMetadata => "inconsistent metadata".into(),
|
||||||
Self::InvalidStructure => "an invalid package format".into(),
|
Self::InvalidStructure => "an invalid package format".into(),
|
||||||
|
Self::PackageNameMismatch {
|
||||||
|
dist_name: wrong_name,
|
||||||
|
} => {
|
||||||
|
format!("the wrong package name in the index page (`{wrong_name}`)")
|
||||||
|
}
|
||||||
Self::Offline => "to be downloaded from a registry".into(),
|
Self::Offline => "to be downloaded from a registry".into(),
|
||||||
Self::RequiresPython(requires_python) => {
|
Self::RequiresPython(requires_python) => {
|
||||||
format!("Python {requires_python}")
|
format!("Python {requires_python}")
|
||||||
|
|
@ -65,6 +73,7 @@ impl UnavailableVersion {
|
||||||
Self::InvalidMetadata => format!("has {self}"),
|
Self::InvalidMetadata => format!("has {self}"),
|
||||||
Self::InconsistentMetadata => format!("has {self}"),
|
Self::InconsistentMetadata => format!("has {self}"),
|
||||||
Self::InvalidStructure => format!("has {self}"),
|
Self::InvalidStructure => format!("has {self}"),
|
||||||
|
Self::PackageNameMismatch { .. } => format!("has {self}"),
|
||||||
Self::Offline => format!("needs {self}"),
|
Self::Offline => format!("needs {self}"),
|
||||||
Self::RequiresPython(..) => format!("requires {self}"),
|
Self::RequiresPython(..) => format!("requires {self}"),
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +85,7 @@ impl UnavailableVersion {
|
||||||
Self::InvalidMetadata => format!("have {self}"),
|
Self::InvalidMetadata => format!("have {self}"),
|
||||||
Self::InconsistentMetadata => format!("have {self}"),
|
Self::InconsistentMetadata => format!("have {self}"),
|
||||||
Self::InvalidStructure => format!("have {self}"),
|
Self::InvalidStructure => format!("have {self}"),
|
||||||
|
Self::PackageNameMismatch { .. } => format!("have {self}"),
|
||||||
Self::Offline => format!("need {self}"),
|
Self::Offline => format!("need {self}"),
|
||||||
Self::RequiresPython(..) => format!("require {self}"),
|
Self::RequiresPython(..) => format!("require {self}"),
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +103,7 @@ impl UnavailableVersion {
|
||||||
Self::InvalidMetadata => None,
|
Self::InvalidMetadata => None,
|
||||||
Self::InconsistentMetadata => None,
|
Self::InconsistentMetadata => None,
|
||||||
Self::InvalidStructure => None,
|
Self::InvalidStructure => None,
|
||||||
|
Self::PackageNameMismatch { .. } => None,
|
||||||
Self::Offline => None,
|
Self::Offline => None,
|
||||||
Self::RequiresPython(..) => None,
|
Self::RequiresPython(..) => None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1370,7 +1370,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
candidate.choice_kind(),
|
candidate.choice_kind(),
|
||||||
filename,
|
filename,
|
||||||
);
|
);
|
||||||
self.visit_candidate(&candidate, dist, package, name, pins, request_sink)?;
|
if let Some(incompatibility) =
|
||||||
|
self.visit_candidate(&candidate, dist, package, name, pins, request_sink)?
|
||||||
|
{
|
||||||
|
return Ok(Some(incompatibility));
|
||||||
|
}
|
||||||
|
|
||||||
let version = candidate.version().clone();
|
let version = candidate.version().clone();
|
||||||
Ok(Some(ResolverVersion::Unforked(version)))
|
Ok(Some(ResolverVersion::Unforked(version)))
|
||||||
|
|
@ -1531,14 +1535,17 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
base_candidate.choice_kind(),
|
base_candidate.choice_kind(),
|
||||||
filename,
|
filename,
|
||||||
);
|
);
|
||||||
self.visit_candidate(
|
|
||||||
|
if let Some(incompatibility) = self.visit_candidate(
|
||||||
&base_candidate,
|
&base_candidate,
|
||||||
base_dist,
|
base_dist,
|
||||||
package,
|
package,
|
||||||
name,
|
name,
|
||||||
pins,
|
pins,
|
||||||
request_sink,
|
request_sink,
|
||||||
)?;
|
)? {
|
||||||
|
return Ok(Some(incompatibility));
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(Some(ResolverVersion::Unforked(
|
return Ok(Some(ResolverVersion::Unforked(
|
||||||
base_candidate.version().clone(),
|
base_candidate.version().clone(),
|
||||||
|
|
@ -1585,15 +1592,21 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
);
|
);
|
||||||
self.visit_candidate(candidate, dist, package, name, pins, request_sink)?;
|
if let Some(incompatibility) =
|
||||||
self.visit_candidate(
|
self.visit_candidate(candidate, dist, package, name, pins, request_sink)?
|
||||||
|
{
|
||||||
|
return Ok(Some(incompatibility));
|
||||||
|
}
|
||||||
|
if let Some(incompatibility) = self.visit_candidate(
|
||||||
&base_candidate,
|
&base_candidate,
|
||||||
base_dist,
|
base_dist,
|
||||||
package,
|
package,
|
||||||
name,
|
name,
|
||||||
pins,
|
pins,
|
||||||
request_sink,
|
request_sink,
|
||||||
)?;
|
)? {
|
||||||
|
return Ok(Some(incompatibility));
|
||||||
|
}
|
||||||
|
|
||||||
let forks = vec![
|
let forks = vec![
|
||||||
VersionFork {
|
VersionFork {
|
||||||
|
|
@ -1611,6 +1624,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visit a selected candidate.
|
/// Visit a selected candidate.
|
||||||
|
///
|
||||||
|
/// Returns an unavailability if the version can't be used.
|
||||||
fn visit_candidate(
|
fn visit_candidate(
|
||||||
&self,
|
&self,
|
||||||
candidate: &Candidate,
|
candidate: &Candidate,
|
||||||
|
|
@ -1619,7 +1634,18 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
name: &PackageName,
|
name: &PackageName,
|
||||||
pins: &mut FilePins,
|
pins: &mut FilePins,
|
||||||
request_sink: &Sender<Request>,
|
request_sink: &Sender<Request>,
|
||||||
) -> Result<(), ResolveError> {
|
) -> Result<Option<ResolverVersion>, ResolveError> {
|
||||||
|
// If there is a package with a different name then the index page on the index, return
|
||||||
|
// an unavailability.
|
||||||
|
if dist.name() != (name) {
|
||||||
|
return Ok(Some(ResolverVersion::Unavailable(
|
||||||
|
candidate.version().clone(),
|
||||||
|
UnavailableVersion::PackageNameMismatch {
|
||||||
|
dist_name: dist.name().clone(),
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
// We want to return a package pinned to a specific version; but we _also_ want to
|
// We want to return a package pinned to a specific version; but we _also_ want to
|
||||||
// store the exact file that we selected to satisfy that version.
|
// store the exact file that we selected to satisfy that version.
|
||||||
pins.insert(candidate, dist);
|
pins.insert(candidate, dist);
|
||||||
|
|
@ -1649,7 +1675,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the distribution is incompatible with the Python requirement, and if so, return
|
/// Check if the distribution is incompatible with the Python requirement, and if so, return
|
||||||
|
|
@ -2500,6 +2526,18 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is a package with a different name then the index page on the index,
|
||||||
|
// `chose_version` later returns an unavailability. Ignoring it here allows the
|
||||||
|
// resolver to continue instead of erroring from a prefetch.
|
||||||
|
if dist.name() != &package_name {
|
||||||
|
warn!(
|
||||||
|
"Expected packages name {} and distribution name {} do not match, skipping prefetch",
|
||||||
|
dist.name(),
|
||||||
|
package_name
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
// Emit a request to fetch the metadata for this version.
|
// Emit a request to fetch the metadata for this version.
|
||||||
if self.index.distributions().register(candidate.version_id()) {
|
if self.index.distributions().register(candidate.version_id()) {
|
||||||
let dist = dist.for_resolution().to_owned();
|
let dist = dist.for_resolution().to_owned();
|
||||||
|
|
|
||||||
|
|
@ -17756,3 +17756,84 @@ fn omit_python_patch_universal() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for cases where the index page for a package contains distributions for a different
|
||||||
|
/// package.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn package_name_on_index_package_mismatch() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
|
||||||
|
// An index for anyio, with tqdm and anyio distributions.
|
||||||
|
let networkx_page = r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Links for anyio</h1>
|
||||||
|
<a href="https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl#sha256=26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2" data-requires-python=">=3.7" data-dist-info-metadata="sha256=688a1632df525a198fec52dcfefb1246d31eacee9c035aacac2d7eaf8d8ff669" data-core-metadata="sha256=688a1632df525a198fec52dcfefb1246d31eacee9c035aacac2d7eaf8d8ff669">tqdm-4.67.1-py3-none-any.whl</a><br />
|
||||||
|
<a href="https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz#sha256=f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2" data-requires-python=">=3.7" >tqdm-4.67.1.tar.gz</a><br />
|
||||||
|
<a href="https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl#sha256=60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1" data-requires-python=">=3.9" data-dist-info-metadata="sha256=d400ffeb480f82ac562ac3b9e055136ca0d01f29e2e63ff9ebf5d08a7289f4b4" data-core-metadata="sha256=d400ffeb480f82ac562ac3b9e055136ca0d01f29e2e63ff9ebf5d08a7289f4b4">anyio-4.10.0-py3-none-any.whl</a><br />
|
||||||
|
<a href="https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz#sha256=3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6" data-requires-python=">=3.9" >anyio-4.10.0.tar.gz</a><br />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/anyio/"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_raw(networkx_page, "text/html"))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Skip over the tqdm version, resolve to the older anyio version.
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("anyio")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context
|
||||||
|
.pip_compile()
|
||||||
|
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||||||
|
.arg("--extra-index-url")
|
||||||
|
.arg(server.uri())
|
||||||
|
.arg("requirements.in"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] requirements.in
|
||||||
|
anyio==4.10.0
|
||||||
|
# via -r requirements.in
|
||||||
|
idna==3.10
|
||||||
|
# via anyio
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
typing-extensions==4.15.0
|
||||||
|
# via anyio
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
");
|
||||||
|
|
||||||
|
// Error with our custom incompatibility when there are only bogus packages in the version
|
||||||
|
// range.
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("anyio>4.10")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context
|
||||||
|
.pip_compile()
|
||||||
|
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||||||
|
.arg("--extra-index-url")
|
||||||
|
.arg(server.uri())
|
||||||
|
.arg("requirements.in"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
× No solution found when resolving dependencies:
|
||||||
|
╰─▶ Because only the following versions of anyio are available:
|
||||||
|
anyio<4.10
|
||||||
|
anyio==4.67.1
|
||||||
|
and anyio==4.67.1 has the wrong package name in the index page (`tqdm`), we can conclude that anyio>4.10 cannot be used.
|
||||||
|
And because you require anyio>4.10, we can conclude that your requirements are unsatisfiable.
|
||||||
|
");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11688,11 +11688,14 @@ async fn bogus_redirect() -> Result<()> {
|
||||||
.arg("sniffio"),
|
.arg("sniffio"),
|
||||||
@r"
|
@r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: The index returned metadata for the wrong package: expected distribution for sniffio, got distribution for anyio
|
× No solution found when resolving dependencies:
|
||||||
|
╰─▶ Because all versions of sniffio have the wrong package name in the index page (`anyio`) and you require sniffio, we can conclude that your requirements are unsatisfiable.
|
||||||
|
|
||||||
|
hint: Pre-releases are available for `sniffio` in the requested range (e.g., 4.0.0rc1), but pre-releases weren't enabled (try: `--prerelease=allow`)
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue