mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Allow incompatible requires-python
for source distributions with static metadata (#8768)
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 / 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 / build binary | linux (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 (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda 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 | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy 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 | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (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 | alpine (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 (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 (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (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 linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
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 / 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 / build binary | linux (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 (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda 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 | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy 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 | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (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 | alpine (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 (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 (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (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 linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
## Summary At present, when we have a Python requirement and we see a wheel, we verify that the Python requirement is compatible with the wheel. For source distributions, though, we verify that both the Python requirement _and_ the currently-installed version are compatible, because we assume that we'll need to build the source distribution in order to get metadata. However, we can often extract source distribution metadata _without_ building (e.g., if there's a `pyproject.toml` with no dynamic keys). This PR thus modifies the source distribution handling to defer that incompatibility ("We couldn't get metadata for this project, because it has no static metadata and requires a higher Python version to run / build") until we actually try to build the package. As a result, you can now resolve source distribution-only packages using Python versions below their `requires-python`, as long as they include static metadata. Closes https://github.com/astral-sh/uv/issues/8767.
This commit is contained in:
parent
647494b998
commit
bf79d985ee
11 changed files with 279 additions and 128 deletions
|
@ -132,6 +132,10 @@ impl<'a> BuildDispatch<'a> {
|
|||
impl<'a> BuildContext for BuildDispatch<'a> {
|
||||
type SourceDistBuilder = SourceBuild;
|
||||
|
||||
fn interpreter(&self) -> &Interpreter {
|
||||
self.interpreter
|
||||
}
|
||||
|
||||
fn cache(&self) -> &Cache {
|
||||
self.cache
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::path::Path;
|
|||
use url::Url;
|
||||
use uv_distribution_filename::SourceDistExtension;
|
||||
use uv_git::GitUrl;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pep508::VerbatimUrl;
|
||||
|
||||
use uv_normalize::PackageName;
|
||||
|
@ -64,6 +64,14 @@ impl BuildableSource<'_> {
|
|||
Self::Url(url) => matches!(url, SourceUrl::Directory(_)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the Python version specifier required by the source, if available.
|
||||
pub fn requires_python(&self) -> Option<&VersionSpecifiers> {
|
||||
let Self::Dist(SourceDist::Registry(dist)) = self else {
|
||||
return None;
|
||||
};
|
||||
dist.file.requires_python.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BuildableSource<'_> {
|
||||
|
|
|
@ -10,7 +10,7 @@ use uv_client::WrappedReqwestError;
|
|||
use uv_distribution_filename::WheelFilenameError;
|
||||
use uv_fs::Simplified;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pypi_types::{HashDigest, ParsedUrlError};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -96,6 +96,8 @@ pub enum Error {
|
|||
NotFound(Url),
|
||||
#[error("Attempted to re-extract the source distribution for `{0}`, but the hashes didn't match. Run `{}` to clear the cache.", "uv cache clean".green())]
|
||||
CacheHeal(String),
|
||||
#[error("The source distribution requires Python {0}, but {1} is installed")]
|
||||
RequiresPython(VersionSpecifiers, Version),
|
||||
|
||||
/// A generic request middleware error happened while making a request.
|
||||
/// Refer to the error message for more details.
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
//! Fetch and build source distributions from remote sources.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Bound;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::distribution_database::ManagedClient;
|
||||
use crate::error::Error;
|
||||
use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata};
|
||||
use crate::reporter::Facade;
|
||||
use crate::source::built_wheel_metadata::BuiltWheelMetadata;
|
||||
use crate::source::revision::Revision;
|
||||
use crate::{Reporter, RequiresDist};
|
||||
use fs_err::tokio as fs;
|
||||
use futures::{FutureExt, TryStreamExt};
|
||||
use reqwest::Response;
|
||||
|
@ -26,19 +34,12 @@ use uv_distribution_types::{
|
|||
use uv_extract::hash::Hasher;
|
||||
use uv_fs::{rename_with_retry, write_atomic, LockedFile};
|
||||
use uv_metadata::read_archive_metadata;
|
||||
use uv_pep440::release_specifiers_to_ranges;
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_pypi_types::{HashDigest, Metadata12, RequiresTxt, ResolutionMetadata};
|
||||
use uv_types::{BuildContext, SourceBuildTrait};
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::distribution_database::ManagedClient;
|
||||
use crate::error::Error;
|
||||
use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata};
|
||||
use crate::reporter::Facade;
|
||||
use crate::source::built_wheel_metadata::BuiltWheelMetadata;
|
||||
use crate::source::revision::Revision;
|
||||
use crate::{Reporter, RequiresDist};
|
||||
|
||||
mod built_wheel_metadata;
|
||||
mod revision;
|
||||
|
||||
|
@ -1798,6 +1799,27 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
|||
) -> Result<Option<ResolutionMetadata>, Error> {
|
||||
debug!("Preparing metadata for: {source}");
|
||||
|
||||
// Ensure that the _installed_ Python version is compatible with the `requires-python`
|
||||
// specifier.
|
||||
if let Some(requires_python) = source.requires_python() {
|
||||
let installed = self.build_context.interpreter().python_version();
|
||||
let target = release_specifiers_to_ranges(requires_python.clone())
|
||||
.bounding_range()
|
||||
.map(|bounding_range| bounding_range.0.cloned())
|
||||
.unwrap_or(Bound::Unbounded);
|
||||
let is_compatible = match target {
|
||||
Bound::Included(target) => *installed >= target,
|
||||
Bound::Excluded(target) => *installed > target,
|
||||
Bound::Unbounded => true,
|
||||
};
|
||||
if !is_compatible {
|
||||
return Err(Error::RequiresPython(
|
||||
requires_python.clone(),
|
||||
installed.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the builder.
|
||||
let mut builder = self
|
||||
.build_context
|
||||
|
|
|
@ -11,7 +11,7 @@ use rustc_hash::FxHashMap;
|
|||
use uv_configuration::IndexStrategy;
|
||||
use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
|
||||
use crate::candidate_selector::CandidateSelector;
|
||||
use crate::error::ErrorTree;
|
||||
|
@ -699,6 +699,14 @@ impl PubGrubReportFormatter<'_> {
|
|||
reason: reason.clone(),
|
||||
});
|
||||
}
|
||||
IncompletePackage::RequiresPython(requires_python, python_version) => {
|
||||
hints.insert(PubGrubHint::IncompatibleBuildRequirement {
|
||||
package: package.clone(),
|
||||
version: version.clone(),
|
||||
requires_python: requires_python.clone(),
|
||||
python_version: python_version.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -862,6 +870,17 @@ pub(crate) enum PubGrubHint {
|
|||
// excluded from `PartialEq` and `Hash`
|
||||
reason: String,
|
||||
},
|
||||
/// The source distribution has a `requires-python` requirement that is not met by the installed
|
||||
/// Python version (and static metadata is not available).
|
||||
IncompatibleBuildRequirement {
|
||||
package: PubGrubPackage,
|
||||
// excluded from `PartialEq` and `Hash`
|
||||
version: Version,
|
||||
// excluded from `PartialEq` and `Hash`
|
||||
requires_python: VersionSpecifiers,
|
||||
// excluded from `PartialEq` and `Hash`
|
||||
python_version: Version,
|
||||
},
|
||||
/// The `Requires-Python` requirement was not satisfied.
|
||||
RequiresPython {
|
||||
source: PythonRequirementSource,
|
||||
|
@ -932,6 +951,9 @@ enum PubGrubHintCore {
|
|||
InvalidVersionStructure {
|
||||
package: PubGrubPackage,
|
||||
},
|
||||
IncompatibleBuildRequirement {
|
||||
package: PubGrubPackage,
|
||||
},
|
||||
RequiresPython {
|
||||
source: PythonRequirementSource,
|
||||
requires_python: RequiresPython,
|
||||
|
@ -985,6 +1007,9 @@ impl From<PubGrubHint> for PubGrubHintCore {
|
|||
PubGrubHint::InvalidVersionStructure { package, .. } => {
|
||||
Self::InvalidVersionStructure { package }
|
||||
}
|
||||
PubGrubHint::IncompatibleBuildRequirement { package, .. } => {
|
||||
Self::IncompatibleBuildRequirement { package }
|
||||
}
|
||||
PubGrubHint::RequiresPython {
|
||||
source,
|
||||
requires_python,
|
||||
|
@ -1187,6 +1212,23 @@ impl std::fmt::Display for PubGrubHint {
|
|||
package_requires_python.bold(),
|
||||
)
|
||||
}
|
||||
Self::IncompatibleBuildRequirement {
|
||||
package,
|
||||
version,
|
||||
requires_python,
|
||||
python_version,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"{}{} The source distribution for {}=={} does not include static metadata. Generating metadata for this package requires Python {}, but Python {} is installed.",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.bold(),
|
||||
version.bold(),
|
||||
requires_python.bold(),
|
||||
python_version.bold(),
|
||||
)
|
||||
}
|
||||
Self::RequiresPython {
|
||||
source: PythonRequirementSource::Interpreter,
|
||||
requires_python: _,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use uv_distribution_types::IncompatibleDist;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
|
||||
/// The reason why a package or a version cannot be used.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
|
@ -40,6 +40,9 @@ pub(crate) enum UnavailableVersion {
|
|||
InvalidStructure,
|
||||
/// The wheel metadata was not found in the cache and the network is not available.
|
||||
Offline,
|
||||
/// The source distribution has a `requires-python` requirement that is not met by the installed
|
||||
/// Python version (and static metadata is not available).
|
||||
RequiresPython(VersionSpecifiers),
|
||||
}
|
||||
|
||||
impl UnavailableVersion {
|
||||
|
@ -51,6 +54,9 @@ impl UnavailableVersion {
|
|||
UnavailableVersion::InconsistentMetadata => "inconsistent metadata".into(),
|
||||
UnavailableVersion::InvalidStructure => "an invalid package format".into(),
|
||||
UnavailableVersion::Offline => "to be downloaded from a registry".into(),
|
||||
UnavailableVersion::RequiresPython(requires_python) => {
|
||||
format!("Python {requires_python}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,6 +68,7 @@ impl UnavailableVersion {
|
|||
UnavailableVersion::InconsistentMetadata => format!("has {self}"),
|
||||
UnavailableVersion::InvalidStructure => format!("has {self}"),
|
||||
UnavailableVersion::Offline => format!("needs {self}"),
|
||||
UnavailableVersion::RequiresPython(..) => format!("requires {self}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,6 +80,7 @@ impl UnavailableVersion {
|
|||
UnavailableVersion::InconsistentMetadata => format!("have {self}"),
|
||||
UnavailableVersion::InvalidStructure => format!("have {self}"),
|
||||
UnavailableVersion::Offline => format!("need {self}"),
|
||||
UnavailableVersion::RequiresPython(..) => format!("require {self}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +151,9 @@ pub(crate) enum IncompletePackage {
|
|||
InconsistentMetadata(String),
|
||||
/// The wheel has an invalid structure.
|
||||
InvalidStructure(String),
|
||||
/// The source distribution has a `requires-python` requirement that is not met by the installed
|
||||
/// Python version (and static metadata is not available).
|
||||
RequiresPython(VersionSpecifiers, Version),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -958,6 +958,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
);
|
||||
return Ok(None);
|
||||
}
|
||||
MetadataResponse::RequiresPython(..) => {
|
||||
unreachable!("`requires-python` is only known upfront for registry distributions")
|
||||
}
|
||||
};
|
||||
|
||||
let version = &metadata.version;
|
||||
|
@ -1074,72 +1077,54 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
}
|
||||
};
|
||||
|
||||
let incompatibility = match dist {
|
||||
// Validate the Python requirement.
|
||||
let requires_python = match dist {
|
||||
CompatibleDist::InstalledDist(_) => None,
|
||||
CompatibleDist::SourceDist { sdist, .. }
|
||||
| CompatibleDist::IncompatibleWheel { sdist, .. } => {
|
||||
// Source distributions must meet both the _target_ Python version and the
|
||||
// _installed_ Python version (to build successfully).
|
||||
sdist
|
||||
.file
|
||||
.requires_python
|
||||
.as_ref()
|
||||
.and_then(|requires_python| {
|
||||
if !python_requirement
|
||||
.installed()
|
||||
.is_contained_by(requires_python)
|
||||
{
|
||||
return Some(IncompatibleDist::Source(
|
||||
IncompatibleSource::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Installed,
|
||||
),
|
||||
));
|
||||
}
|
||||
if !python_requirement.target().is_contained_by(requires_python) {
|
||||
return Some(IncompatibleDist::Source(
|
||||
IncompatibleSource::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Target,
|
||||
),
|
||||
));
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
CompatibleDist::CompatibleWheel { wheel, .. } => {
|
||||
// Wheels must meet the _target_ Python version.
|
||||
wheel
|
||||
.file
|
||||
.requires_python
|
||||
.as_ref()
|
||||
.and_then(|requires_python| {
|
||||
if python_requirement.installed() == python_requirement.target() {
|
||||
if !python_requirement
|
||||
.installed()
|
||||
.is_contained_by(requires_python)
|
||||
{
|
||||
return Some(IncompatibleDist::Wheel(
|
||||
IncompatibleWheel::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Installed,
|
||||
),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if !python_requirement.target().is_contained_by(requires_python) {
|
||||
return Some(IncompatibleDist::Wheel(
|
||||
IncompatibleWheel::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Target,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
sdist.file.requires_python.as_ref()
|
||||
}
|
||||
CompatibleDist::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_ref(),
|
||||
};
|
||||
let incompatibility = requires_python.and_then(|requires_python| {
|
||||
if python_requirement.installed() == python_requirement.target() {
|
||||
if !python_requirement
|
||||
.installed()
|
||||
.is_contained_by(requires_python)
|
||||
{
|
||||
return if matches!(dist, CompatibleDist::CompatibleWheel { .. }) {
|
||||
Some(IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Installed,
|
||||
)))
|
||||
} else {
|
||||
Some(IncompatibleDist::Source(
|
||||
IncompatibleSource::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Installed,
|
||||
),
|
||||
))
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if !python_requirement.target().is_contained_by(requires_python) {
|
||||
return if matches!(dist, CompatibleDist::CompatibleWheel { .. }) {
|
||||
Some(IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Target,
|
||||
)))
|
||||
} else {
|
||||
Some(IncompatibleDist::Source(
|
||||
IncompatibleSource::RequiresPython(
|
||||
requires_python.clone(),
|
||||
PythonRequirementKind::Target,
|
||||
),
|
||||
))
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
// The version is incompatible due to its Python requirement.
|
||||
if let Some(incompatibility) = incompatibility {
|
||||
|
@ -1342,6 +1327,28 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
UnavailableVersion::InvalidStructure,
|
||||
));
|
||||
}
|
||||
MetadataResponse::RequiresPython(requires_python, python_version) => {
|
||||
warn!(
|
||||
"Unable to extract metadata for {name}: {}",
|
||||
uv_distribution::Error::RequiresPython(
|
||||
requires_python.clone(),
|
||||
python_version.clone()
|
||||
)
|
||||
);
|
||||
self.incomplete_packages
|
||||
.entry(name.clone())
|
||||
.or_default()
|
||||
.insert(
|
||||
version.clone(),
|
||||
IncompletePackage::RequiresPython(
|
||||
requires_python.clone(),
|
||||
python_version.clone(),
|
||||
),
|
||||
);
|
||||
return Ok(Dependencies::Unavailable(
|
||||
UnavailableVersion::RequiresPython(requires_python.clone()),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let requirements = self.flatten_requirements(
|
||||
|
@ -1888,33 +1895,22 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
}
|
||||
}
|
||||
|
||||
match dist {
|
||||
CompatibleDist::InstalledDist(_) => {}
|
||||
// Validate the Python requirement.
|
||||
let requires_python = match dist {
|
||||
CompatibleDist::InstalledDist(_) => None,
|
||||
CompatibleDist::SourceDist { sdist, .. }
|
||||
| CompatibleDist::IncompatibleWheel { sdist, .. } => {
|
||||
// Source distributions must meet both the _target_ Python version and the
|
||||
// _installed_ Python version (to build successfully).
|
||||
if let Some(requires_python) = sdist.file.requires_python.as_ref() {
|
||||
if !python_requirement
|
||||
.installed()
|
||||
.is_contained_by(requires_python)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
if !python_requirement.target().is_contained_by(requires_python) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
sdist.file.requires_python.as_ref()
|
||||
}
|
||||
CompatibleDist::CompatibleWheel { wheel, .. } => {
|
||||
// Wheels must meet the _target_ Python version.
|
||||
if let Some(requires_python) = wheel.file.requires_python.as_ref() {
|
||||
if !python_requirement.target().is_contained_by(requires_python) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
wheel.file.requires_python.as_ref()
|
||||
}
|
||||
};
|
||||
if let Some(requires_python) = requires_python.as_ref() {
|
||||
if !python_requirement.target().is_contained_by(requires_python) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit a request to fetch the metadata for this version.
|
||||
if self.index.distributions().register(candidate.version_id()) {
|
||||
|
|
|
@ -4,6 +4,7 @@ use uv_configuration::BuildOptions;
|
|||
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
|
||||
use uv_distribution_types::{Dist, IndexCapabilities, IndexUrl};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_types::{BuildContext, HashStrategy};
|
||||
|
||||
|
@ -42,6 +43,9 @@ pub enum MetadataResponse {
|
|||
InvalidStructure(Box<uv_metadata::Error>),
|
||||
/// The wheel metadata was not found in the cache and the network is not available.
|
||||
Offline,
|
||||
/// The source distribution has a `requires-python` requirement that is not met by the installed
|
||||
/// Python version (and static metadata is not available).
|
||||
RequiresPython(VersionSpecifiers, Version),
|
||||
}
|
||||
|
||||
pub trait ResolverProvider {
|
||||
|
@ -203,6 +207,9 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a,
|
|||
uv_distribution::Error::WheelMetadata(_, err) => {
|
||||
Ok(MetadataResponse::InvalidStructure(err))
|
||||
}
|
||||
uv_distribution::Error::RequiresPython(requires_python, version) => {
|
||||
Ok(MetadataResponse::RequiresPython(requires_python, version))
|
||||
}
|
||||
err => Err(err),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use uv_distribution_types::{
|
|||
use uv_git::GitResolver;
|
||||
use uv_pep508::PackageName;
|
||||
use uv_pypi_types::Requirement;
|
||||
use uv_python::PythonEnvironment;
|
||||
use uv_python::{Interpreter, PythonEnvironment};
|
||||
|
||||
/// Avoids cyclic crate dependencies between resolver, installer and builder.
|
||||
///
|
||||
|
@ -56,6 +56,9 @@ use uv_python::PythonEnvironment;
|
|||
pub trait BuildContext {
|
||||
type SourceDistBuilder: SourceBuildTrait;
|
||||
|
||||
/// Return a reference to the interpreter.
|
||||
fn interpreter(&self) -> &Interpreter;
|
||||
|
||||
/// Return a reference to the cache.
|
||||
fn cache(&self) -> &Cache;
|
||||
|
||||
|
|
|
@ -12661,3 +12661,62 @@ fn prune_unreachable() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allow resolving a package that requires a Python version that is not available, as long as it
|
||||
/// includes static metadata.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/uv/issues/8767>
|
||||
#[test]
|
||||
fn unsupported_requires_python_static_metadata() -> Result<()> {
|
||||
let context = TestContext::new("3.11");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("interpreters-pep-734 <= 0.4.1 ; python_version >= '3.13'")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context
|
||||
.pip_compile()
|
||||
.arg("--universal")
|
||||
.arg("requirements.in")
|
||||
.env(EnvVars::UV_EXCLUDE_NEWER, "2024-11-04T00:00:00Z"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --universal requirements.in
|
||||
interpreters-pep-734==0.4.1 ; python_full_version >= '3.13'
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disallow resolving a package that requires a Python version that is not available, if it uses
|
||||
/// dynamic metadata.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/uv/issues/8767>
|
||||
#[test]
|
||||
fn unsupported_requires_python_dynamic_metadata() -> Result<()> {
|
||||
let context = TestContext::new("3.8");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("source-distribution==0.0.3 ; python_version >= '3.10'")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context
|
||||
.pip_compile()
|
||||
.arg("--universal")
|
||||
.arg("requirements.in")
|
||||
.env(EnvVars::UV_EXCLUDE_NEWER, "2024-11-04T00:00:00Z"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- 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.
|
||||
|
||||
hint: The source distribution for source-distribution{python_full_version >= '3.10'}==0.0.3 does not include static metadata. Generating metadata for this package requires Python >=3.10, but Python 3.8.[X] is installed.
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -175,20 +175,22 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()>
|
|||
// dependencies.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
.arg("--python-version=3.11")
|
||||
, @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead.
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
|
||||
And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable.
|
||||
"#
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
output.assert().failure();
|
||||
output.assert().success();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -287,20 +289,22 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()>
|
|||
// determine its dependencies.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
.arg("--python-version=3.11")
|
||||
, @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead.
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
|
||||
And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable.
|
||||
"#
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
output.assert().failure();
|
||||
output.assert().success();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -345,29 +349,22 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> {
|
|||
// available, but is not compatible with the target version and cannot be used.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
.arg("--python-version=3.11")
|
||||
, @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead.
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
|
||||
And because only the following versions of package-a are available:
|
||||
package-a==1.0.0
|
||||
package-a==2.0.0
|
||||
we can conclude that package-a<2.0.0 cannot be used. (1)
|
||||
|
||||
Because the requested Python version (>=3.11.0) does not satisfy Python>=3.12 and package-a==2.0.0 depends on Python>=3.12, we can conclude that package-a==2.0.0 cannot be used.
|
||||
And because we know from (1) that package-a<2.0.0 cannot be used, we can conclude that all versions of package-a cannot be used.
|
||||
And because you require package-a, we can conclude that your requirements are unsatisfiable.
|
||||
|
||||
hint: The `--python-version` value (>=3.11.0) includes Python versions that are not supported by your dependencies (e.g., package-a==2.0.0 only supports >=3.12). Consider using a higher `--python-version` value.
|
||||
"#
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
output.assert().failure();
|
||||
output.assert().success();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue