mirror of https://github.com/astral-sh/uv
Allow overrides in all satisfies checks (#11995)
## Summary This PR adds support for `SitePackages::satisfies` with unnamed overrides and requirements. The main challenge here was cases like: you have a `requirements.in` with `git+https://github.com/pallets/flask` in it, and an `overrides.txt` with `flask==2.0.0` in it. You _need_ to include `flask==2.0.0`, but you can't know that without resolving the unnamed URL requirement (since overrides only take effect when the package is included, like constraints). We now make the assumption that any unnamed overrides _are_ relevant, for the purpose of the satisfies check. This is conservative, but this whole check is an optimization anyway.
This commit is contained in:
parent
b955211698
commit
40dce4e009
|
|
@ -1,3 +1,4 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::iter::Flatten;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -14,6 +15,7 @@ use uv_distribution_types::{
|
|||
use uv_fs::Simplified;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pep508::VersionOrUrl;
|
||||
use uv_pypi_types::{Requirement, ResolverMarkerEnvironment, VerbatimParsedUrl};
|
||||
use uv_python::{Interpreter, PythonEnvironment};
|
||||
use uv_types::InstalledPackagesProvider;
|
||||
|
|
@ -281,132 +283,123 @@ impl SitePackages {
|
|||
}
|
||||
|
||||
/// Returns if the installed packages satisfy the given requirements.
|
||||
pub fn satisfies(
|
||||
pub fn satisfies_spec(
|
||||
&self,
|
||||
requirements: &[UnresolvedRequirementSpecification],
|
||||
constraints: &[NameRequirementSpecification],
|
||||
overrides: &[UnresolvedRequirementSpecification],
|
||||
markers: &ResolverMarkerEnvironment,
|
||||
) -> Result<SatisfiesResult> {
|
||||
// Collect the constraints, filtering them by their marker environment.
|
||||
let constraints: FxHashMap<&PackageName, Vec<&Requirement>> = constraints
|
||||
.iter()
|
||||
.filter(|constraint| constraint.requirement.evaluate_markers(Some(markers), &[]))
|
||||
.fold(FxHashMap::default(), |mut constraints, constraint| {
|
||||
constraints
|
||||
.entry(&constraint.requirement.name)
|
||||
.or_default()
|
||||
.push(&constraint.requirement);
|
||||
constraints
|
||||
});
|
||||
|
||||
let mut stack = Vec::with_capacity(requirements.len());
|
||||
let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), FxBuildHasher);
|
||||
|
||||
// Add the direct requirements to the queue.
|
||||
for entry in requirements {
|
||||
if entry.requirement.evaluate_markers(Some(markers), &[]) {
|
||||
if seen.insert(entry.clone()) {
|
||||
stack.push(entry.clone());
|
||||
// First, map all unnamed requirements to named requirements.
|
||||
let requirements = {
|
||||
let mut named = Vec::with_capacity(requirements.len());
|
||||
for requirement in requirements {
|
||||
match &requirement.requirement {
|
||||
UnresolvedRequirement::Named(requirement) => {
|
||||
named.push(Cow::Borrowed(requirement));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that all non-editable requirements are met.
|
||||
while let Some(entry) = stack.pop() {
|
||||
let installed = match &entry.requirement {
|
||||
UnresolvedRequirement::Named(requirement) => self.get_packages(&requirement.name),
|
||||
UnresolvedRequirement::Unnamed(requirement) => {
|
||||
self.get_urls(requirement.url.verbatim.raw())
|
||||
}
|
||||
};
|
||||
match installed.as_slice() {
|
||||
match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
|
||||
[] => {
|
||||
// The package isn't installed.
|
||||
return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string()));
|
||||
}
|
||||
[distribution] => {
|
||||
match RequirementSatisfaction::check(
|
||||
distribution,
|
||||
entry.requirement.source().as_ref(),
|
||||
)? {
|
||||
RequirementSatisfaction::Mismatch | RequirementSatisfaction::OutOfDate => {
|
||||
return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string()))
|
||||
}
|
||||
RequirementSatisfaction::Satisfied => {}
|
||||
}
|
||||
|
||||
// Validate that the installed version satisfies the constraints.
|
||||
for constraint in constraints.get(&distribution.name()).into_iter().flatten() {
|
||||
match RequirementSatisfaction::check(distribution, &constraint.source)? {
|
||||
RequirementSatisfaction::Mismatch
|
||||
| RequirementSatisfaction::OutOfDate => {
|
||||
return Ok(SatisfiesResult::Unsatisfied(
|
||||
entry.requirement.to_string(),
|
||||
requirement.url.verbatim.raw().to_string(),
|
||||
))
|
||||
}
|
||||
RequirementSatisfaction::Satisfied => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into the dependencies.
|
||||
let metadata = distribution
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read metadata for: {distribution}"))?;
|
||||
|
||||
// Add the dependencies to the queue.
|
||||
for dependency in metadata.requires_dist {
|
||||
if dependency.evaluate_markers(markers, entry.requirement.extras()) {
|
||||
let dependency = UnresolvedRequirementSpecification {
|
||||
requirement: UnresolvedRequirement::Named(Requirement::from(
|
||||
dependency,
|
||||
[distribution] => {
|
||||
let requirement = uv_pep508::Requirement {
|
||||
name: distribution.name().clone(),
|
||||
version_or_url: Some(VersionOrUrl::Url(
|
||||
requirement.url.clone(),
|
||||
)),
|
||||
hashes: vec![],
|
||||
marker: requirement.marker,
|
||||
extras: requirement.extras.clone(),
|
||||
origin: requirement.origin.clone(),
|
||||
};
|
||||
if seen.insert(dependency.clone()) {
|
||||
stack.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
named.push(Cow::Owned(Requirement::from(requirement)));
|
||||
}
|
||||
_ => {
|
||||
// There are multiple installed distributions for the same package.
|
||||
return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string()));
|
||||
return Ok(SatisfiesResult::Unsatisfied(
|
||||
requirement.url.verbatim.raw().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
named
|
||||
};
|
||||
|
||||
// Second, map all overrides to named requirements. We assume that all overrides are
|
||||
// relevant.
|
||||
let overrides = {
|
||||
let mut named = Vec::with_capacity(overrides.len());
|
||||
for requirement in overrides {
|
||||
match &requirement.requirement {
|
||||
UnresolvedRequirement::Named(requirement) => {
|
||||
named.push(Cow::Borrowed(requirement));
|
||||
}
|
||||
UnresolvedRequirement::Unnamed(requirement) => {
|
||||
match self.get_urls(requirement.url.verbatim.raw()).as_slice() {
|
||||
[] => {
|
||||
return Ok(SatisfiesResult::Unsatisfied(
|
||||
requirement.url.verbatim.raw().to_string(),
|
||||
))
|
||||
}
|
||||
[distribution] => {
|
||||
let requirement = uv_pep508::Requirement {
|
||||
name: distribution.name().clone(),
|
||||
version_or_url: Some(VersionOrUrl::Url(
|
||||
requirement.url.clone(),
|
||||
)),
|
||||
marker: requirement.marker,
|
||||
extras: requirement.extras.clone(),
|
||||
origin: requirement.origin.clone(),
|
||||
};
|
||||
named.push(Cow::Owned(Requirement::from(requirement)));
|
||||
}
|
||||
_ => {
|
||||
return Ok(SatisfiesResult::Unsatisfied(
|
||||
requirement.url.verbatim.raw().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
named
|
||||
};
|
||||
|
||||
self.satisfies_requirements(
|
||||
requirements.iter().map(Cow::as_ref),
|
||||
constraints.iter().map(|constraint| &constraint.requirement),
|
||||
overrides.iter().map(Cow::as_ref),
|
||||
markers,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(SatisfiesResult::Fresh {
|
||||
recursive_requirements: seen,
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`SitePackages::satisfies`], but with resolved names for all requirements.
|
||||
pub fn satisfies_names(
|
||||
/// Like [`SitePackages::satisfies_spec`], but with resolved names for all requirements.
|
||||
pub fn satisfies_requirements<'a>(
|
||||
&self,
|
||||
requirements: &[NameRequirementSpecification],
|
||||
constraints: &[NameRequirementSpecification],
|
||||
overrides: &[NameRequirementSpecification],
|
||||
requirements: impl ExactSizeIterator<Item = &'a Requirement>,
|
||||
constraints: impl Iterator<Item = &'a Requirement>,
|
||||
overrides: impl Iterator<Item = &'a Requirement>,
|
||||
markers: &ResolverMarkerEnvironment,
|
||||
) -> Result<SatisfiesResult> {
|
||||
// Collect the constraints and overrides by package name.
|
||||
let constraints: FxHashMap<&PackageName, Vec<&Requirement>> =
|
||||
constraints.fold(FxHashMap::default(), |mut constraints, constraint| {
|
||||
constraints
|
||||
.iter()
|
||||
.fold(FxHashMap::default(), |mut constraints, constraint| {
|
||||
constraints
|
||||
.entry(&constraint.requirement.name)
|
||||
.entry(&constraint.name)
|
||||
.or_default()
|
||||
.push(&constraint.requirement);
|
||||
.push(constraint);
|
||||
constraints
|
||||
});
|
||||
let overrides: FxHashMap<&PackageName, Vec<&Requirement>> =
|
||||
overrides.fold(FxHashMap::default(), |mut overrides, r#override| {
|
||||
overrides
|
||||
.iter()
|
||||
.fold(FxHashMap::default(), |mut overrides, r#override| {
|
||||
overrides
|
||||
.entry(&r#override.requirement.name)
|
||||
.entry(&r#override.name)
|
||||
.or_default()
|
||||
.push(&r#override.requirement);
|
||||
.push(r#override);
|
||||
overrides
|
||||
});
|
||||
|
||||
|
|
@ -414,46 +407,40 @@ impl SitePackages {
|
|||
let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), FxBuildHasher);
|
||||
|
||||
// Add the direct requirements to the queue.
|
||||
for entry in requirements {
|
||||
if let Some(r#overrides) = overrides.get(&entry.requirement.name) {
|
||||
for r#override in r#overrides {
|
||||
if r#override.evaluate_markers(Some(markers), &[]) {
|
||||
let entry = NameRequirementSpecification::from((*r#override).clone());
|
||||
if seen.insert(entry.clone()) {
|
||||
stack.push(entry);
|
||||
for requirement in requirements {
|
||||
if let Some(r#overrides) = overrides.get(&requirement.name) {
|
||||
for dependency in r#overrides {
|
||||
if dependency.evaluate_markers(Some(markers), &[]) {
|
||||
if seen.insert((*dependency).clone()) {
|
||||
stack.push(Cow::Borrowed(*dependency));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if entry.requirement.evaluate_markers(Some(markers), &[]) {
|
||||
if seen.insert(entry.clone()) {
|
||||
stack.push(entry.clone());
|
||||
if requirement.evaluate_markers(Some(markers), &[]) {
|
||||
if seen.insert(requirement.clone()) {
|
||||
stack.push(Cow::Borrowed(requirement));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that all non-editable requirements are met.
|
||||
while let Some(entry) = stack.pop() {
|
||||
let name = &entry.requirement.name;
|
||||
while let Some(requirement) = stack.pop() {
|
||||
let name = &requirement.name;
|
||||
let installed = self.get_packages(name);
|
||||
match installed.as_slice() {
|
||||
[] => {
|
||||
// The package isn't installed.
|
||||
return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string()));
|
||||
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
|
||||
}
|
||||
[distribution] => {
|
||||
// Validate that the requirement is satisfied.
|
||||
if entry.requirement.evaluate_markers(Some(markers), &[]) {
|
||||
match RequirementSatisfaction::check(
|
||||
distribution,
|
||||
&entry.requirement.source,
|
||||
)? {
|
||||
if requirement.evaluate_markers(Some(markers), &[]) {
|
||||
match RequirementSatisfaction::check(distribution, &requirement.source)? {
|
||||
RequirementSatisfaction::Mismatch
|
||||
| RequirementSatisfaction::OutOfDate => {
|
||||
return Ok(SatisfiesResult::Unsatisfied(
|
||||
entry.requirement.to_string(),
|
||||
))
|
||||
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()))
|
||||
}
|
||||
RequirementSatisfaction::Satisfied => {}
|
||||
}
|
||||
|
|
@ -467,7 +454,7 @@ impl SitePackages {
|
|||
RequirementSatisfaction::Mismatch
|
||||
| RequirementSatisfaction::OutOfDate => {
|
||||
return Ok(SatisfiesResult::Unsatisfied(
|
||||
entry.requirement.to_string(),
|
||||
requirement.to_string(),
|
||||
))
|
||||
}
|
||||
RequirementSatisfaction::Satisfied => {}
|
||||
|
|
@ -485,22 +472,16 @@ impl SitePackages {
|
|||
let dependency = Requirement::from(dependency);
|
||||
if let Some(r#overrides) = overrides.get(&dependency.name) {
|
||||
for dependency in r#overrides {
|
||||
if dependency
|
||||
.evaluate_markers(Some(markers), &entry.requirement.extras)
|
||||
{
|
||||
let dependency =
|
||||
NameRequirementSpecification::from((*dependency).clone());
|
||||
if seen.insert(dependency.clone()) {
|
||||
stack.push(dependency);
|
||||
if dependency.evaluate_markers(Some(markers), &requirement.extras) {
|
||||
if seen.insert((*dependency).clone()) {
|
||||
stack.push(Cow::Borrowed(*dependency));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if dependency.evaluate_markers(Some(markers), &entry.requirement.extras)
|
||||
{
|
||||
let dependency = NameRequirementSpecification::from(dependency);
|
||||
if dependency.evaluate_markers(Some(markers), &requirement.extras) {
|
||||
if seen.insert(dependency.clone()) {
|
||||
stack.push(dependency);
|
||||
stack.push(Cow::Owned(dependency));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -508,13 +489,13 @@ impl SitePackages {
|
|||
}
|
||||
_ => {
|
||||
// There are multiple installed distributions for the same package.
|
||||
return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string()));
|
||||
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SatisfiesResult::Fresh {
|
||||
recursive_requirements: FxHashSet::default(),
|
||||
recursive_requirements: seen,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -525,7 +506,7 @@ pub enum SatisfiesResult {
|
|||
/// All requirements are recursively satisfied.
|
||||
Fresh {
|
||||
/// The flattened set (transitive closure) of all requirements checked.
|
||||
recursive_requirements: FxHashSet<UnresolvedRequirementSpecification>,
|
||||
recursive_requirements: FxHashSet<Requirement>,
|
||||
},
|
||||
/// We found an unsatisfied requirement. Since we exit early, we only know about the first
|
||||
/// unsatisfied requirement.
|
||||
|
|
|
|||
|
|
@ -239,10 +239,9 @@ pub(crate) async fn pip_install(
|
|||
if reinstall.is_none()
|
||||
&& upgrade.is_none()
|
||||
&& source_trees.is_empty()
|
||||
&& overrides.is_empty()
|
||||
&& matches!(modifications, Modifications::Sufficient)
|
||||
{
|
||||
match site_packages.satisfies(&requirements, &constraints, &marker_env)? {
|
||||
match site_packages.satisfies_spec(&requirements, &constraints, &overrides, &marker_env)? {
|
||||
// If the requirements are already satisfied, we're done.
|
||||
SatisfiesResult::Fresh {
|
||||
recursive_requirements,
|
||||
|
|
@ -250,7 +249,7 @@ pub(crate) async fn pip_install(
|
|||
if enabled!(Level::DEBUG) {
|
||||
for requirement in recursive_requirements
|
||||
.iter()
|
||||
.map(|entry| entry.requirement.to_string())
|
||||
.map(ToString::to_string)
|
||||
.sorted()
|
||||
{
|
||||
debug!("Requirement satisfied: {requirement}");
|
||||
|
|
|
|||
|
|
@ -2009,8 +2009,8 @@ pub(crate) async fn update_environment(
|
|||
|
||||
// Check if the current environment satisfies the requirements
|
||||
let site_packages = SitePackages::from_environment(&venv)?;
|
||||
if source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() && overrides.is_empty() {
|
||||
match site_packages.satisfies(&requirements, &constraints, &marker_env)? {
|
||||
if source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() {
|
||||
match site_packages.satisfies_spec(&requirements, &constraints, &overrides, &marker_env)? {
|
||||
// If the requirements are already satisfied, we're done.
|
||||
SatisfiesResult::Fresh {
|
||||
recursive_requirements,
|
||||
|
|
@ -2022,7 +2022,7 @@ pub(crate) async fn update_environment(
|
|||
"All requirements satisfied: {}",
|
||||
recursive_requirements
|
||||
.iter()
|
||||
.map(|entry| entry.requirement.to_string())
|
||||
.map(ToString::to_string)
|
||||
.sorted()
|
||||
.join(" | ")
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1084,9 +1084,10 @@ fn can_skip_ephemeral(
|
|||
return false;
|
||||
}
|
||||
|
||||
match site_packages.satisfies(
|
||||
match site_packages.satisfies_spec(
|
||||
&spec.requirements,
|
||||
&spec.constraints,
|
||||
&spec.overrides,
|
||||
&base_interpreter.resolver_marker_environment(),
|
||||
) {
|
||||
// If the requirements are already satisfied, we're done.
|
||||
|
|
@ -1097,7 +1098,7 @@ fn can_skip_ephemeral(
|
|||
"Base environment satisfies requirements: {}",
|
||||
recursive_requirements
|
||||
.iter()
|
||||
.map(|entry| entry.requirement.to_string())
|
||||
.map(ToString::to_string)
|
||||
.sorted()
|
||||
.join(" | ")
|
||||
);
|
||||
|
|
|
|||
|
|
@ -814,28 +814,11 @@ async fn get_or_create_environment(
|
|||
{
|
||||
// Check if the installed packages meet the requirements.
|
||||
let site_packages = SitePackages::from_environment(&environment)?;
|
||||
|
||||
let requirements = requirements
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(NameRequirementSpecification::from)
|
||||
.collect::<Vec<_>>();
|
||||
let constraints = constraints
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(NameRequirementSpecification::from)
|
||||
.collect::<Vec<_>>();
|
||||
let overrides = overrides
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(NameRequirementSpecification::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if matches!(
|
||||
site_packages.satisfies_names(
|
||||
&requirements,
|
||||
&constraints,
|
||||
&overrides,
|
||||
site_packages.satisfies_requirements(
|
||||
requirements.iter(),
|
||||
constraints.iter(),
|
||||
overrides.iter(),
|
||||
&interpreter.resolver_marker_environment()
|
||||
),
|
||||
Ok(SatisfiesResult::Fresh { .. })
|
||||
|
|
|
|||
Loading…
Reference in New Issue