Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8028,6 +8028,13 @@ pub struct MetadataArgs {
#[command(flatten)]
pub refresh: RefreshArgs,

/// Include module ownership metadata in the output.
///
Comment thread
zsol marked this conversation as resolved.
/// This adds a mapping from importable module names to the package names that provide
/// them. To do this, the venv will be synced in "inexact" mode.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// them. To do this, the venv will be synced in "inexact" mode.
/// them. To do this, the venv will be synced in inexact mode.

#[arg(long)]
pub module_owners: bool,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might prefer --report-modules, but not sure if I feel strongly yet. I'll mull it over.


/// The Python interpreter to use during resolution.
///
/// A Python interpreter is required for building source distributions to determine package
Expand Down
16 changes: 16 additions & 0 deletions crates/uv-resolver/src/lock/export/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::collections::BTreeMap;
use std::fmt::Display;

/// The name of an importable Python module.
type ModuleName = String;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about uv_pypi_types::identifier::Identifier?


use uv_distribution_filename::WheelFilename;
use uv_distribution_types::{RequiresPython, UrlString};
use uv_fs::PortablePathBuf;
Expand Down Expand Up @@ -66,6 +69,9 @@ pub struct Metadata {
requires_python: RequiresPython,
/// Info about conflicting packages
conflicts: MetadataConflicts,
/// A mapping from importable module names to the distributions that provide them
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
module_owners: BTreeMap<ModuleName, Vec<PackageName>>,
Comment on lines +72 to +74
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably reverse key and value in this map and make it package centric, in the sense that all metadata is attached to package node(s), rather than the other thing pointing towards a package.

/// An index of which nodes are workspace members
///
/// These entries are often what you should use as the entry-points into the `resolve` graph.
Expand Down Expand Up @@ -818,13 +824,23 @@ impl Metadata {
version: SchemaVersion::Preview,
},
conflicts,
module_owners: BTreeMap::new(),
workspace_root,
requires_python: lock.requires_python.clone(),
members,
resolution: resolve,
})
}

#[must_use]
pub fn with_module_owners(
mut self,
module_owners: BTreeMap<ModuleName, Vec<PackageName>>,
) -> Self {
self.module_owners = module_owners;
self
}

pub fn to_json(&self) -> Result<String, MetadataError> {
Ok(serde_json::to_string_pretty(self)?)
}
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub(crate) mod environment;
pub(crate) mod export;
pub(crate) mod format;
pub(crate) mod init;
mod install_target;
pub(crate) mod install_target;
pub(crate) mod lock;
pub(crate) mod lock_target;
pub(crate) mod remove;
Expand Down Expand Up @@ -1361,7 +1361,7 @@ impl ScriptPython {

/// The Python environment for a project.
#[derive(Debug)]
enum ProjectEnvironment {
pub(crate) enum ProjectEnvironment {
/// An existing [`PythonEnvironment`] was discovered, which satisfies the project's requirements.
Existing(PythonEnvironment),
/// An existing [`PythonEnvironment`] was discovered, but did not satisfy the project's
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ impl Deref for SyncEnvironment {
}

/// Sync a lockfile with an environment.
pub(super) async fn do_sync(
pub(crate) async fn do_sync(
target: InstallTarget<'_>,
venv: &PythonEnvironment,
extras: &ExtrasSpecificationWithDefaults,
Expand Down
117 changes: 77 additions & 40 deletions crates/uv/src/commands/workspace/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
use std::fmt::Write;
use std::path::Path;

use anyhow::Result;
use anyhow::{Context, Result};
use owo_colors::OwoColorize;

use uv_cache::{Cache, Refresh};
use uv_client::BaseClientBuilder;
use uv_configuration::{Concurrency, DependencyGroupsWithDefaults, DryRun};
use uv_preview::{Preview, PreviewFeature};
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_resolver::{Lock, Metadata};
use uv_resolver::Metadata;
use uv_settings::PythonInstallMirrors;
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache};

use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::lock::{LockMode, LockOperation};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{ProjectError, ProjectInterpreter, UniversalState, WorkspacePython};
use crate::commands::project::{
ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, WorkspacePython,
};
use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer;
use crate::settings::{FrozenSource, LockCheck, ResolverSettings};

use super::module_owners::collect_module_owners;

/// Display metadata about the workspace.
pub(crate) async fn metadata(
project_dir: &Path,
lock_check: LockCheck,
frozen: Option<FrozenSource>,
dry_run: DryRun,
refresh: Refresh,
module_owners: bool,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverSettings,
Expand All @@ -54,38 +59,37 @@ pub(crate) async fn metadata(
.await?;
let target = LockTarget::Workspace(virtual_project.workspace());

// Don't enable any groups' requires-python for interpreter discovery.
let groups = DependencyGroupsWithDefaults::none();
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(virtual_project.workspace()),
&groups,
project_dir,
no_config,
)
.await?;
let interpreter = ProjectInterpreter::discover(
virtual_project.workspace(),
&groups,
workspace_python,
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
Some(false),
cache,
printer,
preview,
)
.await?
.into_interpreter();

// Determine the lock mode.
let interpreter;
let mode = if let Some(frozen_source) = frozen {
LockMode::Frozen(frozen_source.into())
} else {
// Don't enable any groups' requires-python for interpreter discovery
let groups = DependencyGroupsWithDefaults::none();
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(virtual_project.workspace()),
&groups,
project_dir,
no_config,
)
.await?;
interpreter = ProjectInterpreter::discover(
virtual_project.workspace(),
&groups,
workspace_python,
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
Some(false),
cache,
printer,
preview,
)
.await?
.into_interpreter();

if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(&interpreter, lock_check)
} else if dry_run.enabled() {
Expand Down Expand Up @@ -117,7 +121,46 @@ pub(crate) async fn metadata(
)
.await
{
Ok(lock) => print_lock_as_metadata(virtual_project.workspace(), &lock.into_lock(), printer),
Ok(lock) => {
let lock = lock.into_lock();
let mut export = Metadata::from_lock(virtual_project.workspace(), &lock)?;
if module_owners {
let environment = ProjectEnvironment::get_or_init(
virtual_project.workspace(),
&groups,
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
&client_builder,
python_preference,
python_downloads,
false,
no_config,
Some(false),
cache,
DryRun::Disabled,
printer,
preview,
)
.await?;
let module_owners = collect_module_owners(
virtual_project.workspace(),
&lock,
&environment,
&settings,
&client_builder,
&state,
&concurrency,
cache,
workspace_cache,
preview,
)
.await
.context("Failed to collect module owners")?;
export = export.with_module_owners(module_owners);
}

print_metadata(&export, printer)
}
Err(err @ ProjectError::LockMismatch(..)) => {
writeln!(printer.stderr(), "{}", err.to_string().bold())?;
Ok(ExitStatus::Failure)
Expand All @@ -131,13 +174,7 @@ pub(crate) async fn metadata(
}
}

fn print_lock_as_metadata(
workspace: &Workspace,
lock: &Lock,
printer: Printer,
) -> Result<ExitStatus> {
let export = Metadata::from_lock(workspace, lock)?;

fn print_metadata(export: &Metadata, printer: Printer) -> Result<ExitStatus> {
writeln!(printer.stdout(), "{}", export.to_json()?)?;

Ok(ExitStatus::Success)
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/workspace/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod dir;
pub(crate) mod list;
pub(crate) mod metadata;
mod module_owners;
Loading
Loading