Collect path dependency indexes separately

This commit is contained in:
John Mumm 2025-07-24 14:13:00 +02:00
parent 9e5c16d833
commit ec0b75bcfc
No known key found for this signature in database
GPG Key ID: 73D2271AFDC26EA8
4 changed files with 139 additions and 36 deletions

View File

@ -1233,7 +1233,8 @@ impl Lock {
build_constraints: &[Requirement],
dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
dependency_metadata: &DependencyMetadata,
indexes: Option<Cow<'_, IndexLocations>>,
indexes: Option<&IndexLocations>,
path_dependency_indexes: &BTreeSet<UrlString>,
tags: &Tags,
hasher: &HashStrategy,
index: &InMemoryIndex,
@ -1399,7 +1400,7 @@ impl Lock {
}
// Collect the set of available indexes (both `--index-url` and `--find-links` entries).
let remotes = indexes.as_ref().map(|locations| {
let remotes = indexes.map(|locations| {
locations
.allowed_indexes()
.into_iter()
@ -1412,7 +1413,7 @@ impl Lock {
.collect::<BTreeSet<_>>()
});
let locals = indexes.as_ref().map(|locations| {
let locals = indexes.map(|locations| {
locations
.allowed_indexes()
.into_iter()
@ -1452,10 +1453,9 @@ impl Lock {
if let Source::Registry(index) = &package.id.source {
match index {
RegistrySource::Url(url) => {
if remotes
.as_ref()
.is_some_and(|remotes| !remotes.contains(url))
{
if remotes.as_ref().is_some_and(|remotes| {
!remotes.contains(url) && !path_dependency_indexes.contains(url)
}) {
let name = &package.id.name;
let version = &package
.id

View File

@ -1,6 +1,7 @@
//! Resolve the current [`ProjectWorkspace`] or [`Workspace`].
use std::collections::{BTreeMap, BTreeSet};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
@ -939,10 +940,13 @@ impl Workspace {
// We will only add indexes if we have not already seen the URLs.
let known_urls: FxHashSet<_> = self.indexes.iter().map(Index::url).collect();
let mut pyprojects = std::collections::VecDeque::new();
pyprojects.push_back((self.install_path.clone(), self.pyproject_toml.clone()));
let mut pyproject_queue = VecDeque::new();
for package in self.packages.values() {
pyproject_queue
.push_back((package.root.clone(), Cow::Borrowed(&package.pyproject_toml)));
}
while let Some((base_path, pyproject)) = pyprojects.pop_front() {
while let Some((base_path, pyproject)) = pyproject_queue.pop_front() {
if let Some(tool_uv_sources) = pyproject
.tool
.as_ref()
@ -975,8 +979,8 @@ impl Workspace {
let dep_pyproject_path = canonical_path.join("pyproject.toml");
match pyproject_toml_from_path(dep_pyproject_path.clone()) {
Ok(dep_pyproject) => {
if let Some(dep_indexes) = dep_pyproject
Ok(pyproject_toml) => {
if let Some(dep_indexes) = pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
@ -990,7 +994,8 @@ impl Workspace {
);
}
pyprojects.push_back((canonical_path, dep_pyproject));
pyproject_queue
.push_back((canonical_path, Cow::Owned(pyproject_toml)));
}
Err(e) => {
debug!(

View File

@ -1,6 +1,5 @@
#![allow(clippy::single_match_else)]
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use std::path::Path;
@ -18,8 +17,8 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_distribution_types::{
DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification,
Requirement, UnresolvedRequirementSpecification,
DependencyMetadata, HashGeneration, Index, IndexLocations, IndexUrl,
NameRequirementSpecification, Requirement, UnresolvedRequirementSpecification, UrlString,
};
use uv_git::ResolvedRepositoryReference;
use uv_normalize::{GroupName, PackageName};
@ -1067,26 +1066,26 @@ impl ValidatedLock {
// However, if _no_ indexes were provided, we assume that the user wants to reuse the existing
// distributions, even though a failure to reuse the lockfile will result in re-resolving
// against PyPI by default.
let validation_indexes = if index_locations.is_none() {
let indexes = if index_locations.is_none() {
None
} else {
// If indexes were defined as sources in path dependencies, add them to the
// index locations to use for validation.
if let LockTarget::Workspace(workspace) = target {
let path_dependency_source_indexes =
workspace.collect_path_dependency_source_indexes();
if path_dependency_source_indexes.is_empty() {
Some(Cow::Borrowed(index_locations))
} else {
Some(Cow::Owned(index_locations.clone().combine(
path_dependency_source_indexes,
Vec::new(),
false,
)))
Some(index_locations)
};
// Collect indexes specified in path dependencies
let path_dependency_indexes = if let LockTarget::Workspace(workspace) = target {
workspace
.collect_path_dependency_source_indexes()
.into_iter()
.filter_map(|index| match index.url() {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
Some(UrlString::from(index.url().without_credentials().as_ref()))
}
IndexUrl::Path(_) => None,
})
.collect::<BTreeSet<_>>()
} else {
Some(Cow::Borrowed(index_locations))
}
BTreeSet::default()
};
// Determine whether the lockfile satisfies the workspace requirements.
@ -1101,7 +1100,8 @@ impl ValidatedLock {
build_constraints,
dependency_groups,
dependency_metadata,
validation_indexes,
indexes,
&path_dependency_indexes,
interpreter.tags()?,
hasher,
index,

View File

@ -27634,6 +27634,104 @@ fn lock_path_dependency_explicit_index() -> Result<()> {
Ok(())
}
/// Test that lockfile validation includes explicit indexes from path dependencies
/// defined in a non-root workspace member.
#[test]
fn lock_path_dependency_explicit_index_workspace_member() -> Result<()> {
let context = TestContext::new("3.12");
// Create the path dependency with explicit index
let pkg_a = context.temp_dir.child("pkg_a");
fs_err::create_dir_all(&pkg_a)?;
let pyproject_toml = pkg_a.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "pkg-a"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
[tool.uv.sources]
iniconfig = { index = "inner-index" }
[[tool.uv.index]]
name = "inner-index"
url = "https://pypi-proxy.fly.dev/simple"
explicit = true
"#,
)?;
// Create a project that depends on pkg_a
let member = context.temp_dir.child("member");
fs_err::create_dir_all(&member)?;
let pyproject_toml = member.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "member"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["pkg-a"]
[tool.uv.sources]
pkg-a = { path = "../pkg_a/", editable = true }
black = { index = "middle-index" }
[[tool.uv.index]]
name = "middle-index"
url = "https://middle-index.com/simple"
explicit = true
"#,
)?;
// Create a root with workspace member
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "root-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["member"]
[tool.uv.workspace]
members = ["member"]
[tool.uv.sources]
member = { workspace = true }
anyio = { index = "outer-index" }
[[tool.uv.index]]
name = "outer-index"
url = "https://outer-index.com/simple"
explicit = true
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
");
uv_snapshot!(context.filters(), context.lock().arg("--check"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
");
Ok(())
}
/// Test that lockfile validation works correctly when path dependency has
/// both explicit and non-explicit indexes.
#[test]