Add locked subgraph materialization primitives

This commit is contained in:
Zsolt Dollenstein
2026-06-18 10:00:44 +00:00
parent 16a9cec5da
commit d50d1ac2bd
6 changed files with 742 additions and 95 deletions
+586 -89
View File
@@ -54,6 +54,122 @@ pub trait Installable<'lock> {
groups: &DependencyGroupsWithDefaults,
build_options: &BuildOptions,
install_options: &InstallOptions,
) -> Result<Resolution, LockError> {
let roots = self
.roots()
.map(|root_name| {
self.lock()
.find_by_name(root_name)
.map_err(|_| LockErrorKind::MultipleRootPackages {
name: root_name.clone(),
})?
.ok_or_else(|| {
LockError::from(LockErrorKind::MissingRootPackage {
name: root_name.clone(),
})
})
})
.collect::<Result<Vec<_>, LockError>>()?;
InstallableExt::to_resolution_from_packages(
self,
&roots,
true,
marker_env,
tags,
extras,
groups,
build_options,
install_options,
)
}
/// Create an installable [`Node`] from a [`Package`].
fn installable_node(
&self,
package: &Package,
tags: &Tags,
marker_env: &ResolverMarkerEnvironment,
build_options: &BuildOptions,
) -> Result<Node, LockError> {
let tag_policy = TagPolicy::Required(tags);
let HashedDist { dist, hashes } =
package.to_dist(self.install_path(), tag_policy, build_options, marker_env)?;
let version = package.version().cloned();
let dist = ResolvedDist::Installable {
dist: Arc::new(dist),
version,
};
Ok(Node::Dist {
dist,
hashes,
install: true,
})
}
/// Create a non-installable [`Node`] from a [`Package`].
fn non_installable_node(
&self,
package: &Package,
tags: &Tags,
marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> {
let HashedDist { dist, .. } = package.to_dist(
self.install_path(),
TagPolicy::Preferred(tags),
&BuildOptions::default(),
marker_env,
)?;
let version = package.version().cloned();
let dist = ResolvedDist::Installable {
dist: Arc::new(dist),
version,
};
let hashes = package.hashes();
Ok(Node::Dist {
dist,
hashes,
install: false,
})
}
/// Convert a lockfile entry to a graph [`Node`].
fn package_to_node(
&self,
package: &Package,
tags: &Tags,
build_options: &BuildOptions,
install_options: &InstallOptions,
marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> {
if install_options.include_package(
package.as_install_target(),
self.project_name(),
self.lock().members(),
) {
self.installable_node(package, tags, marker_env, build_options)
} else {
self.non_installable_node(package, tags, marker_env)
}
}
}
/// Internal lock-to-resolution implementation shared by [`Installable`] and [`Lock`].
trait InstallableExt<'lock>: Installable<'lock> {
/// Convert concrete locked packages to a [`Resolution`].
///
/// `include_manifest` controls whether requirements attached directly to the lock target are
/// included in addition to `roots`.
fn to_resolution_from_packages(
&self,
roots: &[&Package],
include_manifest: bool,
marker_env: &ResolverMarkerEnvironment,
tags: &Tags,
extras: &ExtrasSpecificationWithDefaults,
groups: &DependencyGroupsWithDefaults,
build_options: &BuildOptions,
install_options: &InstallOptions,
) -> Result<Resolution, LockError> {
let size_guess = self.lock().packages.len();
let mut petgraph = Graph::with_capacity(size_guess, size_guess);
@@ -75,17 +191,7 @@ pub trait Installable<'lock> {
// computed below. We somehow need to add the dependency groups _after_ we've computed all
// enabled extras, but the groups themselves could depend on the set of enabled extras.
if !self.lock().conflicts().is_empty() {
for root_name in self.roots() {
let dist = self
.lock()
.find_by_name(root_name)
.map_err(|_| LockErrorKind::MultipleRootPackages {
name: root_name.clone(),
})?
.ok_or_else(|| LockErrorKind::MissingRootPackage {
name: root_name.clone(),
})?;
for dist in roots.iter().copied() {
// Track the activated extras.
if groups.prod() {
activated_projects.push(&dist.id.name);
@@ -106,18 +212,8 @@ pub trait Installable<'lock> {
}
// Initialize the workspace roots.
let mut roots = vec![];
for root_name in self.roots() {
let dist = self
.lock()
.find_by_name(root_name)
.map_err(|_| LockErrorKind::MultipleRootPackages {
name: root_name.clone(),
})?
.ok_or_else(|| LockErrorKind::MissingRootPackage {
name: root_name.clone(),
})?;
let mut initialized_roots = vec![];
for dist in roots.iter().copied() {
// Add the workspace package to the graph.
let index = petgraph.add_node(if groups.prod() {
self.package_to_node(dist, tags, build_options, install_options, marker_env)?
@@ -130,11 +226,11 @@ pub trait Installable<'lock> {
petgraph.add_edge(root, index, Edge::Prod);
// Push the package onto the queue.
roots.push((dist, index));
initialized_roots.push((dist, index));
}
// Add the workspace dependencies to the queue.
for (dist, index) in roots {
for (dist, index) in initialized_roots {
if groups.prod() {
// Push its dependencies onto the queue.
queue.push_back((dist, None));
@@ -227,7 +323,12 @@ pub trait Installable<'lock> {
// Add any requirements that are exclusive to the workspace root (e.g., dependencies in
// PEP 723 scripts).
for dependency in self.lock().requirements() {
for dependency in self
.lock()
.requirements()
.iter()
.filter(|_| include_manifest)
{
if !dependency.marker.evaluate(marker_env, &[]) {
continue;
}
@@ -271,6 +372,7 @@ pub trait Installable<'lock> {
.lock()
.dependency_groups()
.iter()
.filter(|_| include_manifest)
.filter_map(|(group, deps)| {
if groups.contains(group) {
Some(deps.iter().map(move |dep| (group, dep)))
@@ -530,73 +632,468 @@ pub trait Installable<'lock> {
Ok(Resolution::new(petgraph))
}
}
/// Create an installable [`Node`] from a [`Package`].
fn installable_node(
&self,
package: &Package,
tags: &Tags,
marker_env: &ResolverMarkerEnvironment,
build_options: &BuildOptions,
) -> Result<Node, LockError> {
let tag_policy = TagPolicy::Required(tags);
let HashedDist { dist, hashes } =
package.to_dist(self.install_path(), tag_policy, build_options, marker_env)?;
let version = package.version().cloned();
let dist = ResolvedDist::Installable {
dist: Arc::new(dist),
version,
};
Ok(Node::Dist {
dist,
hashes,
install: true,
})
impl<'lock, T> InstallableExt<'lock> for T where T: Installable<'lock> + ?Sized {}
/// An [`Installable`] adapter for materializing concrete packages directly from a [`Lock`].
struct LockedPackages<'lock> {
lock: &'lock Lock,
install_path: &'lock Path,
project_name: Option<&'lock PackageName>,
}
impl<'lock> Installable<'lock> for LockedPackages<'lock> {
fn install_path(&self) -> &'lock Path {
self.install_path
}
/// Create a non-installable [`Node`] from a [`Package`].
fn non_installable_node(
&self,
package: &Package,
tags: &Tags,
marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> {
let HashedDist { dist, .. } = package.to_dist(
self.install_path(),
TagPolicy::Preferred(tags),
&BuildOptions::default(),
marker_env,
)?;
let version = package.version().cloned();
let dist = ResolvedDist::Installable {
dist: Arc::new(dist),
version,
};
let hashes = package.hashes();
Ok(Node::Dist {
dist,
hashes,
install: false,
})
fn lock(&self) -> &'lock Lock {
self.lock
}
/// Convert a lockfile entry to a graph [`Node`].
fn package_to_node(
&self,
package: &Package,
tags: &Tags,
build_options: &BuildOptions,
install_options: &InstallOptions,
marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> {
if install_options.include_package(
package.as_install_target(),
self.project_name(),
self.lock().members(),
) {
self.installable_node(package, tags, marker_env, build_options)
} else {
self.non_installable_node(package, tags, marker_env)
}
fn roots(&self) -> impl Iterator<Item = &PackageName> {
std::iter::empty()
}
fn project_name(&self) -> Option<&PackageName> {
self.project_name
}
}
impl Lock {
/// Materialize the exact dependency subgraph reachable from concrete locked `roots`.
///
/// Each root must be a [`Package`] from this lock. Unlike [`Installable::to_resolution`], this
/// method does not include requirements or dependency groups attached directly to the lock
/// manifest. Extras and dependency groups on the concrete roots are still included according
/// to `extras` and `groups`. `project_name` identifies the project for project-specific
/// [`InstallOptions`] filters, if applicable. Callers are responsible for selecting roots that
/// apply to `marker_env`.
pub fn to_resolution<'lock>(
&'lock self,
install_path: &'lock Path,
roots: impl IntoIterator<Item = &'lock Package>,
project_name: Option<&'lock PackageName>,
marker_env: &ResolverMarkerEnvironment,
tags: &Tags,
extras: &ExtrasSpecificationWithDefaults,
groups: &DependencyGroupsWithDefaults,
build_options: &BuildOptions,
install_options: &InstallOptions,
) -> Result<Resolution, LockError> {
let mut seen = FxHashSet::default();
let mut concrete_roots = Vec::new();
for root in roots {
let Some(index) = self.by_id.get(&root.id) else {
return Err(LockErrorKind::RootPackageMissingFromLock {
id: root.id.clone(),
}
.into());
};
if seen.insert(&root.id) {
let Some(root) = self.packages.get(*index) else {
return Err(LockErrorKind::RootPackageMissingFromLock {
id: root.id.clone(),
}
.into());
};
concrete_roots.push(root);
}
}
LockedPackages {
lock: self,
install_path,
project_name,
}
.to_resolution_from_packages(
&concrete_roots,
false,
marker_env,
tags,
extras,
groups,
build_options,
install_options,
)
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::sync::LazyLock;
use petgraph::visit::EdgeRef;
use uv_configuration::{DependencyGroups, ExtrasSpecification};
use uv_distribution_types::Name;
use uv_normalize::{DefaultExtras, DefaultGroups};
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
use uv_platform_tags::{Arch, Os, Platform, TagsOptions};
use super::*;
static TAGS: LazyLock<Tags> = LazyLock::new(|| {
Tags::from_env(
&Platform::new(
Os::Macos {
major: 14,
minor: 0,
},
Arch::Aarch64,
),
(3, 11),
"cpython",
(3, 11),
TagsOptions::default(),
)
.expect("valid tags")
});
static DARWIN_MARKERS: LazyLock<ResolverMarkerEnvironment> =
LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("darwin", "Darwin")));
static LINUX_MARKERS: LazyLock<ResolverMarkerEnvironment> =
LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("linux", "Linux")));
fn marker_environment(
sys_platform: &'static str,
platform_system: &'static str,
) -> MarkerEnvironment {
MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
implementation_name: "cpython",
implementation_version: "3.11.5",
os_name: "posix",
platform_machine: "arm64",
platform_python_implementation: "CPython",
platform_release: "23.0.0",
platform_system,
platform_version: "test",
python_full_version: "3.11.5",
python_version: "3.11",
sys_platform,
})
.expect("valid marker environment")
}
fn lock() -> Lock {
toml::from_str(
r#"
version = 1
revision = 3
requires-python = ">=3.11"
resolution-markers = [
"sys_platform == 'darwin'",
"sys_platform != 'darwin'",
]
[manifest]
requirements = [{ name = "unrelated" }]
[[package]]
name = "dev-dependency"
version = "1.0.0"
source = { registry = "https://example.com/simple" }
sdist = { url = "https://example.com/dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
[[package]]
name = "forked"
version = "1.0.0"
source = { registry = "https://example.com/simple" }
resolution-markers = ["sys_platform == 'darwin'"]
sdist = { url = "https://example.com/forked-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
[[package]]
name = "forked"
version = "2.0.0"
source = { registry = "https://example.com/simple" }
resolution-markers = ["sys_platform != 'darwin'"]
sdist = { url = "https://example.com/forked-2.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
[[package]]
name = "optional-dependency"
version = "1.0.0"
source = { registry = "https://example.com/simple" }
sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
[[package]]
name = "root-a"
version = "1.0.0"
source = { registry = "https://example.com/simple" }
dependencies = [
{ name = "forked", version = "1.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform == 'darwin'" },
{ name = "forked", version = "2.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform != 'darwin'" },
{ name = "shared" },
]
sdist = { url = "https://example.com/root_a-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
[package.optional-dependencies]
feature = [{ name = "optional-dependency" }]
[package.dependency-groups]
dev = [{ name = "dev-dependency" }]
[package.metadata]
provides-extras = ["feature"]
[[package]]
name = "root-b"
version = "1.0.0"
source = { registry = "https://example.com/simple" }
dependencies = [{ name = "shared" }]
sdist = { url = "https://example.com/root_b-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
[[package]]
name = "shared"
version = "1.0.0"
source = { registry = "https://example.com/simple" }
sdist = { url = "https://example.com/shared-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
[[package]]
name = "unrelated"
version = "1.0.0"
source = { registry = "https://example.com/simple" }
sdist = { url = "https://example.com/unrelated-1.0.0.tar.gz", hash = "sha256:8888888888888888888888888888888888888888888888888888888888888888" }
"#,
)
.expect("valid lock")
}
fn package<'lock>(lock: &'lock Lock, name: &str, version: &str) -> &'lock Package {
lock.packages()
.iter()
.find(|package| {
package.name().as_ref() == name
&& package
.version()
.is_some_and(|package_version| package_version.to_string() == version)
})
.expect("locked package")
}
fn materialize(
lock: &Lock,
roots: &[&Package],
marker_env: &ResolverMarkerEnvironment,
) -> Resolution {
let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
lock.to_resolution(
Path::new("."),
roots.iter().copied(),
None,
marker_env,
&TAGS,
&extras,
&groups,
&BuildOptions::default(),
&InstallOptions::default(),
)
.expect("valid resolution")
}
struct OverridingInstallable<'lock> {
lock: &'lock Lock,
root_name: &'lock PackageName,
package_to_node_calls: Cell<usize>,
}
impl<'lock> Installable<'lock> for OverridingInstallable<'lock> {
fn install_path(&self) -> &'lock Path {
Path::new(".")
}
fn lock(&self) -> &'lock Lock {
self.lock
}
fn roots(&self) -> impl Iterator<Item = &PackageName> {
std::iter::once(self.root_name)
}
fn project_name(&self) -> Option<&PackageName> {
None
}
fn package_to_node(
&self,
_package: &Package,
_tags: &Tags,
_build_options: &BuildOptions,
_install_options: &InstallOptions,
_marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> {
self.package_to_node_calls
.set(self.package_to_node_calls.get() + 1);
Ok(Node::Root)
}
}
fn graph_snapshot(resolution: &Resolution) -> (Vec<String>, Vec<String>) {
let graph = resolution.graph();
let labels = graph
.node_weights()
.map(|node| match node {
Node::Root => "root".to_string(),
Node::Dist {
dist,
hashes,
install,
} => format!(
"{}=={} (install: {install}, hashes: {})",
dist.name(),
dist.version()
.map(ToString::to_string)
.unwrap_or_else(|| "<dynamic>".to_string()),
hashes.iter().map(ToString::to_string).join(", ")
),
})
.collect::<Vec<_>>();
let mut nodes = labels.clone();
nodes.sort_unstable();
let mut edges = graph
.edge_references()
.map(|edge| {
format!(
"{} --{:?}--> {}",
labels[edge.source().index()],
edge.weight(),
labels[edge.target().index()]
)
})
.collect::<Vec<_>>();
edges.sort_unstable();
(nodes, edges)
}
#[test]
fn materializes_multiple_concrete_roots_with_shared_dependencies() {
let lock = lock();
let resolution = materialize(
&lock,
&[
package(&lock, "root-a", "1.0.0"),
package(&lock, "root-b", "1.0.0"),
],
&DARWIN_MARKERS,
);
insta::with_settings!({
filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
}, {
insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
(
[
"dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"forked==1.0.0 (install: true, hashes: sha256:[HASH])",
"optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
"shared==1.0.0 (install: true, hashes: sha256:[HASH])",
],
[
"root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
"root --Prod--> root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-b==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
],
)
"#);
});
}
#[test]
fn materializes_the_selected_universal_lock_fork() {
let lock = lock();
let root = package(&lock, "root-a", "1.0.0");
let darwin = materialize(&lock, &[root], &DARWIN_MARKERS);
let linux = materialize(&lock, &[root], &LINUX_MARKERS);
let concrete_fork =
materialize(&lock, &[package(&lock, "forked", "1.0.0")], &DARWIN_MARKERS);
insta::with_settings!({
filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
}, {
insta::assert_debug_snapshot!(graph_snapshot(&darwin), @r#"
(
[
"dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"forked==1.0.0 (install: true, hashes: sha256:[HASH])",
"optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
"shared==1.0.0 (install: true, hashes: sha256:[HASH])",
],
[
"root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
],
)
"#);
insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
(
[
"dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"forked==2.0.0 (install: true, hashes: sha256:[HASH])",
"optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
"shared==1.0.0 (install: true, hashes: sha256:[HASH])",
],
[
"root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==2.0.0 (install: true, hashes: sha256:[HASH])",
"root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
],
)
"#);
insta::assert_debug_snapshot!(graph_snapshot(&concrete_fork), @r#"
(
[
"forked==1.0.0 (install: true, hashes: sha256:[HASH])",
"root",
],
[
"root --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
],
)
"#);
});
}
#[test]
fn installable_to_resolution_preserves_node_overrides() {
let mut lock = lock();
lock.manifest.requirements.clear();
let target = OverridingInstallable {
root_name: package(&lock, "root-a", "1.0.0").name(),
lock: &lock,
package_to_node_calls: Cell::new(0),
};
let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
target
.to_resolution(
&DARWIN_MARKERS,
&TAGS,
&extras,
&groups,
&BuildOptions::default(),
&InstallOptions::default(),
)
.expect("valid resolution");
assert!(target.package_to_node_calls.get() > 0);
}
}
+6
View File
@@ -6611,6 +6611,12 @@ enum LockErrorKind {
/// The ID of the package.
name: PackageName,
},
/// An error that occurs when a concrete root package does not belong to the lock.
#[error("Could not find root package `{id}` in lock", id = id.cyan())]
RootPackageMissingFromLock {
/// The ID of the package.
id: PackageId,
},
/// An error that occurs when resolving metadata for a package.
#[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
Resolution {
+43 -2
View File
@@ -157,6 +157,47 @@ impl CachedEnvironment {
.await?,
);
Self::from_resolution(
&resolution,
build_constraints,
&interpreter,
settings,
client_builder,
state,
install,
installer_metadata,
concurrency,
cache,
printer,
preview,
)
.await
}
/// Get or create a [`CachedEnvironment`] from an existing [`Resolution`].
///
/// Prefer [`Self::from_spec`] when starting from unresolved requirements; it selects the base
/// interpreter and resolves the requirements for that interpreter before delegating here.
///
/// This method is intended for callers that already have a concrete [`Resolution`], and
/// performs environment reuse or creation and installation without invoking the resolver.
/// `interpreter` must be the base interpreter for which `resolution` was produced. In
/// particular, callers materializing a universal lock must derive its markers and tags from
/// the same interpreter.
pub(crate) async fn from_resolution(
resolution: &Resolution,
build_constraints: Constraints,
interpreter: &Interpreter,
settings: &ResolverInstallerSettings,
client_builder: &BaseClientBuilder<'_>,
state: &PlatformState,
install: Box<dyn InstallLogger>,
installer_metadata: bool,
concurrency: &Concurrency,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<Self, ProjectError> {
// Hash the resolution by hashing the generated lockfile.
let resolution_hash = {
let mut distributions = resolution
@@ -216,7 +257,7 @@ impl CachedEnvironment {
let temp_dir = cache.venv_dir()?;
let venv = uv_virtualenv::create_venv(
temp_dir.path(),
interpreter,
interpreter.clone(),
uv_virtualenv::Prompt::None,
false,
uv_virtualenv::OnExisting::Remove(uv_virtualenv::RemovalReason::TemporaryEnvironment),
@@ -227,7 +268,7 @@ impl CachedEnvironment {
sync_environment(
venv,
&resolution,
resolution,
Modifications::Exact,
build_constraints,
settings.into(),
@@ -6,11 +6,17 @@ use std::str::FromStr;
use itertools::Either;
use rustc_hash::FxHashSet;
use uv_configuration::{Constraints, DependencyGroupsWithDefaults, ExtrasSpecification};
use uv_distribution_types::Index;
use uv_configuration::{
BuildOptions, Constraints, DependencyGroupsWithDefaults, ExtrasSpecification,
ExtrasSpecificationWithDefaults, InstallOptions,
};
use uv_distribution_types::{Index, Resolution};
use uv_normalize::{ExtraName, PackageName};
use uv_pypi_types::{DependencyGroupSpecifier, LenientRequirement, VerbatimParsedUrl};
use uv_resolver::{Installable, Lock, Package};
use uv_platform_tags::Tags;
use uv_pypi_types::{
DependencyGroupSpecifier, LenientRequirement, ResolverMarkerEnvironment, VerbatimParsedUrl,
};
use uv_resolver::{Installable, Lock, LockError, Package};
use uv_scripts::Pep723Script;
use uv_workspace::Workspace;
use uv_workspace::pyproject::{Source, Sources, ToolUvSources};
@@ -113,6 +119,56 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
}
impl<'lock> InstallTarget<'lock> {
/// Convert the target's locked packages to a [`Resolution`].
pub(crate) fn to_resolution(
self,
marker_env: &ResolverMarkerEnvironment,
tags: &Tags,
extras: &ExtrasSpecificationWithDefaults,
groups: &DependencyGroupsWithDefaults,
build_options: &BuildOptions,
install_options: &InstallOptions,
) -> Result<Resolution, LockError> {
// Project and workspace targets have package-backed roots, so materialize them through the
// concrete-root API. Non-project workspaces can also have package roots, but their root
// dependencies live on the lock manifest, so they need the generic path. Scripts and
// invalid or ambiguous roots also use that path.
let use_concrete_roots = match self {
Self::Project { workspace, .. }
| Self::Projects { workspace, .. }
| Self::Workspace { workspace, .. } => !workspace.is_non_project(),
Self::NonProjectWorkspace { .. } | Self::Script { .. } => false,
};
if use_concrete_roots
&& let Some(roots) = self
.roots()
.map(|root_name| self.lock().find_by_name(root_name).ok().flatten())
.collect::<Option<Vec<_>>>()
{
return self.lock().to_resolution(
self.install_path(),
roots,
self.project_name(),
marker_env,
tags,
extras,
groups,
build_options,
install_options,
);
}
Installable::to_resolution(
&self,
marker_env,
tags,
extras,
groups,
build_options,
install_options,
)
}
/// Return an iterator over the [`Index`] definitions in the target.
pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> {
match self {
+11
View File
@@ -1353,6 +1353,17 @@ fn sync_non_project_dev_dependencies() -> Result<()> {
+ urllib3==2.2.1
");
// Selecting a member still includes the non-project root's default dependency group.
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child"), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 11 packages in [TIME]
Checked 10 packages in [TIME]
");
Ok(())
}
@@ -1724,6 +1724,42 @@ fn workspace_metadata_group_only() -> Result<()> {
"#
);
// With `--sync`, modules provided by the non-project root's dependency group should be
// attributed to their locked package.
let assert = context
.workspace_metadata()
.arg("--sync")
.current_dir(&workspace)
.assert()
.success();
let metadata: serde_json::Value = serde_json::from_slice(&assert.get_output().stdout)?;
let module_owners = serde_json::to_string_pretty(&metadata["module_owners"])?;
insta::assert_snapshot!(module_owners, @r#"
{
"iniconfig": [
{
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
}
],
"iniconfig._parse": [
{
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
}
],
"iniconfig._version": [
{
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
}
],
"iniconfig.exceptions": [
{
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
}
]
}
"#);
Ok(())
}