mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Implement uv tree
(#4708)
## Summary Implements the `uv tree`, which displays dependencies from the lockfile as a tree. Resolves https://github.com/astral-sh/uv/issues/4699.
This commit is contained in:
parent
4bc36c0cb8
commit
dc7ad3abdb
12 changed files with 434 additions and 101 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4456,6 +4456,7 @@ dependencies = [
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"futures",
|
"futures",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"indexmap",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"indoc",
|
"indoc",
|
||||||
"insta",
|
"insta",
|
||||||
|
|
|
@ -284,6 +284,9 @@ pub enum ProjectCommand {
|
||||||
/// Remove one or more packages from the project requirements.
|
/// Remove one or more packages from the project requirements.
|
||||||
#[clap(hide = true)]
|
#[clap(hide = true)]
|
||||||
Remove(RemoveArgs),
|
Remove(RemoveArgs),
|
||||||
|
/// Display the dependency tree for the project.
|
||||||
|
#[clap(hide = true)]
|
||||||
|
Tree(TreeArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A re-implementation of `Option`, used to avoid Clap's automatic `Option` flattening in
|
/// A re-implementation of `Option`, used to avoid Clap's automatic `Option` flattening in
|
||||||
|
@ -1426,29 +1429,8 @@ pub struct PipShowArgs {
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct PipTreeArgs {
|
pub struct PipTreeArgs {
|
||||||
/// Maximum display depth of the dependency tree
|
#[command(flatten)]
|
||||||
#[arg(long, short, default_value_t = 255)]
|
pub tree: DisplayTreeArgs,
|
||||||
pub depth: u8,
|
|
||||||
|
|
||||||
/// Prune the given package from the display of the dependency tree.
|
|
||||||
#[arg(long)]
|
|
||||||
pub prune: Vec<PackageName>,
|
|
||||||
|
|
||||||
/// Display only the specified packages.
|
|
||||||
#[arg(long)]
|
|
||||||
pub package: Vec<PackageName>,
|
|
||||||
|
|
||||||
/// Do not de-duplicate repeated dependencies.
|
|
||||||
/// Usually, when a package has already displayed its dependencies,
|
|
||||||
/// further occurrences will not re-display its dependencies,
|
|
||||||
/// and will include a (*) to indicate it has already been shown.
|
|
||||||
/// This flag will cause those duplicates to be repeated.
|
|
||||||
#[arg(long)]
|
|
||||||
pub no_dedupe: bool,
|
|
||||||
|
|
||||||
#[arg(long, alias = "reverse")]
|
|
||||||
/// Show the reverse dependencies for the given package. This flag will invert the tree and display the packages that depend on the given package.
|
|
||||||
pub invert: bool,
|
|
||||||
|
|
||||||
/// Validate the virtual environment, to detect packages with missing dependencies or other
|
/// Validate the virtual environment, to detect packages with missing dependencies or other
|
||||||
/// issues.
|
/// issues.
|
||||||
|
@ -1886,6 +1868,34 @@ pub struct RemoveArgs {
|
||||||
pub python: Option<String>,
|
pub python: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub struct TreeArgs {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub tree: DisplayTreeArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub build: BuildArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub resolver: ResolverArgs,
|
||||||
|
|
||||||
|
/// The Python interpreter for which packages should be listed.
|
||||||
|
///
|
||||||
|
/// By default, `uv` installs into the virtual environment in the current working directory or
|
||||||
|
/// any parent directory. The `--python` option allows you to specify a different interpreter,
|
||||||
|
/// which is intended for use in continuous integration (CI) environments or other automated
|
||||||
|
/// workflows.
|
||||||
|
///
|
||||||
|
/// Supported formats:
|
||||||
|
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
|
||||||
|
/// `python3.10` on Linux and macOS.
|
||||||
|
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
|
||||||
|
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
|
||||||
|
#[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)]
|
||||||
|
pub python: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct ToolNamespace {
|
pub struct ToolNamespace {
|
||||||
|
@ -2437,3 +2447,30 @@ pub struct ResolverInstallerArgs {
|
||||||
)]
|
)]
|
||||||
pub no_compile_bytecode: bool,
|
pub no_compile_bytecode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct DisplayTreeArgs {
|
||||||
|
/// Maximum display depth of the dependency tree
|
||||||
|
#[arg(long, short, default_value_t = 255)]
|
||||||
|
pub depth: u8,
|
||||||
|
|
||||||
|
/// Prune the given package from the display of the dependency tree.
|
||||||
|
#[arg(long)]
|
||||||
|
pub prune: Vec<PackageName>,
|
||||||
|
|
||||||
|
/// Display only the specified packages.
|
||||||
|
#[arg(long)]
|
||||||
|
pub package: Vec<PackageName>,
|
||||||
|
|
||||||
|
/// Do not de-duplicate repeated dependencies.
|
||||||
|
/// Usually, when a package has already displayed its dependencies,
|
||||||
|
/// further occurrences will not re-display its dependencies,
|
||||||
|
/// and will include a (*) to indicate it has already been shown.
|
||||||
|
/// This flag will cause those duplicates to be repeated.
|
||||||
|
#[arg(long)]
|
||||||
|
pub no_dedupe: bool,
|
||||||
|
|
||||||
|
/// Show the reverse dependencies for the given package. This flag will invert the tree and display the packages that depend on the given package.
|
||||||
|
#[arg(long, alias = "reverse")]
|
||||||
|
pub invert: bool,
|
||||||
|
}
|
||||||
|
|
|
@ -324,6 +324,11 @@ impl Lock {
|
||||||
&self.distributions
|
&self.distributions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the owned [`Distribution`] entries in this lock.
|
||||||
|
pub fn into_distributions(self) -> Vec<Distribution> {
|
||||||
|
self.distributions
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the supported Python version range for the lockfile, if present.
|
/// Returns the supported Python version range for the lockfile, if present.
|
||||||
pub fn requires_python(&self) -> Option<&RequiresPython> {
|
pub fn requires_python(&self) -> Option<&RequiresPython> {
|
||||||
self.requires_python.as_ref()
|
self.requires_python.as_ref()
|
||||||
|
|
|
@ -51,6 +51,7 @@ flate2 = { workspace = true, default-features = false }
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
indicatif = { workspace = true }
|
indicatif = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
miette = { workspace = true, features = ["fancy"] }
|
miette = { workspace = true, features = ["fancy"] }
|
||||||
owo-colors = { workspace = true }
|
owo-colors = { workspace = true }
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub(crate) use project::lock::lock;
|
||||||
pub(crate) use project::remove::remove;
|
pub(crate) use project::remove::remove;
|
||||||
pub(crate) use project::run::run;
|
pub(crate) use project::run::run;
|
||||||
pub(crate) use project::sync::sync;
|
pub(crate) use project::sync::sync;
|
||||||
|
pub(crate) use project::tree::tree;
|
||||||
pub(crate) use python::dir::dir as python_dir;
|
pub(crate) use python::dir::dir as python_dir;
|
||||||
pub(crate) use python::find::find as python_find;
|
pub(crate) use python::find::find as python_find;
|
||||||
pub(crate) use python::install::install as python_install;
|
pub(crate) use python::install::install as python_install;
|
||||||
|
|
|
@ -2,14 +2,15 @@ use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use indexmap::IndexMap;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use distribution_types::{Diagnostic, InstalledDist, Name};
|
use distribution_types::{Diagnostic, Name};
|
||||||
use pep508_rs::{MarkerEnvironment, Requirement};
|
use pep508_rs::MarkerEnvironment;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
|
use uv_distribution::Metadata;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
@ -47,19 +48,28 @@ pub(crate) fn pip_tree(
|
||||||
environment.python_executable().user_display().cyan()
|
environment.python_executable().user_display().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build the installed index.
|
// Read packages from the virtual environment.
|
||||||
let site_packages = SitePackages::from_environment(&environment)?;
|
let site_packages = SitePackages::from_environment(&environment)?;
|
||||||
|
let mut packages: IndexMap<_, Vec<_>> = IndexMap::new();
|
||||||
|
for package in site_packages.iter() {
|
||||||
|
let metadata = Metadata::from_metadata23(package.metadata()?);
|
||||||
|
packages
|
||||||
|
.entry(package.name().clone())
|
||||||
|
.or_default()
|
||||||
|
.push(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the tree.
|
||||||
let rendered_tree = DisplayDependencyGraph::new(
|
let rendered_tree = DisplayDependencyGraph::new(
|
||||||
&site_packages,
|
|
||||||
depth.into(),
|
depth.into(),
|
||||||
prune,
|
prune,
|
||||||
package,
|
package,
|
||||||
no_dedupe,
|
no_dedupe,
|
||||||
invert,
|
invert,
|
||||||
environment.interpreter().markers(),
|
environment.interpreter().markers(),
|
||||||
)?
|
packages,
|
||||||
.render()?
|
)
|
||||||
|
.render()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
writeln!(printer.stdout(), "{rendered_tree}")?;
|
writeln!(printer.stdout(), "{rendered_tree}")?;
|
||||||
|
@ -89,32 +99,9 @@ pub(crate) fn pip_tree(
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter out all required packages of the given distribution if they
|
|
||||||
/// are required by an extra.
|
|
||||||
///
|
|
||||||
/// For example, `requests==2.32.3` requires `charset-normalizer`, `idna`, `urllib`, and `certifi` at
|
|
||||||
/// all times, `PySocks` on `socks` extra and `chardet` on `use_chardet_on_py3` extra.
|
|
||||||
/// This function will return `["charset-normalizer", "idna", "urllib", "certifi"]` for `requests`.
|
|
||||||
fn filtered_requirements<'env>(
|
|
||||||
dist: &'env InstalledDist,
|
|
||||||
markers: &'env MarkerEnvironment,
|
|
||||||
) -> Result<impl Iterator<Item = Requirement<VerbatimParsedUrl>> + 'env> {
|
|
||||||
Ok(dist
|
|
||||||
.metadata()?
|
|
||||||
.requires_dist
|
|
||||||
.into_iter()
|
|
||||||
.filter(|requirement| {
|
|
||||||
requirement
|
|
||||||
.marker
|
|
||||||
.as_ref()
|
|
||||||
.map_or(true, |m| m.evaluate(markers, &[]))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct DisplayDependencyGraph<'env> {
|
pub(crate) struct DisplayDependencyGraph {
|
||||||
// Installed packages.
|
packages: IndexMap<PackageName, Vec<Metadata>>,
|
||||||
site_packages: &'env SitePackages,
|
|
||||||
/// Maximum display depth of the dependency tree
|
/// Maximum display depth of the dependency tree
|
||||||
depth: usize,
|
depth: usize,
|
||||||
/// Prune the given packages from the display of the dependency tree.
|
/// Prune the given packages from the display of the dependency tree.
|
||||||
|
@ -129,82 +116,88 @@ struct DisplayDependencyGraph<'env> {
|
||||||
requirements: HashMap<PackageName, Vec<PackageName>>,
|
requirements: HashMap<PackageName, Vec<PackageName>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'env> DisplayDependencyGraph<'env> {
|
impl DisplayDependencyGraph {
|
||||||
/// Create a new [`DisplayDependencyGraph`] for the set of installed distributions.
|
/// Create a new [`DisplayDependencyGraph`] for the set of installed distributions.
|
||||||
fn new(
|
pub(crate) fn new(
|
||||||
site_packages: &'env SitePackages,
|
|
||||||
depth: usize,
|
depth: usize,
|
||||||
prune: Vec<PackageName>,
|
prune: Vec<PackageName>,
|
||||||
package: Vec<PackageName>,
|
package: Vec<PackageName>,
|
||||||
no_dedupe: bool,
|
no_dedupe: bool,
|
||||||
invert: bool,
|
invert: bool,
|
||||||
markers: &'env MarkerEnvironment,
|
markers: &MarkerEnvironment,
|
||||||
) -> Result<DisplayDependencyGraph<'env>> {
|
packages: IndexMap<PackageName, Vec<Metadata>>,
|
||||||
|
) -> Self {
|
||||||
let mut requirements: HashMap<_, Vec<_>> = HashMap::new();
|
let mut requirements: HashMap<_, Vec<_>> = HashMap::new();
|
||||||
|
|
||||||
// Add all transitive requirements.
|
// Add all transitive requirements.
|
||||||
for site_package in site_packages.iter() {
|
for metadata in packages.values().flatten() {
|
||||||
for required in filtered_requirements(site_package, markers)? {
|
// Ignore any optional dependencies.
|
||||||
|
for required in metadata.requires_dist.iter().filter(|requirement| {
|
||||||
|
requirement
|
||||||
|
.marker
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |m| m.evaluate(markers, &[]))
|
||||||
|
}) {
|
||||||
if invert {
|
if invert {
|
||||||
requirements
|
requirements
|
||||||
.entry(required.name.clone())
|
.entry(required.name.clone())
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(site_package.name().clone());
|
.push(metadata.name.clone());
|
||||||
} else {
|
} else {
|
||||||
requirements
|
requirements
|
||||||
.entry(site_package.name().clone())
|
.entry(metadata.name.clone())
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(required.name.clone());
|
.push(required.name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Self {
|
||||||
site_packages,
|
packages,
|
||||||
depth,
|
depth,
|
||||||
prune,
|
prune,
|
||||||
package,
|
package,
|
||||||
no_dedupe,
|
no_dedupe,
|
||||||
requirements,
|
requirements,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform a depth-first traversal of the given distribution and its dependencies.
|
/// Perform a depth-first traversal of the given distribution and its dependencies.
|
||||||
fn visit(
|
fn visit<'env>(
|
||||||
&self,
|
&'env self,
|
||||||
installed_dist: &'env InstalledDist,
|
metadata: &'env Metadata,
|
||||||
visited: &mut FxHashMap<&'env PackageName, Vec<PackageName>>,
|
visited: &mut FxHashMap<&'env PackageName, Vec<PackageName>>,
|
||||||
path: &mut Vec<&'env PackageName>,
|
path: &mut Vec<&'env PackageName>,
|
||||||
) -> Result<Vec<String>> {
|
) -> Vec<String> {
|
||||||
// Short-circuit if the current path is longer than the provided depth.
|
// Short-circuit if the current path is longer than the provided depth.
|
||||||
if path.len() > self.depth {
|
if path.len() > self.depth {
|
||||||
return Ok(Vec::new());
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let package_name = installed_dist.name();
|
let package_name = &metadata.name;
|
||||||
let line = format!("{} v{}", package_name, installed_dist.version());
|
let line = format!("{} v{}", package_name, metadata.version);
|
||||||
|
|
||||||
// Skip the traversal if:
|
// Skip the traversal if:
|
||||||
// 1. The package is in the current traversal path (i.e., a dependency cycle).
|
// 1. The package is in the current traversal path (i.e., a dependency cycle).
|
||||||
// 2. The package has been visited and de-duplication is enabled (default).
|
// 2. The package has been visited and de-duplication is enabled (default).
|
||||||
if let Some(requirements) = visited.get(package_name) {
|
if let Some(requirements) = visited.get(package_name) {
|
||||||
if !self.no_dedupe || path.contains(&package_name) {
|
if !self.no_dedupe || path.contains(&package_name) {
|
||||||
return Ok(if requirements.is_empty() {
|
return if requirements.is_empty() {
|
||||||
vec![line]
|
vec![line]
|
||||||
} else {
|
} else {
|
||||||
vec![format!("{} (*)", line)]
|
vec![format!("{} (*)", line)]
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let requirements = self
|
let requirements = self
|
||||||
.requirements
|
.requirements
|
||||||
.get(installed_dist.name())
|
.get(package_name)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter(|req| {
|
.filter(|&req| {
|
||||||
// Skip if the current package is not one of the installed distributions.
|
// Skip if the current package is not one of the installed distributions.
|
||||||
!self.prune.contains(req) && self.site_packages.contains_package(req)
|
!self.prune.contains(req) && self.packages.contains_key(req)
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -241,9 +234,9 @@ impl<'env> DisplayDependencyGraph<'env> {
|
||||||
("├── ", "│ ")
|
("├── ", "│ ")
|
||||||
};
|
};
|
||||||
|
|
||||||
for distribution in self.site_packages.get_packages(req) {
|
for distribution in self.packages.get(req).into_iter().flatten() {
|
||||||
for (visited_index, visited_line) in
|
for (visited_index, visited_line) in
|
||||||
self.visit(distribution, visited, path)?.iter().enumerate()
|
self.visit(distribution, visited, path).iter().enumerate()
|
||||||
{
|
{
|
||||||
let prefix = if visited_index == 0 {
|
let prefix = if visited_index == 0 {
|
||||||
prefix_top
|
prefix_top
|
||||||
|
@ -257,11 +250,11 @@ impl<'env> DisplayDependencyGraph<'env> {
|
||||||
}
|
}
|
||||||
path.pop();
|
path.pop();
|
||||||
|
|
||||||
Ok(lines)
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Depth-first traverse the nodes to render the tree.
|
/// Depth-first traverse the nodes to render the tree.
|
||||||
fn render(&self) -> Result<Vec<String>> {
|
pub(crate) fn render(&self) -> Vec<String> {
|
||||||
let mut visited: FxHashMap<&PackageName, Vec<PackageName>> = FxHashMap::default();
|
let mut visited: FxHashMap<&PackageName, Vec<PackageName>> = FxHashMap::default();
|
||||||
let mut path: Vec<&PackageName> = Vec::new();
|
let mut path: Vec<&PackageName> = Vec::new();
|
||||||
let mut lines: Vec<String> = Vec::new();
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
@ -269,12 +262,12 @@ impl<'env> DisplayDependencyGraph<'env> {
|
||||||
if self.package.is_empty() {
|
if self.package.is_empty() {
|
||||||
// The root nodes are those that are not required by any other package.
|
// The root nodes are those that are not required by any other package.
|
||||||
let children: HashSet<_> = self.requirements.values().flatten().collect();
|
let children: HashSet<_> = self.requirements.values().flatten().collect();
|
||||||
for site_package in self.site_packages.iter() {
|
for package in self.packages.values().flatten() {
|
||||||
// If the current package is not required by any other package, start the traversal
|
// If the current package is not required by any other package, start the traversal
|
||||||
// with the current package as the root.
|
// with the current package as the root.
|
||||||
if !children.contains(site_package.name()) {
|
if !children.contains(&package.name) {
|
||||||
path.clear();
|
path.clear();
|
||||||
lines.extend(self.visit(site_package, &mut visited, &mut path)?);
|
lines.extend(self.visit(package, &mut visited, &mut path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -282,12 +275,14 @@ impl<'env> DisplayDependencyGraph<'env> {
|
||||||
if index != 0 {
|
if index != 0 {
|
||||||
lines.push(String::new());
|
lines.push(String::new());
|
||||||
}
|
}
|
||||||
for installed_dist in self.site_packages.get_packages(package) {
|
|
||||||
|
for package in self.packages.get(package).into_iter().flatten() {
|
||||||
path.clear();
|
path.clear();
|
||||||
lines.extend(self.visit(installed_dist, &mut visited, &mut path)?);
|
lines.extend(self.visit(package, &mut visited, &mut path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(lines)
|
|
||||||
|
lines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ pub(crate) mod lock;
|
||||||
pub(crate) mod remove;
|
pub(crate) mod remove;
|
||||||
pub(crate) mod run;
|
pub(crate) mod run;
|
||||||
pub(crate) mod sync;
|
pub(crate) mod sync;
|
||||||
|
pub(crate) mod tree;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub(crate) enum ProjectError {
|
pub(crate) enum ProjectError {
|
||||||
|
|
111
crates/uv/src/commands/project/tree.rs
Normal file
111
crates/uv/src/commands/project/tree.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
use pep508_rs::PackageName;
|
||||||
|
use uv_cache::Cache;
|
||||||
|
use uv_client::Connectivity;
|
||||||
|
use uv_configuration::{Concurrency, PreviewMode};
|
||||||
|
use uv_distribution::Workspace;
|
||||||
|
use uv_python::{PythonFetch, PythonPreference, PythonRequest};
|
||||||
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
use crate::commands::pip::tree::DisplayDependencyGraph;
|
||||||
|
use crate::commands::project::FoundInterpreter;
|
||||||
|
use crate::commands::ExitStatus;
|
||||||
|
use crate::printer::Printer;
|
||||||
|
use crate::settings::ResolverSettings;
|
||||||
|
|
||||||
|
use super::lock::do_lock;
|
||||||
|
use super::SharedState;
|
||||||
|
|
||||||
|
/// Run a command.
|
||||||
|
pub(crate) async fn tree(
|
||||||
|
depth: u8,
|
||||||
|
prune: Vec<PackageName>,
|
||||||
|
package: Vec<PackageName>,
|
||||||
|
no_dedupe: bool,
|
||||||
|
invert: bool,
|
||||||
|
python: Option<String>,
|
||||||
|
settings: ResolverSettings,
|
||||||
|
python_preference: PythonPreference,
|
||||||
|
python_fetch: PythonFetch,
|
||||||
|
preview: PreviewMode,
|
||||||
|
connectivity: Connectivity,
|
||||||
|
concurrency: Concurrency,
|
||||||
|
native_tls: bool,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<ExitStatus> {
|
||||||
|
if preview.is_disabled() {
|
||||||
|
warn_user_once!("`uv run` is experimental and may change without warning.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the project requirements.
|
||||||
|
let workspace = Workspace::discover(&std::env::current_dir()?, None).await?;
|
||||||
|
|
||||||
|
// Find an interpreter for the project
|
||||||
|
let interpreter = FoundInterpreter::discover(
|
||||||
|
&workspace,
|
||||||
|
python.as_deref().map(PythonRequest::parse),
|
||||||
|
python_preference,
|
||||||
|
python_fetch,
|
||||||
|
connectivity,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_interpreter();
|
||||||
|
|
||||||
|
// Update the lock file.
|
||||||
|
let lock = do_lock(
|
||||||
|
&workspace,
|
||||||
|
&interpreter,
|
||||||
|
settings.as_ref(),
|
||||||
|
&SharedState::default(),
|
||||||
|
preview,
|
||||||
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Read packages from the lockfile.
|
||||||
|
let mut packages: IndexMap<_, Vec<_>> = IndexMap::new();
|
||||||
|
for dist in lock.into_distributions() {
|
||||||
|
let name = dist.name().clone();
|
||||||
|
let metadata = dist.into_metadata(workspace.install_path())?;
|
||||||
|
packages.entry(name).or_default().push(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the tree.
|
||||||
|
let rendered_tree = DisplayDependencyGraph::new(
|
||||||
|
depth.into(),
|
||||||
|
prune,
|
||||||
|
package,
|
||||||
|
no_dedupe,
|
||||||
|
invert,
|
||||||
|
interpreter.markers(),
|
||||||
|
packages,
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
writeln!(printer.stdout(), "{rendered_tree}")?;
|
||||||
|
|
||||||
|
if rendered_tree.contains('*') {
|
||||||
|
let message = if no_dedupe {
|
||||||
|
"(*) Package tree is a cycle and cannot be shown".italic()
|
||||||
|
} else {
|
||||||
|
"(*) Package tree already displayed".italic()
|
||||||
|
};
|
||||||
|
writeln!(printer.stdout(), "{message}")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExitStatus::Success)
|
||||||
|
}
|
|
@ -974,6 +974,33 @@ async fn run_project(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
ProjectCommand::Tree(args) => {
|
||||||
|
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||||
|
let args = settings::TreeSettings::resolve(args, filesystem);
|
||||||
|
show_settings!(args);
|
||||||
|
|
||||||
|
// Initialize the cache.
|
||||||
|
let cache = cache.init()?;
|
||||||
|
|
||||||
|
commands::tree(
|
||||||
|
args.depth,
|
||||||
|
args.prune,
|
||||||
|
args.package,
|
||||||
|
args.no_dedupe,
|
||||||
|
args.invert,
|
||||||
|
args.python,
|
||||||
|
args.resolver,
|
||||||
|
globals.python_preference,
|
||||||
|
globals.python_fetch,
|
||||||
|
globals.preview,
|
||||||
|
globals.connectivity,
|
||||||
|
Concurrency::default(),
|
||||||
|
globals.native_tls,
|
||||||
|
&cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ use uv_cli::{
|
||||||
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
||||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
||||||
PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
|
PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
|
||||||
ToolUninstallArgs, VenvArgs,
|
ToolUninstallArgs, TreeArgs, VenvArgs,
|
||||||
};
|
};
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
|
@ -596,6 +596,40 @@ impl RemoveSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resolved settings to use for a `tree` invocation.
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct TreeSettings {
|
||||||
|
pub(crate) depth: u8,
|
||||||
|
pub(crate) prune: Vec<PackageName>,
|
||||||
|
pub(crate) package: Vec<PackageName>,
|
||||||
|
pub(crate) no_dedupe: bool,
|
||||||
|
pub(crate) invert: bool,
|
||||||
|
pub(crate) python: Option<String>,
|
||||||
|
pub(crate) resolver: ResolverSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeSettings {
|
||||||
|
/// Resolve the [`TreeSettings`] from the CLI and workspace configuration.
|
||||||
|
pub(crate) fn resolve(args: TreeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||||
|
let TreeArgs {
|
||||||
|
tree,
|
||||||
|
build,
|
||||||
|
resolver,
|
||||||
|
python,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
python,
|
||||||
|
depth: tree.depth,
|
||||||
|
prune: tree.prune,
|
||||||
|
package: tree.package,
|
||||||
|
no_dedupe: tree.no_dedupe,
|
||||||
|
invert: tree.invert,
|
||||||
|
resolver: ResolverSettings::combine(resolver_options(resolver, build), filesystem),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// The resolved settings to use for a `pip compile` invocation.
|
/// The resolved settings to use for a `pip compile` invocation.
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -1092,7 +1126,7 @@ impl PipShowSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The resolved settings to use for a `pip show` invocation.
|
/// The resolved settings to use for a `pip tree` invocation.
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct PipTreeSettings {
|
pub(crate) struct PipTreeSettings {
|
||||||
|
@ -1109,11 +1143,7 @@ impl PipTreeSettings {
|
||||||
/// Resolve the [`PipTreeSettings`] from the CLI and workspace configuration.
|
/// Resolve the [`PipTreeSettings`] from the CLI and workspace configuration.
|
||||||
pub(crate) fn resolve(args: PipTreeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
pub(crate) fn resolve(args: PipTreeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||||
let PipTreeArgs {
|
let PipTreeArgs {
|
||||||
depth,
|
tree,
|
||||||
prune,
|
|
||||||
package,
|
|
||||||
no_dedupe,
|
|
||||||
invert,
|
|
||||||
strict,
|
strict,
|
||||||
no_strict,
|
no_strict,
|
||||||
python,
|
python,
|
||||||
|
@ -1123,11 +1153,11 @@ impl PipTreeSettings {
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
depth,
|
depth: tree.depth,
|
||||||
prune,
|
prune: tree.prune,
|
||||||
package,
|
no_dedupe: tree.no_dedupe,
|
||||||
no_dedupe,
|
invert: tree.invert,
|
||||||
invert,
|
package: tree.package,
|
||||||
// Shared settings.
|
// Shared settings.
|
||||||
shared: PipSettings::combine(
|
shared: PipSettings::combine(
|
||||||
PipOptions {
|
PipOptions {
|
||||||
|
|
|
@ -472,6 +472,14 @@ impl TestContext {
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a `uv tree` command with options shared across scenarios.
|
||||||
|
pub fn tree(&self) -> Command {
|
||||||
|
let mut command = Command::new(get_bin());
|
||||||
|
command.arg("tree");
|
||||||
|
self.add_shared_args(&mut command);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a `uv clean` command.
|
/// Create a `uv clean` command.
|
||||||
pub fn clean(&self) -> Command {
|
pub fn clean(&self) -> Command {
|
||||||
let mut command = Command::new(get_bin());
|
let mut command = Command::new(get_bin());
|
||||||
|
|
116
crates/uv/tests/tree.rs
Normal file
116
crates/uv/tests/tree.rs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use assert_fs::prelude::*;
|
||||||
|
|
||||||
|
use common::{uv_snapshot, TestContext};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_dependencies() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
# ...
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"scikit-learn==1.4.1.post1"
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tree(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
project v0.1.0
|
||||||
|
└── scikit-learn v1.4.1.post1
|
||||||
|
├── joblib v1.3.2
|
||||||
|
├── numpy v1.26.4
|
||||||
|
├── scipy v1.12.0
|
||||||
|
│ └── numpy v1.26.4
|
||||||
|
└── threadpoolctl v3.4.0
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv run` is experimental and may change without warning.
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
// `uv tree` should update the lockfile
|
||||||
|
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
|
||||||
|
assert!(!lock.is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
# ...
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"scikit-learn==1.4.1.post1"
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tree().arg("--invert"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
joblib v1.3.2
|
||||||
|
└── scikit-learn v1.4.1.post1
|
||||||
|
└── project v0.1.0
|
||||||
|
numpy v1.26.4
|
||||||
|
├── scikit-learn v1.4.1.post1 (*)
|
||||||
|
└── scipy v1.12.0
|
||||||
|
└── scikit-learn v1.4.1.post1 (*)
|
||||||
|
threadpoolctl v3.4.0
|
||||||
|
└── scikit-learn v1.4.1.post1 (*)
|
||||||
|
(*) Package tree already displayed
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv run` is experimental and may change without warning.
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tree().arg("--invert").arg("--no-dedupe"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
joblib v1.3.2
|
||||||
|
└── scikit-learn v1.4.1.post1
|
||||||
|
└── project v0.1.0
|
||||||
|
numpy v1.26.4
|
||||||
|
├── scikit-learn v1.4.1.post1
|
||||||
|
│ └── project v0.1.0
|
||||||
|
└── scipy v1.12.0
|
||||||
|
└── scikit-learn v1.4.1.post1
|
||||||
|
└── project v0.1.0
|
||||||
|
threadpoolctl v3.4.0
|
||||||
|
└── scikit-learn v1.4.1.post1
|
||||||
|
└── project v0.1.0
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv run` is experimental and may change without warning.
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue