Skip to content

Commit

Permalink
Allow prereleases, locals, and URLs in non-editable path requirements (
Browse files Browse the repository at this point in the history
…#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.
  • Loading branch information
charliermarsh committed Mar 27, 2024
1 parent 4b69ad4 commit cf30932
Show file tree
Hide file tree
Showing 17 changed files with 484 additions and 77 deletions.
8 changes: 8 additions & 0 deletions crates/distribution-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,14 @@ impl SourceDist {
dist => dist,
}
}

/// Returns the path to the source distribution, if if it's a local distribution.
pub fn as_path(&self) -> Option<&Path> {
match self {
Self::Path(dist) => Some(&dist.path),
_ => None,
}
}
}

impl Name for RegistryBuiltDist {
Expand Down
35 changes: 35 additions & 0 deletions crates/distribution-types/src/requirements.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use pep508_rs::Requirement;
use uv_normalize::ExtraName;

/// A set of requirements as requested by a parent requirement.
///
/// For example, given `flask[dotenv]`, the `RequestedRequirements` would include the `dotenv`
/// extra, along with all of the requirements that are included in the `flask` distribution
/// including their unevaluated markers.
#[derive(Debug, Clone)]
pub struct RequestedRequirements {
/// The set of extras included on the originating requirement.
extras: Vec<ExtraName>,
/// The set of requirements that were requested by the originating requirement.
requirements: Vec<Requirement>,
}

impl RequestedRequirements {
/// Instantiate a [`RequestedRequirements`] with the given `extras` and `requirements`.
pub fn new(extras: Vec<ExtraName>, requirements: Vec<Requirement>) -> Self {
Self {
extras,
requirements,
}
}

/// Return the extras that were included on the originating requirement.
pub fn extras(&self) -> &[ExtraName] {
&self.extras
}

/// Return the requirements that were included on the originating requirement.
pub fn requirements(&self) -> &[Requirement] {
&self.requirements
}
}
5 changes: 5 additions & 0 deletions crates/pep508-rs/src/verbatim_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,11 @@ impl Scheme {
_ => None,
}
}

/// Returns `true` if the scheme is a file scheme.
pub fn is_file(self) -> bool {
matches!(self, Self::File)
}
}

impl std::fmt::Display for Scheme {
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-requirements/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
pub use crate::lookahead::*;
pub use crate::resolver::*;
pub use crate::source_tree::*;
pub use crate::sources::*;
pub use crate::specification::*;

mod confirm;
mod lookahead;
mod pyproject;
mod resolver;
mod source_tree;
Expand Down
104 changes: 104 additions & 0 deletions crates/uv-requirements/src/lookahead.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::sync::Arc;

use anyhow::{Context, Result};
use futures::{StreamExt, TryStreamExt};

use distribution_types::{BuildableSource, Dist};
use pep508_rs::{Requirement, VersionOrUrl};
use uv_client::RegistryClient;
use uv_distribution::{Reporter, SourceDistCachedBuilder};
use uv_types::{BuildContext, RequestedRequirements};

/// A resolver for resolving lookahead requirements from local dependencies.
///
/// The resolver extends certain privileges to "first-party" requirements. For example, first-party
/// requirements are allowed to contain direct URL references, local version specifiers, and more.
///
/// We make an exception for transitive requirements of _local_ dependencies. For example,
/// `pip install .` should treat the dependencies of `.` as if they were first-party dependencies.
/// This matches our treatment of editable installs (`pip install -e .`).
///
/// The lookahead resolver resolves requirements for local dependencies, so that the resolver can
/// treat them as first-party dependencies for the purpose of analyzing their specifiers.
pub struct LookaheadResolver<'a> {
/// The requirements for the project.
requirements: &'a [Requirement],
/// The reporter to use when building source distributions.
reporter: Option<Arc<dyn Reporter>>,
}

impl<'a> LookaheadResolver<'a> {
/// Instantiate a new [`LookaheadResolver`] for a given set of `source_trees`.
pub fn new(requirements: &'a [Requirement]) -> Self {
Self {
requirements,
reporter: None,
}
}

/// Set the [`Reporter`] to use for this resolver.
#[must_use]
pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self {
let reporter: Arc<dyn Reporter> = Arc::new(reporter);
Self {
reporter: Some(reporter),
..self
}
}

/// Resolve the requirements from the provided source trees.
pub async fn resolve<T: BuildContext>(
self,
context: &T,
client: &RegistryClient,
) -> Result<Vec<RequestedRequirements>> {
let requirements: Vec<_> = futures::stream::iter(self.requirements.iter())
.map(|requirement| async { self.lookahead(requirement, context, client).await })
.buffered(50)
.try_collect()
.await?;
Ok(requirements.into_iter().flatten().collect())
}

/// Infer the package name for a given "unnamed" requirement.
async fn lookahead<T: BuildContext>(
&self,
requirement: &Requirement,
context: &T,
client: &RegistryClient,
) -> Result<Option<RequestedRequirements>> {
// Determine whether the requirement represents a local distribution.
let Some(VersionOrUrl::Url(url)) = requirement.version_or_url.as_ref() else {
return Ok(None);
};

// Convert to a buildable distribution.
let dist = Dist::from_url(requirement.name.clone(), url.clone())?;

// Only support source trees (and not, e.g., wheels).
let Dist::Source(source_dist) = &dist else {
return Ok(None);
};
if !source_dist.as_path().is_some_and(std::path::Path::is_dir) {
return Ok(None);
}

// Run the PEP 517 build process to extract metadata from the source distribution.
let builder = if let Some(reporter) = self.reporter.clone() {
SourceDistCachedBuilder::new(context, client).with_reporter(reporter)
} else {
SourceDistCachedBuilder::new(context, client)
};

let metadata = builder
.download_and_build_metadata(&BuildableSource::Dist(source_dist))
.await
.context("Failed to build source distribution")?;

// Return the requirements from the metadata.
Ok(Some(RequestedRequirements::new(
requirement.extras.clone(),
metadata.requires_dist,
)))
}
}
29 changes: 29 additions & 0 deletions crates/uv-resolver/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,44 @@ use distribution_types::LocalEditable;
use pep508_rs::Requirement;
use pypi_types::Metadata23;
use uv_normalize::PackageName;
use uv_types::RequestedRequirements;

use crate::preferences::Preference;

/// A manifest of requirements, constraints, and preferences.
#[derive(Clone, Debug)]
pub struct Manifest {
/// The direct requirements for the project.
pub(crate) requirements: Vec<Requirement>,

/// The constraints for the project.
pub(crate) constraints: Vec<Requirement>,

/// The overrides for the project.
pub(crate) overrides: Vec<Requirement>,

/// The preferences for the project.
///
/// These represent "preferred" versions of a given package. For example, they may be the
/// versions that are already installed in the environment, or already pinned in an existing
/// lockfile.
pub(crate) preferences: Vec<Preference>,

/// The name of the project.
pub(crate) project: Option<PackageName>,

/// The editable requirements for the project, which are built in advance.
///
/// The requirements of the editables should be included in resolution as if they were
/// direct requirements in their own right.
pub(crate) editables: Vec<(LocalEditable, Metadata23)>,

/// The lookahead requirements for the project.
///
/// These represent transitive dependencies that should be incorporated when making
/// determinations around "allowed" versions (for example, "allowed" URLs or "allowed"
/// pre-release versions).
pub(crate) lookaheads: Vec<RequestedRequirements>,
}

impl Manifest {
Expand All @@ -24,6 +50,7 @@ impl Manifest {
preferences: Vec<Preference>,
project: Option<PackageName>,
editables: Vec<(LocalEditable, Metadata23)>,
lookaheads: Vec<RequestedRequirements>,
) -> Self {
Self {
requirements,
Expand All @@ -32,6 +59,7 @@ impl Manifest {
preferences,
project,
editables,
lookaheads,
}
}

Expand All @@ -43,6 +71,7 @@ impl Manifest {
preferences: Vec::new(),
project: None,
editables: Vec::new(),
lookaheads: Vec::new(),
}
}
}
10 changes: 10 additions & 0 deletions crates/uv-resolver/src/prerelease_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ impl PreReleaseStrategy {
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
Expand Down Expand Up @@ -95,6 +100,11 @@ impl PreReleaseStrategy {
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
Expand Down
7 changes: 6 additions & 1 deletion crates/uv-resolver/src/resolution_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,16 @@ impl ResolutionStrategy {
ResolutionMode::Highest => Self::Highest,
ResolutionMode::Lowest => Self::Lowest,
ResolutionMode::LowestDirect => Self::LowestDirect(
// Consider `requirements` and dependencies of `editables` to be "direct" dependencies.
// Consider `requirements` and dependencies of any local requirements to be "direct" dependencies.
manifest
.requirements
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
Expand Down
39 changes: 17 additions & 22 deletions crates/uv-resolver/src/resolver/locals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,23 @@ impl Locals {

// Add all direct requirements and constraints. There's no need to look for conflicts,
// since conflicting versions will be tracked upstream.
for requirement in manifest
.requirements
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(
manifest
.constraints
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata
.requires_dist
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
}))
.chain(
manifest
.overrides
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
for requirement in
manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
})
}))
{
if let Some(version_or_url) = requirement.version_or_url.as_ref() {
for local in iter_locals(version_or_url) {
Expand Down
Loading

0 comments on commit cf30932

Please sign in to comment.