mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-01 06:21:13 +00:00
Recursively allow URL requirements for local dependencies (#2702)
## Summary This is a trimmed-down version of https://github.com/astral-sh/uv/pull/2684 that only applies to local source trees for now, which enables workspace-like workflows (whereby local packages can depend on other local packages at arbitrary depth). Closes #2699. ## Test Plan Added new tests. Also cloned this MRE that was shared with me (https://github.com/timothyjlaurent/uv-poetry-monorepo-mre), and verified that it was installed without error: ``` ❯ cargo run pip install ./uv-poetry-monorepo-mre/app --no-cache Finished dev [unoptimized + debuginfo] target(s) in 0.15s Running `target/debug/uv pip install ./uv-poetry-monorepo-mre/app --no-cache` Resolved 4 packages in 1.28s Built app @ file:///Users/crmarsh/workspace/uv/uv-poetry-monorepo-mre/app Built lib1 @ file:///Users/crmarsh/workspace/uv/uv-poetry-monorepo-mre/lib1 Built lib2 @ file:///Users/crmarsh/workspace/uv/uv-poetry-monorepo-mre/lib2 Downloaded 4 packages in 457ms Installed 4 packages in 2ms + app==0.1.0 (from file:///Users/crmarsh/workspace/uv/uv-poetry-monorepo-mre/app) + lib1==0.1.0 (from file:///Users/crmarsh/workspace/uv/uv-poetry-monorepo-mre/lib1) + lib2==0.1.0 (from file:///Users/crmarsh/workspace/uv/uv-poetry-monorepo-mre/lib2) + ruff==0.3.4 ```
This commit is contained in:
parent
ce5df77ecb
commit
1f31350d46
4 changed files with 161 additions and 33 deletions
|
@ -1,10 +1,12 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::stream::FuturesUnordered;
|
||||||
|
use futures::StreamExt;
|
||||||
|
|
||||||
use distribution_types::{BuildableSource, Dist};
|
use distribution_types::{BuildableSource, Dist};
|
||||||
use pep508_rs::{Requirement, VersionOrUrl};
|
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
|
||||||
use uv_client::RegistryClient;
|
use uv_client::RegistryClient;
|
||||||
use uv_distribution::{Reporter, SourceDistCachedBuilder};
|
use uv_distribution::{Reporter, SourceDistCachedBuilder};
|
||||||
use uv_types::{BuildContext, RequestedRequirements};
|
use uv_types::{BuildContext, RequestedRequirements};
|
||||||
|
@ -20,16 +22,16 @@ use uv_types::{BuildContext, RequestedRequirements};
|
||||||
///
|
///
|
||||||
/// The lookahead resolver resolves requirements for local dependencies, so that the resolver can
|
/// 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.
|
/// treat them as first-party dependencies for the purpose of analyzing their specifiers.
|
||||||
pub struct LookaheadResolver<'a> {
|
pub struct LookaheadResolver {
|
||||||
/// The requirements for the project.
|
/// The requirements for the project.
|
||||||
requirements: &'a [Requirement],
|
requirements: Vec<Requirement>,
|
||||||
/// The reporter to use when building source distributions.
|
/// The reporter to use when building source distributions.
|
||||||
reporter: Option<Arc<dyn Reporter>>,
|
reporter: Option<Arc<dyn Reporter>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LookaheadResolver<'a> {
|
impl LookaheadResolver {
|
||||||
/// Instantiate a new [`LookaheadResolver`] for a given set of `source_trees`.
|
/// Instantiate a new [`LookaheadResolver`] for a given set of requirements.
|
||||||
pub fn new(requirements: &'a [Requirement]) -> Self {
|
pub fn new(requirements: Vec<Requirement>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
requirements,
|
requirements,
|
||||||
reporter: None,
|
reporter: None,
|
||||||
|
@ -50,20 +52,37 @@ impl<'a> LookaheadResolver<'a> {
|
||||||
pub async fn resolve<T: BuildContext>(
|
pub async fn resolve<T: BuildContext>(
|
||||||
self,
|
self,
|
||||||
context: &T,
|
context: &T,
|
||||||
|
markers: &MarkerEnvironment,
|
||||||
client: &RegistryClient,
|
client: &RegistryClient,
|
||||||
) -> Result<Vec<RequestedRequirements>> {
|
) -> Result<Vec<RequestedRequirements>> {
|
||||||
let requirements: Vec<_> = futures::stream::iter(self.requirements.iter())
|
let mut queue = VecDeque::from(self.requirements.clone());
|
||||||
.map(|requirement| async { self.lookahead(requirement, context, client).await })
|
let mut results = Vec::new();
|
||||||
.buffered(50)
|
let mut futures = FuturesUnordered::new();
|
||||||
.try_collect()
|
|
||||||
.await?;
|
while !queue.is_empty() || !futures.is_empty() {
|
||||||
Ok(requirements.into_iter().flatten().collect())
|
while let Some(requirement) = queue.pop_front() {
|
||||||
|
futures.push(self.lookahead(requirement, context, client));
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(result) = futures.next().await {
|
||||||
|
if let Some(lookahead) = result? {
|
||||||
|
for requirement in lookahead.requirements() {
|
||||||
|
if requirement.evaluate_markers(markers, lookahead.extras()) {
|
||||||
|
queue.push_back(requirement.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.push(lookahead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Infer the package name for a given "unnamed" requirement.
|
/// Infer the package name for a given "unnamed" requirement.
|
||||||
async fn lookahead<T: BuildContext>(
|
async fn lookahead<T: BuildContext>(
|
||||||
&self,
|
&self,
|
||||||
requirement: &Requirement,
|
requirement: Requirement,
|
||||||
context: &T,
|
context: &T,
|
||||||
client: &RegistryClient,
|
client: &RegistryClient,
|
||||||
) -> Result<Option<RequestedRequirements>> {
|
) -> Result<Option<RequestedRequirements>> {
|
||||||
|
@ -73,7 +92,7 @@ impl<'a> LookaheadResolver<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert to a buildable distribution.
|
// Convert to a buildable distribution.
|
||||||
let dist = Dist::from_url(requirement.name.clone(), url.clone())?;
|
let dist = Dist::from_url(requirement.name, url.clone())?;
|
||||||
|
|
||||||
// Only support source trees (and not, e.g., wheels).
|
// Only support source trees (and not, e.g., wheels).
|
||||||
let Dist::Source(source_dist) = &dist else {
|
let Dist::Source(source_dist) = &dist else {
|
||||||
|
@ -92,12 +111,11 @@ impl<'a> LookaheadResolver<'a> {
|
||||||
|
|
||||||
let metadata = builder
|
let metadata = builder
|
||||||
.download_and_build_metadata(&BuildableSource::Dist(source_dist))
|
.download_and_build_metadata(&BuildableSource::Dist(source_dist))
|
||||||
.await
|
.await?;
|
||||||
.context("Failed to build source distribution")?;
|
|
||||||
|
|
||||||
// Return the requirements from the metadata.
|
// Return the requirements from the metadata.
|
||||||
Ok(Some(RequestedRequirements::new(
|
Ok(Some(RequestedRequirements::new(
|
||||||
requirement.extras.clone(),
|
requirement.extras,
|
||||||
metadata.requires_dist,
|
metadata.requires_dist,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,14 +277,8 @@ pub(crate) async fn pip_compile(
|
||||||
requirements
|
requirements
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine any lookahead requirements.
|
|
||||||
let lookaheads = LookaheadResolver::new(&requirements)
|
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
|
||||||
.resolve(&build_dispatch, &client)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Build the editables and add their requirements
|
// Build the editables and add their requirements
|
||||||
let editable_metadata = if editables.is_empty() {
|
let editables = if editables.is_empty() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
@ -339,6 +333,24 @@ pub(crate) async fn pip_compile(
|
||||||
editables
|
editables
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determine any lookahead requirements.
|
||||||
|
let lookaheads = LookaheadResolver::new(
|
||||||
|
requirements
|
||||||
|
.iter()
|
||||||
|
.filter(|requirement| requirement.evaluate_markers(&markers, &[]))
|
||||||
|
.chain(editables.iter().flat_map(|(editable, metadata)| {
|
||||||
|
metadata
|
||||||
|
.requires_dist
|
||||||
|
.iter()
|
||||||
|
.filter(|requirement| requirement.evaluate_markers(&markers, &editable.extras))
|
||||||
|
}))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
|
.resolve(&build_dispatch, &markers, &client)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Create a manifest of the requirements.
|
// Create a manifest of the requirements.
|
||||||
let manifest = Manifest::new(
|
let manifest = Manifest::new(
|
||||||
requirements,
|
requirements,
|
||||||
|
@ -346,7 +358,7 @@ pub(crate) async fn pip_compile(
|
||||||
overrides,
|
overrides,
|
||||||
preferences,
|
preferences,
|
||||||
project,
|
project,
|
||||||
editable_metadata,
|
editables,
|
||||||
// Do not consider any installed packages during compilation
|
// Do not consider any installed packages during compilation
|
||||||
Exclusions::All,
|
Exclusions::All,
|
||||||
lookaheads,
|
lookaheads,
|
||||||
|
|
|
@ -16,7 +16,7 @@ use distribution_types::{
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
use pep508_rs::{MarkerEnvironment, Requirement};
|
use pep508_rs::{MarkerEnvironment, Requirement};
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use pypi_types::Yanked;
|
use pypi_types::{Metadata23, Yanked};
|
||||||
use requirements_txt::EditableRequirement;
|
use requirements_txt::EditableRequirement;
|
||||||
use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE};
|
use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE};
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
|
@ -524,7 +524,7 @@ async fn resolve(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Map the editables to their metadata.
|
// Map the editables to their metadata.
|
||||||
let editables = editables
|
let editables: Vec<(LocalEditable, Metadata23)> = editables
|
||||||
.iter()
|
.iter()
|
||||||
.map(|built_editable| {
|
.map(|built_editable| {
|
||||||
(
|
(
|
||||||
|
@ -535,10 +535,22 @@ async fn resolve(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Determine any lookahead requirements.
|
// Determine any lookahead requirements.
|
||||||
let lookaheads = LookaheadResolver::new(&requirements)
|
let lookaheads = LookaheadResolver::new(
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
requirements
|
||||||
.resolve(build_dispatch, client)
|
.iter()
|
||||||
.await?;
|
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
|
||||||
|
.chain(editables.iter().flat_map(|(editable, metadata)| {
|
||||||
|
metadata
|
||||||
|
.requires_dist
|
||||||
|
.iter()
|
||||||
|
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
|
||||||
|
}))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
|
.resolve(build_dispatch, markers, client)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Create a manifest of the requirements.
|
// Create a manifest of the requirements.
|
||||||
let manifest = Manifest::new(
|
let manifest = Manifest::new(
|
||||||
|
|
|
@ -6542,6 +6542,92 @@ fn pendulum_no_tzdata_on_windows() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allow URL dependencies recursively for local source trees.
|
||||||
|
#[test]
|
||||||
|
fn allow_recursive_url_local_path() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Create a standalone library.
|
||||||
|
let lib2 = context.temp_dir.child("lib2");
|
||||||
|
lib2.create_dir_all()?;
|
||||||
|
let pyproject_toml = lib2.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"[project]
|
||||||
|
name = "lib2"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"idna"
|
||||||
|
]
|
||||||
|
requires-python = ">3.8"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create a library that depends on the standalone library.
|
||||||
|
let lib1 = context.temp_dir.child("lib1");
|
||||||
|
lib1.create_dir_all()?;
|
||||||
|
let pyproject_toml = lib1.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(&format!(
|
||||||
|
r#"[project]
|
||||||
|
name = "lib1"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"lib2 @ {}"
|
||||||
|
]
|
||||||
|
requires-python = ">3.8"
|
||||||
|
"#,
|
||||||
|
Url::from_directory_path(lib2.path()).unwrap().as_str(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
// Create an application that depends on the library.
|
||||||
|
let app = context.temp_dir.child("app");
|
||||||
|
app.create_dir_all()?;
|
||||||
|
let pyproject_toml = app.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(&format!(
|
||||||
|
r#"[project]
|
||||||
|
name = "example"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyio",
|
||||||
|
"lib1 @ {}"
|
||||||
|
]
|
||||||
|
requires-python = ">3.8"
|
||||||
|
"#,
|
||||||
|
Url::from_directory_path(lib1.path()).unwrap().as_str(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
// Write to a requirements file.
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("./app")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.compile()
|
||||||
|
.arg("requirements.in"), @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
|
||||||
|
anyio==4.3.0
|
||||||
|
# via example
|
||||||
|
example @ ./app
|
||||||
|
idna==3.6
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# lib2
|
||||||
|
lib1 @ file://[TEMP_DIR]/lib1/
|
||||||
|
# via example
|
||||||
|
lib2 @ file://[TEMP_DIR]/lib2/
|
||||||
|
# via lib1
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Allow pre-releases for dependencies of source path requirements.
|
/// Allow pre-releases for dependencies of source path requirements.
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_release_path_requirement() -> Result<()> {
|
fn pre_release_path_requirement() -> Result<()> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue