Skip to content

Commit 854af2c

Browse files
committed
Add module owners to workspace metadata
1 parent 6e3cade commit 854af2c

10 files changed

Lines changed: 596 additions & 44 deletions

File tree

crates/uv-cli/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8028,6 +8028,13 @@ pub struct MetadataArgs {
80288028
#[command(flatten)]
80298029
pub refresh: RefreshArgs,
80308030

8031+
/// Include module ownership metadata in the output.
8032+
///
8033+
/// This adds a mapping from importable module names to the package names that provide
8034+
/// them. To do this, the venv will be synced in "inexact" mode.
8035+
#[arg(long)]
8036+
pub module_owners: bool,
8037+
80318038
/// The Python interpreter to use during resolution.
80328039
///
80338040
/// A Python interpreter is required for building source distributions to determine package

crates/uv-resolver/src/lock/export/metadata.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::collections::BTreeMap;
22
use std::fmt::Display;
33

4+
/// The name of an importable Python module.
5+
type ModuleName = String;
6+
47
use uv_distribution_filename::WheelFilename;
58
use uv_distribution_types::{RequiresPython, UrlString};
69
use uv_fs::PortablePathBuf;
@@ -66,6 +69,9 @@ pub struct Metadata {
6669
requires_python: RequiresPython,
6770
/// Info about conflicting packages
6871
conflicts: MetadataConflicts,
72+
/// A mapping from importable module names to the distributions that provide them
73+
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
74+
module_owners: BTreeMap<ModuleName, Vec<PackageName>>,
6975
/// An index of which nodes are workspace members
7076
///
7177
/// These entries are often what you should use as the entry-points into the `resolve` graph.
@@ -818,13 +824,23 @@ impl Metadata {
818824
version: SchemaVersion::Preview,
819825
},
820826
conflicts,
827+
module_owners: BTreeMap::new(),
821828
workspace_root,
822829
requires_python: lock.requires_python.clone(),
823830
members,
824831
resolution: resolve,
825832
})
826833
}
827834

835+
#[must_use]
836+
pub fn with_module_owners(
837+
mut self,
838+
module_owners: BTreeMap<ModuleName, Vec<PackageName>>,
839+
) -> Self {
840+
self.module_owners = module_owners;
841+
self
842+
}
843+
828844
pub fn to_json(&self) -> Result<String, MetadataError> {
829845
Ok(serde_json::to_string_pretty(self)?)
830846
}

crates/uv/src/commands/project/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub(crate) mod environment;
6868
pub(crate) mod export;
6969
pub(crate) mod format;
7070
pub(crate) mod init;
71-
mod install_target;
71+
pub(crate) mod install_target;
7272
pub(crate) mod lock;
7373
pub(crate) mod lock_target;
7474
pub(crate) mod remove;
@@ -1361,7 +1361,7 @@ impl ScriptPython {
13611361

13621362
/// The Python environment for a project.
13631363
#[derive(Debug)]
1364-
enum ProjectEnvironment {
1364+
pub(crate) enum ProjectEnvironment {
13651365
/// An existing [`PythonEnvironment`] was discovered, which satisfies the project's requirements.
13661366
Existing(PythonEnvironment),
13671367
/// An existing [`PythonEnvironment`] was discovered, but did not satisfy the project's

crates/uv/src/commands/project/sync.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ impl Deref for SyncEnvironment {
630630
}
631631

632632
/// Sync a lockfile with an environment.
633-
pub(super) async fn do_sync(
633+
pub(crate) async fn do_sync(
634634
target: InstallTarget<'_>,
635635
venv: &PythonEnvironment,
636636
extras: &ExtrasSpecificationWithDefaults,

crates/uv/src/commands/workspace/metadata.rs

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
use std::fmt::Write;
22
use std::path::Path;
33

4-
use anyhow::Result;
4+
use anyhow::{Context, Result};
55
use owo_colors::OwoColorize;
66

77
use uv_cache::{Cache, Refresh};
88
use uv_client::BaseClientBuilder;
99
use uv_configuration::{Concurrency, DependencyGroupsWithDefaults, DryRun};
1010
use uv_preview::{Preview, PreviewFeature};
1111
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
12-
use uv_resolver::{Lock, Metadata};
12+
use uv_resolver::Metadata;
1313
use uv_settings::PythonInstallMirrors;
1414
use uv_warnings::warn_user;
15-
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache};
15+
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache};
1616

1717
use crate::commands::pip::loggers::DefaultResolveLogger;
1818
use crate::commands::project::lock::{LockMode, LockOperation};
1919
use crate::commands::project::lock_target::LockTarget;
20-
use crate::commands::project::{ProjectError, ProjectInterpreter, UniversalState, WorkspacePython};
20+
use crate::commands::project::{
21+
ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, WorkspacePython,
22+
};
2123
use crate::commands::{ExitStatus, diagnostics};
2224
use crate::printer::Printer;
2325
use crate::settings::{FrozenSource, LockCheck, ResolverSettings};
2426

27+
use super::module_owners::collect_module_owners;
28+
2529
/// Display metadata about the workspace.
2630
pub(crate) async fn metadata(
2731
project_dir: &Path,
2832
lock_check: LockCheck,
2933
frozen: Option<FrozenSource>,
3034
dry_run: DryRun,
3135
refresh: Refresh,
36+
module_owners: bool,
3237
python: Option<String>,
3338
install_mirrors: PythonInstallMirrors,
3439
settings: ResolverSettings,
@@ -54,38 +59,37 @@ pub(crate) async fn metadata(
5459
.await?;
5560
let target = LockTarget::Workspace(virtual_project.workspace());
5661

62+
// Don't enable any groups' requires-python for interpreter discovery.
63+
let groups = DependencyGroupsWithDefaults::none();
64+
let workspace_python = WorkspacePython::from_request(
65+
python.as_deref().map(PythonRequest::parse),
66+
Some(virtual_project.workspace()),
67+
&groups,
68+
project_dir,
69+
no_config,
70+
)
71+
.await?;
72+
let interpreter = ProjectInterpreter::discover(
73+
virtual_project.workspace(),
74+
&groups,
75+
workspace_python,
76+
&client_builder,
77+
python_preference,
78+
python_downloads,
79+
&install_mirrors,
80+
false,
81+
Some(false),
82+
cache,
83+
printer,
84+
preview,
85+
)
86+
.await?
87+
.into_interpreter();
88+
5789
// Determine the lock mode.
58-
let interpreter;
5990
let mode = if let Some(frozen_source) = frozen {
6091
LockMode::Frozen(frozen_source.into())
6192
} else {
62-
// Don't enable any groups' requires-python for interpreter discovery
63-
let groups = DependencyGroupsWithDefaults::none();
64-
let workspace_python = WorkspacePython::from_request(
65-
python.as_deref().map(PythonRequest::parse),
66-
Some(virtual_project.workspace()),
67-
&groups,
68-
project_dir,
69-
no_config,
70-
)
71-
.await?;
72-
interpreter = ProjectInterpreter::discover(
73-
virtual_project.workspace(),
74-
&groups,
75-
workspace_python,
76-
&client_builder,
77-
python_preference,
78-
python_downloads,
79-
&install_mirrors,
80-
false,
81-
Some(false),
82-
cache,
83-
printer,
84-
preview,
85-
)
86-
.await?
87-
.into_interpreter();
88-
8993
if let LockCheck::Enabled(lock_check) = lock_check {
9094
LockMode::Locked(&interpreter, lock_check)
9195
} else if dry_run.enabled() {
@@ -117,7 +121,46 @@ pub(crate) async fn metadata(
117121
)
118122
.await
119123
{
120-
Ok(lock) => print_lock_as_metadata(virtual_project.workspace(), &lock.into_lock(), printer),
124+
Ok(lock) => {
125+
let lock = lock.into_lock();
126+
let mut export = Metadata::from_lock(virtual_project.workspace(), &lock)?;
127+
if module_owners {
128+
let environment = ProjectEnvironment::get_or_init(
129+
virtual_project.workspace(),
130+
&groups,
131+
python.as_deref().map(PythonRequest::parse),
132+
&install_mirrors,
133+
&client_builder,
134+
python_preference,
135+
python_downloads,
136+
false,
137+
no_config,
138+
Some(false),
139+
cache,
140+
DryRun::Disabled,
141+
printer,
142+
preview,
143+
)
144+
.await?;
145+
let module_owners = collect_module_owners(
146+
virtual_project.workspace(),
147+
&lock,
148+
&environment,
149+
&settings,
150+
&client_builder,
151+
&state,
152+
&concurrency,
153+
cache,
154+
workspace_cache,
155+
preview,
156+
)
157+
.await
158+
.context("Failed to collect module owners")?;
159+
export = export.with_module_owners(module_owners);
160+
}
161+
162+
print_metadata(&export, printer)
163+
}
121164
Err(err @ ProjectError::LockMismatch(..)) => {
122165
writeln!(printer.stderr(), "{}", err.to_string().bold())?;
123166
Ok(ExitStatus::Failure)
@@ -131,13 +174,7 @@ pub(crate) async fn metadata(
131174
}
132175
}
133176

134-
fn print_lock_as_metadata(
135-
workspace: &Workspace,
136-
lock: &Lock,
137-
printer: Printer,
138-
) -> Result<ExitStatus> {
139-
let export = Metadata::from_lock(workspace, lock)?;
140-
177+
fn print_metadata(export: &Metadata, printer: Printer) -> Result<ExitStatus> {
141178
writeln!(printer.stdout(), "{}", export.to_json()?)?;
142179

143180
Ok(ExitStatus::Success)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub(crate) mod dir;
22
pub(crate) mod list;
33
pub(crate) mod metadata;
4+
mod module_owners;

0 commit comments

Comments
 (0)