Restrict observed requirements to direct when --no-deps is specified (#3191)

## Summary

This PR avoids: (1) using the lookahead resolver when `--no-deps` is
specified (we'll never use those requirements), and (2) including any
transitive requirements when searching for allowed URLs, etc., when
`--no-deps` is specified.

Closes https://github.com/astral-sh/uv/issues/3183.
This commit is contained in:
Charlie Marsh 2024-04-22 13:17:58 -04:00 committed by GitHub
parent a4f125ca34
commit 792a917a97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 228 additions and 93 deletions

View file

@ -1,10 +1,10 @@
use pubgrub::range::Range;
use tracing::debug;
use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
use pep440_rs::Version;
use pep508_rs::MarkerEnvironment;
use tracing::debug;
use uv_normalize::PackageName;
use uv_types::InstalledPackagesProvider;
@ -32,11 +32,13 @@ impl CandidateSelector {
options.resolution_mode,
manifest,
markers,
options.dependency_mode,
),
prerelease_strategy: PreReleaseStrategy::from_mode(
options.prerelease_mode,
manifest,
markers,
options.dependency_mode,
),
}
}

View file

@ -1,11 +1,12 @@
use distribution_types::LocalEditable;
use either::Either;
use pep508_rs::{MarkerEnvironment, Requirement};
use pypi_types::Metadata23;
use uv_configuration::{Constraints, Overrides};
use uv_normalize::PackageName;
use uv_types::RequestedRequirements;
use crate::{preferences::Preference, Exclusions};
use crate::{preferences::Preference, DependencyMode, Exclusions};
/// A manifest of requirements, constraints, and preferences.
#[derive(Clone, Debug)]
@ -97,64 +98,103 @@ impl Manifest {
pub fn requirements<'a>(
&'a self,
markers: &'a MarkerEnvironment,
mode: DependencyMode,
) -> impl Iterator<Item = &Requirement> {
self.lookaheads
.iter()
.flat_map(|lookahead| {
self.overrides
.apply(lookahead.requirements())
.filter(|requirement| requirement.evaluate_markers(markers, lookahead.extras()))
})
.chain(self.editables.iter().flat_map(|(editable, metadata)| {
self.overrides
.apply(&metadata.requires_dist)
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
}))
.chain(
self.overrides
.apply(&self.requirements)
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.chain(
self.constraints
.requirements()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.chain(
self.overrides
.requirements()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
match mode {
// Include all direct and transitive requirements, with constraints and overrides applied.
DependencyMode::Transitive => Either::Left( self
.lookaheads
.iter()
.flat_map(|lookahead| {
self.overrides
.apply(lookahead.requirements())
.filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
})
.chain(self.editables.iter().flat_map(|(editable, metadata)| {
self.overrides
.apply(&metadata.requires_dist)
.filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
})
}))
.chain(
self.overrides
.apply(&self.requirements)
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.chain(
self.constraints
.requirements()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.chain(
self.overrides
.requirements()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
))
,
// Include direct requirements, with constraints and overrides applied.
DependencyMode::Direct => Either::Right(
self.overrides.apply(& self.requirements)
.chain(self.constraints.requirements())
.chain(self.overrides.requirements())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))),
}
}
/// Return an iterator over the names of all direct dependency requirements.
/// Return an iterator over the names of all user-provided requirements.
///
/// This includes:
/// - Direct requirements
/// - Dependencies of editable requirements
/// - Transitive dependencies of local package requirements
///
/// At time of writing, this is used for:
/// - Determining which packages should use the "lowest-compatible version" of a package, when
/// the `lowest-direct` strategy is in use.
pub fn direct_dependencies<'a>(
pub fn user_requirements<'a>(
&'a self,
markers: &'a MarkerEnvironment,
) -> impl Iterator<Item = &PackageName> {
self.lookaheads
.iter()
.filter(|lookahead| lookahead.direct())
.flat_map(|lookahead| {
mode: DependencyMode,
) -> impl Iterator<Item = &Requirement> {
match mode {
// Include direct requirements, dependencies of editables, and transitive dependencies
// of local packages.
DependencyMode::Transitive => Either::Left(
self.lookaheads
.iter()
.filter(|lookahead| lookahead.direct())
.flat_map(|lookahead| {
self.overrides
.apply(lookahead.requirements())
.filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
})
.chain(self.editables.iter().flat_map(|(editable, metadata)| {
self.overrides
.apply(&metadata.requires_dist)
.filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
})
}))
.chain(
self.overrides
.apply(&self.requirements)
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
),
),
// Restrict to the direct requirements.
DependencyMode::Direct => Either::Right(
self.overrides
.apply(lookahead.requirements())
.filter(|requirement| requirement.evaluate_markers(markers, lookahead.extras()))
})
.chain(self.editables.iter().flat_map(|(editable, metadata)| {
self.overrides
.apply(&metadata.requires_dist)
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
}))
.chain(
self.overrides
.apply(&self.requirements)
.apply(self.requirements.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.map(|requirement| &requirement.name)
),
}
}
/// Apply the overrides and constraints to a set of requirements.

View file

@ -3,7 +3,7 @@ use rustc_hash::FxHashSet;
use pep508_rs::{MarkerEnvironment, VersionOrUrl};
use uv_normalize::PackageName;
use crate::Manifest;
use crate::{DependencyMode, Manifest};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
@ -60,6 +60,7 @@ impl PreReleaseStrategy {
mode: PreReleaseMode,
manifest: &Manifest,
markers: &MarkerEnvironment,
dependencies: DependencyMode,
) -> Self {
match mode {
PreReleaseMode::Disallow => Self::Disallow,
@ -67,7 +68,7 @@ impl PreReleaseStrategy {
PreReleaseMode::IfNecessary => Self::IfNecessary,
PreReleaseMode::Explicit => Self::Explicit(
manifest
.requirements(markers)
.requirements(markers, dependencies)
.filter(|requirement| {
let Some(version_or_url) = &requirement.version_or_url else {
return false;
@ -87,7 +88,7 @@ impl PreReleaseStrategy {
),
PreReleaseMode::IfNecessaryOrExplicit => Self::IfNecessaryOrExplicit(
manifest
.requirements(markers)
.requirements(markers, dependencies)
.filter(|requirement| {
let Some(version_or_url) = &requirement.version_or_url else {
return false;

View file

@ -3,7 +3,7 @@ use rustc_hash::FxHashSet;
use pep508_rs::MarkerEnvironment;
use uv_normalize::PackageName;
use crate::Manifest;
use crate::{DependencyMode, Manifest};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
@ -42,13 +42,17 @@ impl ResolutionStrategy {
mode: ResolutionMode,
manifest: &Manifest,
markers: &MarkerEnvironment,
dependencies: DependencyMode,
) -> Self {
match mode {
ResolutionMode::Highest => Self::Highest,
ResolutionMode::Lowest => Self::Lowest,
ResolutionMode::LowestDirect => {
Self::LowestDirect(manifest.direct_dependencies(markers).cloned().collect())
}
ResolutionMode::LowestDirect => Self::LowestDirect(
manifest
.user_requirements(markers, dependencies)
.map(|requirement| requirement.name.clone())
.collect(),
),
}
}
}

View file

@ -9,7 +9,7 @@ use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError}
use pep508_rs::{MarkerEnvironment, VersionOrUrl};
use uv_normalize::PackageName;
use crate::Manifest;
use crate::{DependencyMode, Manifest};
#[derive(Debug, Default)]
pub(crate) struct Locals {
@ -19,12 +19,16 @@ pub(crate) struct Locals {
impl Locals {
/// Determine the set of permitted local versions in the [`Manifest`].
pub(crate) fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self {
pub(crate) fn from_manifest(
manifest: &Manifest,
markers: &MarkerEnvironment,
dependencies: DependencyMode,
) -> Self {
let mut required: FxHashMap<PackageName, Version> = FxHashMap::default();
// Add all direct requirements and constraints. There's no need to look for conflicts,
// since conflicts will be enforced by the solver.
for requirement in manifest.requirements(markers) {
for requirement in manifest.requirements(markers, dependencies) {
if let Some(version_or_url) = requirement.version_or_url.as_ref() {
for local in iter_locals(version_or_url) {
required.insert(requirement.name.clone(), local);

View file

@ -166,7 +166,7 @@ impl<
flat_index,
tags,
PythonRequirement::new(interpreter, markers),
AllowedYanks::from_manifest(&manifest, markers),
AllowedYanks::from_manifest(&manifest, markers, options.dependency_mode),
hasher,
options.exclude_newer,
build_context.no_binary(),
@ -210,8 +210,8 @@ impl<
visited: DashSet::default(),
selector: CandidateSelector::for_resolution(options, &manifest, markers),
dependency_mode: options.dependency_mode,
urls: Urls::from_manifest(&manifest, markers)?,
locals: Locals::from_manifest(&manifest, markers),
urls: Urls::from_manifest(&manifest, markers, options.dependency_mode)?,
locals: Locals::from_manifest(&manifest, markers, options.dependency_mode),
project: manifest.project,
requirements: manifest.requirements,
constraints: manifest.constraints,

View file

@ -6,7 +6,7 @@ use pep508_rs::{MarkerEnvironment, VerbatimUrl};
use uv_distribution::is_same_reference;
use uv_normalize::PackageName;
use crate::{Manifest, ResolveError};
use crate::{DependencyMode, Manifest, ResolveError};
/// A map of package names to their associated, required URLs.
#[derive(Debug, Default)]
@ -16,6 +16,7 @@ impl Urls {
pub(crate) fn from_manifest(
manifest: &Manifest,
markers: &MarkerEnvironment,
dependencies: DependencyMode,
) -> Result<Self, ResolveError> {
let mut urls: FxHashMap<PackageName, VerbatimUrl> = FxHashMap::default();
@ -37,7 +38,7 @@ impl Urls {
}
// Add all direct requirements and constraints. If there are any conflicts, return an error.
for requirement in manifest.requirements(markers) {
for requirement in manifest.requirements(markers, dependencies) {
if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url {
if let Some(previous) = urls.insert(requirement.name.clone(), url.clone()) {
if !is_equal(&previous, url) {

View file

@ -4,7 +4,7 @@ use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, VersionOrUrl};
use uv_normalize::PackageName;
use crate::{Manifest, Preference};
use crate::{DependencyMode, Manifest, Preference};
/// A set of package versions that are permitted, even if they're marked as yanked by the
/// relevant index.
@ -12,11 +12,15 @@ use crate::{Manifest, Preference};
pub struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
impl AllowedYanks {
pub fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self {
pub fn from_manifest(
manifest: &Manifest,
markers: &MarkerEnvironment,
dependencies: DependencyMode,
) -> Self {
let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default();
for requirement in manifest
.requirements(markers)
.requirements(markers, dependencies)
.chain(manifest.preferences.iter().map(Preference::requirement))
{
let Some(VersionOrUrl::VersionSpecifier(specifiers)) = &requirement.version_or_url

View file

@ -402,19 +402,24 @@ pub(crate) async fn pip_compile(
};
// Determine any lookahead requirements.
let lookaheads = LookaheadResolver::new(
&requirements,
&constraints,
&overrides,
&editables,
&hasher,
&build_dispatch,
&client,
&top_level_index,
)
.with_reporter(ResolverReporter::from(printer))
.resolve(&markers)
.await?;
let lookaheads = match dependency_mode {
DependencyMode::Transitive => {
LookaheadResolver::new(
&requirements,
&constraints,
&overrides,
&editables,
&hasher,
&build_dispatch,
&client,
&top_level_index,
)
.with_reporter(ResolverReporter::from(printer))
.resolve(&markers)
.await?
}
DependencyMode::Direct => Vec::new(),
};
// Create a manifest of the requirements.
let manifest = Manifest::new(

View file

@ -586,19 +586,24 @@ async fn resolve(
.collect();
// Determine any lookahead requirements.
let lookaheads = LookaheadResolver::new(
&requirements,
&constraints,
&overrides,
&editables,
hasher,
build_dispatch,
client,
index,
)
.with_reporter(ResolverReporter::from(printer))
.resolve(markers)
.await?;
let lookaheads = match options.dependency_mode {
DependencyMode::Transitive => {
LookaheadResolver::new(
&requirements,
&constraints,
&overrides,
&editables,
hasher,
build_dispatch,
client,
index,
)
.with_reporter(ResolverReporter::from(printer))
.resolve(markers)
.await?
}
DependencyMode::Direct => Vec::new(),
};
// Create a manifest of the requirements.
let manifest = Manifest::new(

View file

@ -5185,6 +5185,75 @@ fn no_deps_invalid_extra() -> Result<()> {
Ok(())
}
/// Resolve a package with `--no-deps` in which the requirements have a conflict in their
/// transitive dependencies. The resolution should succeed, since `--no-deps` ignores the
/// transitive dependencies.
#[test]
fn no_deps_transitive_conflict() -> Result<()> {
let context = TestContext::new("3.12");
// Create an editable with a dependency on `anyio` at a dedicated URL.
let editable_dir1 = context.temp_dir.child("editable1");
editable_dir1.create_dir_all()?;
let pyproject_toml = editable_dir1.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "editable1"
version = "0.0.1"
dependencies = [
"anyio @ https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl"
]
"#,
)?;
// Create an editable with a dependency on `anyio` at a different, dedicated URL.
let editable_dir2 = context.temp_dir.child("editable2");
editable_dir2.create_dir_all()?;
let pyproject_toml = editable_dir2.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "editable2"
version = "0.0.1"
dependencies = [
"anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl"
]
"#,
)?;
// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&indoc::formatdoc! {r#"
-e {}
-e {}
"#,
editable_dir1.path().display(),
editable_dir2.path().display()
})?;
uv_snapshot!(context.filters(), context.compile()
.arg("requirements.in")
.arg("--no-deps"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --no-deps
-e [TEMP_DIR]/editable1
-e [TEMP_DIR]/editable2
----- stderr -----
Built 2 editables in [TIME]
Resolved 2 packages in [TIME]
"###
);
Ok(())
}
/// Resolve an editable package with an invalid extra.
#[test]
fn editable_invalid_extra() -> Result<()> {