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",
|
||||
"futures",
|
||||
"ignore",
|
||||
"indexmap",
|
||||
"indicatif",
|
||||
"indoc",
|
||||
"insta",
|
||||
|
|
|
@ -284,6 +284,9 @@ pub enum ProjectCommand {
|
|||
/// Remove one or more packages from the project requirements.
|
||||
#[clap(hide = true)]
|
||||
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
|
||||
|
@ -1426,29 +1429,8 @@ pub struct PipShowArgs {
|
|||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct PipTreeArgs {
|
||||
/// 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,
|
||||
|
||||
#[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,
|
||||
#[command(flatten)]
|
||||
pub tree: DisplayTreeArgs,
|
||||
|
||||
/// Validate the virtual environment, to detect packages with missing dependencies or other
|
||||
/// issues.
|
||||
|
@ -1886,6 +1868,34 @@ pub struct RemoveArgs {
|
|||
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)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct ToolNamespace {
|
||||
|
@ -2437,3 +2447,30 @@ pub struct ResolverInstallerArgs {
|
|||
)]
|
||||
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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn requires_python(&self) -> Option<&RequiresPython> {
|
||||
self.requires_python.as_ref()
|
||||
|
|
|
@ -51,6 +51,7 @@ flate2 = { workspace = true, default-features = false }
|
|||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
futures = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy"] }
|
||||
owo-colors = { workspace = true }
|
||||
|
|
|
@ -22,6 +22,7 @@ pub(crate) use project::lock::lock;
|
|||
pub(crate) use project::remove::remove;
|
||||
pub(crate) use project::run::run;
|
||||
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::find::find as python_find;
|
||||
pub(crate) use python::install::install as python_install;
|
||||
|
|
|
@ -2,14 +2,15 @@ use std::collections::{HashMap, HashSet};
|
|||
use std::fmt::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexMap;
|
||||
use owo_colors::OwoColorize;
|
||||
use rustc_hash::FxHashMap;
|
||||
use tracing::debug;
|
||||
|
||||
use distribution_types::{Diagnostic, InstalledDist, Name};
|
||||
use pep508_rs::{MarkerEnvironment, Requirement};
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use distribution_types::{Diagnostic, Name};
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use uv_cache::Cache;
|
||||
use uv_distribution::Metadata;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_normalize::PackageName;
|
||||
|
@ -47,19 +48,28 @@ pub(crate) fn pip_tree(
|
|||
environment.python_executable().user_display().cyan()
|
||||
);
|
||||
|
||||
// Build the installed index.
|
||||
// Read packages from the virtual 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(
|
||||
&site_packages,
|
||||
depth.into(),
|
||||
prune,
|
||||
package,
|
||||
no_dedupe,
|
||||
invert,
|
||||
environment.interpreter().markers(),
|
||||
)?
|
||||
.render()?
|
||||
packages,
|
||||
)
|
||||
.render()
|
||||
.join("\n");
|
||||
|
||||
writeln!(printer.stdout(), "{rendered_tree}")?;
|
||||
|
@ -89,32 +99,9 @@ pub(crate) fn pip_tree(
|
|||
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)]
|
||||
struct DisplayDependencyGraph<'env> {
|
||||
// Installed packages.
|
||||
site_packages: &'env SitePackages,
|
||||
pub(crate) struct DisplayDependencyGraph {
|
||||
packages: IndexMap<PackageName, Vec<Metadata>>,
|
||||
/// Maximum display depth of the dependency tree
|
||||
depth: usize,
|
||||
/// Prune the given packages from the display of the dependency tree.
|
||||
|
@ -129,82 +116,88 @@ struct DisplayDependencyGraph<'env> {
|
|||
requirements: HashMap<PackageName, Vec<PackageName>>,
|
||||
}
|
||||
|
||||
impl<'env> DisplayDependencyGraph<'env> {
|
||||
impl DisplayDependencyGraph {
|
||||
/// Create a new [`DisplayDependencyGraph`] for the set of installed distributions.
|
||||
fn new(
|
||||
site_packages: &'env SitePackages,
|
||||
pub(crate) fn new(
|
||||
depth: usize,
|
||||
prune: Vec<PackageName>,
|
||||
package: Vec<PackageName>,
|
||||
no_dedupe: bool,
|
||||
invert: bool,
|
||||
markers: &'env MarkerEnvironment,
|
||||
) -> Result<DisplayDependencyGraph<'env>> {
|
||||
markers: &MarkerEnvironment,
|
||||
packages: IndexMap<PackageName, Vec<Metadata>>,
|
||||
) -> Self {
|
||||
let mut requirements: HashMap<_, Vec<_>> = HashMap::new();
|
||||
|
||||
// Add all transitive requirements.
|
||||
for site_package in site_packages.iter() {
|
||||
for required in filtered_requirements(site_package, markers)? {
|
||||
for metadata in packages.values().flatten() {
|
||||
// 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 {
|
||||
requirements
|
||||
.entry(required.name.clone())
|
||||
.or_default()
|
||||
.push(site_package.name().clone());
|
||||
.push(metadata.name.clone());
|
||||
} else {
|
||||
requirements
|
||||
.entry(site_package.name().clone())
|
||||
.entry(metadata.name.clone())
|
||||
.or_default()
|
||||
.push(required.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
site_packages,
|
||||
Self {
|
||||
packages,
|
||||
depth,
|
||||
prune,
|
||||
package,
|
||||
no_dedupe,
|
||||
requirements,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a depth-first traversal of the given distribution and its dependencies.
|
||||
fn visit(
|
||||
&self,
|
||||
installed_dist: &'env InstalledDist,
|
||||
fn visit<'env>(
|
||||
&'env self,
|
||||
metadata: &'env Metadata,
|
||||
visited: &mut FxHashMap<&'env PackageName, Vec<PackageName>>,
|
||||
path: &mut Vec<&'env PackageName>,
|
||||
) -> Result<Vec<String>> {
|
||||
) -> Vec<String> {
|
||||
// Short-circuit if the current path is longer than the provided depth.
|
||||
if path.len() > self.depth {
|
||||
return Ok(Vec::new());
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let package_name = installed_dist.name();
|
||||
let line = format!("{} v{}", package_name, installed_dist.version());
|
||||
let package_name = &metadata.name;
|
||||
let line = format!("{} v{}", package_name, metadata.version);
|
||||
|
||||
// Skip the traversal if:
|
||||
// 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).
|
||||
if let Some(requirements) = visited.get(package_name) {
|
||||
if !self.no_dedupe || path.contains(&package_name) {
|
||||
return Ok(if requirements.is_empty() {
|
||||
return if requirements.is_empty() {
|
||||
vec![line]
|
||||
} else {
|
||||
vec![format!("{} (*)", line)]
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let requirements = self
|
||||
.requirements
|
||||
.get(installed_dist.name())
|
||||
.get(package_name)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|req| {
|
||||
.filter(|&req| {
|
||||
// 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()
|
||||
.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
|
||||
self.visit(distribution, visited, path)?.iter().enumerate()
|
||||
self.visit(distribution, visited, path).iter().enumerate()
|
||||
{
|
||||
let prefix = if visited_index == 0 {
|
||||
prefix_top
|
||||
|
@ -257,11 +250,11 @@ impl<'env> DisplayDependencyGraph<'env> {
|
|||
}
|
||||
path.pop();
|
||||
|
||||
Ok(lines)
|
||||
lines
|
||||
}
|
||||
|
||||
/// 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 path: Vec<&PackageName> = Vec::new();
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
|
@ -269,12 +262,12 @@ impl<'env> DisplayDependencyGraph<'env> {
|
|||
if self.package.is_empty() {
|
||||
// The root nodes are those that are not required by any other package.
|
||||
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
|
||||
// with the current package as the root.
|
||||
if !children.contains(site_package.name()) {
|
||||
if !children.contains(&package.name) {
|
||||
path.clear();
|
||||
lines.extend(self.visit(site_package, &mut visited, &mut path)?);
|
||||
lines.extend(self.visit(package, &mut visited, &mut path));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -282,12 +275,14 @@ impl<'env> DisplayDependencyGraph<'env> {
|
|||
if index != 0 {
|
||||
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();
|
||||
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 run;
|
||||
pub(crate) mod sync;
|
||||
pub(crate) mod tree;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
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
|
||||
}
|
||||
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,
|
||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
||||
PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
|
||||
ToolUninstallArgs, VenvArgs,
|
||||
ToolUninstallArgs, TreeArgs, VenvArgs,
|
||||
};
|
||||
use uv_client::Connectivity;
|
||||
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.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[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)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PipTreeSettings {
|
||||
|
@ -1109,11 +1143,7 @@ impl PipTreeSettings {
|
|||
/// Resolve the [`PipTreeSettings`] from the CLI and workspace configuration.
|
||||
pub(crate) fn resolve(args: PipTreeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let PipTreeArgs {
|
||||
depth,
|
||||
prune,
|
||||
package,
|
||||
no_dedupe,
|
||||
invert,
|
||||
tree,
|
||||
strict,
|
||||
no_strict,
|
||||
python,
|
||||
|
@ -1123,11 +1153,11 @@ impl PipTreeSettings {
|
|||
} = args;
|
||||
|
||||
Self {
|
||||
depth,
|
||||
prune,
|
||||
package,
|
||||
no_dedupe,
|
||||
invert,
|
||||
depth: tree.depth,
|
||||
prune: tree.prune,
|
||||
no_dedupe: tree.no_dedupe,
|
||||
invert: tree.invert,
|
||||
package: tree.package,
|
||||
// Shared settings.
|
||||
shared: PipSettings::combine(
|
||||
PipOptions {
|
||||
|
|
|
@ -472,6 +472,14 @@ impl TestContext {
|
|||
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.
|
||||
pub fn clean(&self) -> Command {
|
||||
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