Skip to content

Commit cf30932

Browse files
Allow prereleases, locals, and URLs in non-editable path requirements (#2671)
## Summary This PR enables the resolver to "accept" URLs, prereleases, and local version specifiers for direct dependencies of path dependencies. As a result, `uv pip install .` and `uv pip install -e .` now behave identically, in that neither has a restriction on URL dependencies and the like. Closes #2643. Closes #1853.
1 parent 4b69ad4 commit cf30932

File tree

17 files changed

+484
-77
lines changed

17 files changed

+484
-77
lines changed

crates/distribution-types/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,14 @@ impl SourceDist {
425425
dist => dist,
426426
}
427427
}
428+
429+
/// Returns the path to the source distribution, if if it's a local distribution.
430+
pub fn as_path(&self) -> Option<&Path> {
431+
match self {
432+
Self::Path(dist) => Some(&dist.path),
433+
_ => None,
434+
}
435+
}
428436
}
429437

430438
impl Name for RegistryBuiltDist {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use pep508_rs::Requirement;
2+
use uv_normalize::ExtraName;
3+
4+
/// A set of requirements as requested by a parent requirement.
5+
///
6+
/// For example, given `flask[dotenv]`, the `RequestedRequirements` would include the `dotenv`
7+
/// extra, along with all of the requirements that are included in the `flask` distribution
8+
/// including their unevaluated markers.
9+
#[derive(Debug, Clone)]
10+
pub struct RequestedRequirements {
11+
/// The set of extras included on the originating requirement.
12+
extras: Vec<ExtraName>,
13+
/// The set of requirements that were requested by the originating requirement.
14+
requirements: Vec<Requirement>,
15+
}
16+
17+
impl RequestedRequirements {
18+
/// Instantiate a [`RequestedRequirements`] with the given `extras` and `requirements`.
19+
pub fn new(extras: Vec<ExtraName>, requirements: Vec<Requirement>) -> Self {
20+
Self {
21+
extras,
22+
requirements,
23+
}
24+
}
25+
26+
/// Return the extras that were included on the originating requirement.
27+
pub fn extras(&self) -> &[ExtraName] {
28+
&self.extras
29+
}
30+
31+
/// Return the requirements that were included on the originating requirement.
32+
pub fn requirements(&self) -> &[Requirement] {
33+
&self.requirements
34+
}
35+
}

crates/pep508-rs/src/verbatim_url.rs

+5
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,11 @@ impl Scheme {
385385
_ => None,
386386
}
387387
}
388+
389+
/// Returns `true` if the scheme is a file scheme.
390+
pub fn is_file(self) -> bool {
391+
matches!(self, Self::File)
392+
}
388393
}
389394

390395
impl std::fmt::Display for Scheme {

crates/uv-requirements/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
pub use crate::lookahead::*;
12
pub use crate::resolver::*;
23
pub use crate::source_tree::*;
34
pub use crate::sources::*;
45
pub use crate::specification::*;
56

67
mod confirm;
8+
mod lookahead;
79
mod pyproject;
810
mod resolver;
911
mod source_tree;
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::sync::Arc;
2+
3+
use anyhow::{Context, Result};
4+
use futures::{StreamExt, TryStreamExt};
5+
6+
use distribution_types::{BuildableSource, Dist};
7+
use pep508_rs::{Requirement, VersionOrUrl};
8+
use uv_client::RegistryClient;
9+
use uv_distribution::{Reporter, SourceDistCachedBuilder};
10+
use uv_types::{BuildContext, RequestedRequirements};
11+
12+
/// A resolver for resolving lookahead requirements from local dependencies.
13+
///
14+
/// The resolver extends certain privileges to "first-party" requirements. For example, first-party
15+
/// requirements are allowed to contain direct URL references, local version specifiers, and more.
16+
///
17+
/// We make an exception for transitive requirements of _local_ dependencies. For example,
18+
/// `pip install .` should treat the dependencies of `.` as if they were first-party dependencies.
19+
/// This matches our treatment of editable installs (`pip install -e .`).
20+
///
21+
/// The lookahead resolver resolves requirements for local dependencies, so that the resolver can
22+
/// treat them as first-party dependencies for the purpose of analyzing their specifiers.
23+
pub struct LookaheadResolver<'a> {
24+
/// The requirements for the project.
25+
requirements: &'a [Requirement],
26+
/// The reporter to use when building source distributions.
27+
reporter: Option<Arc<dyn Reporter>>,
28+
}
29+
30+
impl<'a> LookaheadResolver<'a> {
31+
/// Instantiate a new [`LookaheadResolver`] for a given set of `source_trees`.
32+
pub fn new(requirements: &'a [Requirement]) -> Self {
33+
Self {
34+
requirements,
35+
reporter: None,
36+
}
37+
}
38+
39+
/// Set the [`Reporter`] to use for this resolver.
40+
#[must_use]
41+
pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self {
42+
let reporter: Arc<dyn Reporter> = Arc::new(reporter);
43+
Self {
44+
reporter: Some(reporter),
45+
..self
46+
}
47+
}
48+
49+
/// Resolve the requirements from the provided source trees.
50+
pub async fn resolve<T: BuildContext>(
51+
self,
52+
context: &T,
53+
client: &RegistryClient,
54+
) -> Result<Vec<RequestedRequirements>> {
55+
let requirements: Vec<_> = futures::stream::iter(self.requirements.iter())
56+
.map(|requirement| async { self.lookahead(requirement, context, client).await })
57+
.buffered(50)
58+
.try_collect()
59+
.await?;
60+
Ok(requirements.into_iter().flatten().collect())
61+
}
62+
63+
/// Infer the package name for a given "unnamed" requirement.
64+
async fn lookahead<T: BuildContext>(
65+
&self,
66+
requirement: &Requirement,
67+
context: &T,
68+
client: &RegistryClient,
69+
) -> Result<Option<RequestedRequirements>> {
70+
// Determine whether the requirement represents a local distribution.
71+
let Some(VersionOrUrl::Url(url)) = requirement.version_or_url.as_ref() else {
72+
return Ok(None);
73+
};
74+
75+
// Convert to a buildable distribution.
76+
let dist = Dist::from_url(requirement.name.clone(), url.clone())?;
77+
78+
// Only support source trees (and not, e.g., wheels).
79+
let Dist::Source(source_dist) = &dist else {
80+
return Ok(None);
81+
};
82+
if !source_dist.as_path().is_some_and(std::path::Path::is_dir) {
83+
return Ok(None);
84+
}
85+
86+
// Run the PEP 517 build process to extract metadata from the source distribution.
87+
let builder = if let Some(reporter) = self.reporter.clone() {
88+
SourceDistCachedBuilder::new(context, client).with_reporter(reporter)
89+
} else {
90+
SourceDistCachedBuilder::new(context, client)
91+
};
92+
93+
let metadata = builder
94+
.download_and_build_metadata(&BuildableSource::Dist(source_dist))
95+
.await
96+
.context("Failed to build source distribution")?;
97+
98+
// Return the requirements from the metadata.
99+
Ok(Some(RequestedRequirements::new(
100+
requirement.extras.clone(),
101+
metadata.requires_dist,
102+
)))
103+
}
104+
}

crates/uv-resolver/src/manifest.rs

+29
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,44 @@ use distribution_types::LocalEditable;
22
use pep508_rs::Requirement;
33
use pypi_types::Metadata23;
44
use uv_normalize::PackageName;
5+
use uv_types::RequestedRequirements;
56

67
use crate::preferences::Preference;
78

89
/// A manifest of requirements, constraints, and preferences.
910
#[derive(Clone, Debug)]
1011
pub struct Manifest {
12+
/// The direct requirements for the project.
1113
pub(crate) requirements: Vec<Requirement>,
14+
15+
/// The constraints for the project.
1216
pub(crate) constraints: Vec<Requirement>,
17+
18+
/// The overrides for the project.
1319
pub(crate) overrides: Vec<Requirement>,
20+
21+
/// The preferences for the project.
22+
///
23+
/// These represent "preferred" versions of a given package. For example, they may be the
24+
/// versions that are already installed in the environment, or already pinned in an existing
25+
/// lockfile.
1426
pub(crate) preferences: Vec<Preference>,
27+
28+
/// The name of the project.
1529
pub(crate) project: Option<PackageName>,
30+
31+
/// The editable requirements for the project, which are built in advance.
32+
///
33+
/// The requirements of the editables should be included in resolution as if they were
34+
/// direct requirements in their own right.
1635
pub(crate) editables: Vec<(LocalEditable, Metadata23)>,
36+
37+
/// The lookahead requirements for the project.
38+
///
39+
/// These represent transitive dependencies that should be incorporated when making
40+
/// determinations around "allowed" versions (for example, "allowed" URLs or "allowed"
41+
/// pre-release versions).
42+
pub(crate) lookaheads: Vec<RequestedRequirements>,
1743
}
1844

1945
impl Manifest {
@@ -24,6 +50,7 @@ impl Manifest {
2450
preferences: Vec<Preference>,
2551
project: Option<PackageName>,
2652
editables: Vec<(LocalEditable, Metadata23)>,
53+
lookaheads: Vec<RequestedRequirements>,
2754
) -> Self {
2855
Self {
2956
requirements,
@@ -32,6 +59,7 @@ impl Manifest {
3259
preferences,
3360
project,
3461
editables,
62+
lookaheads,
3563
}
3664
}
3765

@@ -43,6 +71,7 @@ impl Manifest {
4371
preferences: Vec::new(),
4472
project: None,
4573
editables: Vec::new(),
74+
lookaheads: Vec::new(),
4675
}
4776
}
4877
}

crates/uv-resolver/src/prerelease_mode.rs

+10
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ impl PreReleaseStrategy {
6666
.chain(manifest.constraints.iter())
6767
.chain(manifest.overrides.iter())
6868
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
69+
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
70+
lookahead.requirements().iter().filter(|requirement| {
71+
requirement.evaluate_markers(markers, lookahead.extras())
72+
})
73+
}))
6974
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
7075
metadata.requires_dist.iter().filter(|requirement| {
7176
requirement.evaluate_markers(markers, &editable.extras)
@@ -95,6 +100,11 @@ impl PreReleaseStrategy {
95100
.chain(manifest.constraints.iter())
96101
.chain(manifest.overrides.iter())
97102
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
103+
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
104+
lookahead.requirements().iter().filter(|requirement| {
105+
requirement.evaluate_markers(markers, lookahead.extras())
106+
})
107+
}))
98108
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
99109
metadata.requires_dist.iter().filter(|requirement| {
100110
requirement.evaluate_markers(markers, &editable.extras)

crates/uv-resolver/src/resolution_mode.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,16 @@ impl ResolutionStrategy {
4141
ResolutionMode::Highest => Self::Highest,
4242
ResolutionMode::Lowest => Self::Lowest,
4343
ResolutionMode::LowestDirect => Self::LowestDirect(
44-
// Consider `requirements` and dependencies of `editables` to be "direct" dependencies.
44+
// Consider `requirements` and dependencies of any local requirements to be "direct" dependencies.
4545
manifest
4646
.requirements
4747
.iter()
4848
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
49+
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
50+
lookahead.requirements().iter().filter(|requirement| {
51+
requirement.evaluate_markers(markers, lookahead.extras())
52+
})
53+
}))
4954
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
5055
metadata.requires_dist.iter().filter(|requirement| {
5156
requirement.evaluate_markers(markers, &editable.extras)

crates/uv-resolver/src/resolver/locals.rs

+17-22
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,23 @@ impl Locals {
2424

2525
// Add all direct requirements and constraints. There's no need to look for conflicts,
2626
// since conflicting versions will be tracked upstream.
27-
for requirement in manifest
28-
.requirements
29-
.iter()
30-
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
31-
.chain(
32-
manifest
33-
.constraints
34-
.iter()
35-
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
36-
)
37-
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
38-
metadata
39-
.requires_dist
40-
.iter()
41-
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
42-
}))
43-
.chain(
44-
manifest
45-
.overrides
46-
.iter()
47-
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
48-
)
27+
for requirement in
28+
manifest
29+
.requirements
30+
.iter()
31+
.chain(manifest.constraints.iter())
32+
.chain(manifest.overrides.iter())
33+
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
34+
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
35+
lookahead.requirements().iter().filter(|requirement| {
36+
requirement.evaluate_markers(markers, lookahead.extras())
37+
})
38+
}))
39+
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
40+
metadata.requires_dist.iter().filter(|requirement| {
41+
requirement.evaluate_markers(markers, &editable.extras)
42+
})
43+
}))
4944
{
5045
if let Some(version_or_url) = requirement.version_or_url.as_ref() {
5146
for local in iter_locals(version_or_url) {

0 commit comments

Comments
 (0)