Error when direct URL requirements don't match Requires-Python (#2196)

## Summary

Closes https://github.com/astral-sh/uv/issues/2195.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2024-03-13 19:37:01 -07:00 committed by GitHub
parent 044a77cfd2
commit d29645ce75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 177 additions and 29 deletions

View file

@ -17,7 +17,6 @@ use tokio_stream::wrappers::ReceiverStream;
use tracing::{debug, info_span, instrument, trace, Instrument};
use url::Url;
use distribution_filename::WheelFilename;
use distribution_types::{
BuiltDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel,
Name, RemoteSource, SourceDist, VersionOrUrl,
@ -583,38 +582,57 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
// If the dist is an editable, return the version from the editable metadata.
if let Some((_local, metadata)) = self.editables.get(package_name) {
let version = metadata.version.clone();
return if range.contains(&version) {
Ok(Some(ResolverVersion::Available(version)))
} else {
Ok(None)
};
let version = &metadata.version;
// The version is incompatible with the requirement.
if !range.contains(version) {
return Ok(None);
}
// The version is incompatible due to its Python requirement.
if let Some(requires_python) = metadata.requires_python.as_ref() {
let target = self.python_requirement.target();
if !requires_python.contains(target) {
return Ok(Some(ResolverVersion::Unavailable(
version.clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
IncompatibleSource::RequiresPython(requires_python.clone()),
)),
)));
}
}
return Ok(Some(ResolverVersion::Available(version.clone())));
}
if let Ok(wheel_filename) = WheelFilename::try_from(url.raw()) {
// If the URL is that of a wheel, extract the version.
let version = wheel_filename.version;
if range.contains(&version) {
Ok(Some(ResolverVersion::Available(version)))
} else {
Ok(None)
}
} else {
// Otherwise, assume this is a source distribution.
let dist = PubGrubDistribution::from_url(package_name, url);
let metadata = self
.index
.distributions
.wait(&dist.package_id())
.await
.ok_or(ResolveError::Unregistered)?;
let version = &metadata.version;
if range.contains(version) {
Ok(Some(ResolverVersion::Available(version.clone())))
} else {
Ok(None)
let dist = PubGrubDistribution::from_url(package_name, url);
let metadata = self
.index
.distributions
.wait(&dist.package_id())
.await
.ok_or(ResolveError::Unregistered)?;
let version = &metadata.version;
// The version is incompatible with the requirement.
if !range.contains(version) {
return Ok(None);
}
// The version is incompatible due to its Python requirement.
if let Some(requires_python) = metadata.requires_python.as_ref() {
let target = self.python_requirement.target();
if !requires_python.contains(target) {
return Ok(Some(ResolverVersion::Unavailable(
version.clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
IncompatibleSource::RequiresPython(requires_python.clone()),
)),
)));
}
}
Ok(Some(ResolverVersion::Available(version.clone())))
}
PubGrubPackage::Package(package_name, extra, None) => {

View file

@ -5104,3 +5104,45 @@ fn no_stream() -> Result<()> {
Ok(())
}
/// Raise an error when a direct URL dependency's `Requires-Python` constraint is not met.
#[test]
fn requires_python_direct_url() -> Result<()> {
let context = TestContext::new("3.12");
// Create an editable package with a `Requires-Python` constraint that is not met.
let editable_dir = TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = "<=3.8"
"#,
)?;
// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!("example @ {}", editable_dir.path().display()))?;
uv_snapshot!(context.compile()
.arg("requirements.in"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No solution found when resolving dependencies:
Because the current Python version (3.12.1) does not satisfy Python<=3.8
and example==0.0.0 depends on Python<=3.8, we can conclude that
example==0.0.0 cannot be used.
And because only example==0.0.0 is available and you require example, we
can conclude that the requirements are unsatisfiable.
"###
);
Ok(())
}

View file

@ -2722,3 +2722,41 @@ fn dry_run_install_then_upgrade() -> std::result::Result<(), Box<dyn std::error:
Ok(())
}
/// Raise an error when a direct URL's `Requires-Python` constraint is not met.
#[test]
fn requires_python_direct_url() -> Result<()> {
let context = TestContext::new("3.12");
// Create an editable package with a `Requires-Python` constraint that is not met.
let editable_dir = assert_fs::TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = "<=3.8"
"#,
)?;
uv_snapshot!(command(&context)
.arg(format!("example @ {}", editable_dir.path().display())), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No solution found when resolving dependencies:
Because the current Python version (3.12.1) does not satisfy Python<=3.8
and example==0.0.0 depends on Python<=3.8, we can conclude that
example==0.0.0 cannot be used.
And because only example==0.0.0 is available and you require example, we
can conclude that the requirements are unsatisfiable.
"###
);
Ok(())
}

View file

@ -3024,3 +3024,53 @@ fn no_stream() -> Result<()> {
Ok(())
}
/// Raise an error when a direct URL dependency's `Requires-Python` constraint is not met.
///
/// TODO(charlie): This currently passes, but should fail. `sync` does not currently validate the
/// `Requires-Python` constraint for direct URL dependencies. (It _does_ respect `Requires-Python`
/// for registry-based dependencies.)
#[test]
fn requires_python_direct_url() -> Result<()> {
let context = TestContext::new("3.12");
// Create an editable package with a `Requires-Python` constraint that is not met.
let editable_dir = assert_fs::TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = "<=3.5"
"#,
)?;
// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!("example @ {}", editable_dir.path().display()))?;
// In addition to the standard filters, remove the temporary directory from the snapshot.
let filters: Vec<_> = [(r"\(from file://.*\)", "(from file://[TEMP_DIR])")]
.into_iter()
.chain(INSTA_FILTERS.to_vec())
.collect();
uv_snapshot!(filters, command(&context)
.arg("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ example==0.0.0 (from file://[TEMP_DIR])
"###
);
Ok(())
}