mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
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:
parent
a4f125ca34
commit
792a917a97
11 changed files with 228 additions and 93 deletions
|
@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue