mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
implement --no-dedupe
for uv pip tree
(#4449)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary Resolves https://github.com/astral-sh/uv/issues/4439 partially. Implements for `uv pip tree`: - `--no-dedupe` flag, similar to `cargo tree --no-dedupe` . - denote dependency cycles with `(#)` and add a footnote if there's a cycle (using `(*)` would require keeping track of the cycle state, so opted to do this instead). <!-- What's the purpose of the change? What does it do, and why? --> ## Test Plan The existing tests pass + added a couple of tests to validate `--no-dedupe` behavior. <!-- How was it tested? -->
This commit is contained in:
parent
03cfdc2275
commit
c127632419
5 changed files with 232 additions and 24 deletions
|
@ -1385,6 +1385,14 @@ pub struct PipShowArgs {
|
|||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct PipTreeArgs {
|
||||
/// 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,
|
||||
|
||||
/// Validate the virtual environment, to detect packages with missing dependencies or other
|
||||
/// issues.
|
||||
#[arg(long, overrides_with("no_strict"))]
|
||||
|
|
|
@ -20,6 +20,7 @@ use pypi_types::VerbatimParsedUrl;
|
|||
|
||||
/// Display the installed packages in the current environment as a dependency tree.
|
||||
pub(crate) fn pip_tree(
|
||||
no_dedupe: bool,
|
||||
strict: bool,
|
||||
python: Option<&str>,
|
||||
system: bool,
|
||||
|
@ -43,7 +44,7 @@ pub(crate) fn pip_tree(
|
|||
// Build the installed index.
|
||||
let site_packages = SitePackages::from_executable(&environment)?;
|
||||
|
||||
let rendered_tree = DisplayDependencyGraph::new(&site_packages)
|
||||
let rendered_tree = DisplayDependencyGraph::new(&site_packages, no_dedupe)
|
||||
.render()
|
||||
.join("\n");
|
||||
writeln!(printer.stdout(), "{rendered_tree}").unwrap();
|
||||
|
@ -54,6 +55,9 @@ pub(crate) fn pip_tree(
|
|||
"(*) Package tree already displayed".italic()
|
||||
)?;
|
||||
}
|
||||
if rendered_tree.contains('#') {
|
||||
writeln!(printer.stdout(), "{}", "(#) Dependency cycle".italic())?;
|
||||
}
|
||||
|
||||
// Validate that the environment is consistent.
|
||||
if strict {
|
||||
|
@ -91,22 +95,6 @@ fn required_with_no_extra(dist: &InstalledDist) -> Vec<pep508_rs::Requirement<Ve
|
|||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
// Render the line for the given installed distribution in the dependency tree.
|
||||
fn render_line(installed_dist: &InstalledDist, is_visited: bool) -> String {
|
||||
let mut line = String::new();
|
||||
write!(
|
||||
&mut line,
|
||||
"{} v{}",
|
||||
installed_dist.name(),
|
||||
installed_dist.version()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if is_visited {
|
||||
line.push_str(" (*)");
|
||||
}
|
||||
line
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct DisplayDependencyGraph<'a> {
|
||||
site_packages: &'a SitePackages,
|
||||
|
@ -116,11 +104,14 @@ struct DisplayDependencyGraph<'a> {
|
|||
// It is used to determine the starting nodes when recursing the
|
||||
// dependency graph.
|
||||
required_packages: HashSet<PackageName>,
|
||||
|
||||
// Whether to de-duplicate the displayed dependencies.
|
||||
no_dedupe: bool,
|
||||
}
|
||||
|
||||
impl<'a> DisplayDependencyGraph<'a> {
|
||||
/// Create a new [`DisplayDependencyGraph`] for the set of installed distributions.
|
||||
fn new(site_packages: &'a SitePackages) -> DisplayDependencyGraph<'a> {
|
||||
fn new(site_packages: &'a SitePackages, no_dedupe: bool) -> DisplayDependencyGraph<'a> {
|
||||
let mut dist_by_package_name = HashMap::new();
|
||||
let mut required_packages = HashSet::new();
|
||||
for site_package in site_packages.iter() {
|
||||
|
@ -136,6 +127,7 @@ impl<'a> DisplayDependencyGraph<'a> {
|
|||
site_packages,
|
||||
dist_by_package_name,
|
||||
required_packages,
|
||||
no_dedupe,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,14 +138,22 @@ impl<'a> DisplayDependencyGraph<'a> {
|
|||
visited: &mut HashSet<String>,
|
||||
path: &mut Vec<String>,
|
||||
) -> Vec<String> {
|
||||
let mut lines = Vec::new();
|
||||
let package_name = installed_dist.name().to_string();
|
||||
let is_visited = visited.contains(&package_name);
|
||||
lines.push(render_line(installed_dist, is_visited));
|
||||
if is_visited {
|
||||
return lines;
|
||||
let line = format!("{} v{}", package_name, installed_dist.version());
|
||||
|
||||
if path.contains(&package_name) {
|
||||
return vec![format!("{} (#)", line)];
|
||||
}
|
||||
|
||||
// If the package has been visited and de-duplication is enabled (default),
|
||||
// skip the traversal.
|
||||
if is_visited && !self.no_dedupe {
|
||||
return vec![format!("{} (*)", line)];
|
||||
}
|
||||
|
||||
let mut lines = vec![line];
|
||||
|
||||
path.push(package_name.clone());
|
||||
visited.insert(package_name.clone());
|
||||
let required_packages = required_with_no_extra(installed_dist);
|
||||
|
|
|
@ -546,6 +546,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
let cache = cache.init()?;
|
||||
|
||||
commands::pip_tree(
|
||||
args.no_dedupe,
|
||||
args.shared.strict,
|
||||
args.shared.python.as_deref(),
|
||||
args.shared.system,
|
||||
|
|
|
@ -954,6 +954,7 @@ impl PipShowSettings {
|
|||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PipTreeSettings {
|
||||
pub(crate) no_dedupe: bool,
|
||||
// CLI-only settings.
|
||||
pub(crate) shared: PipSettings,
|
||||
}
|
||||
|
@ -962,6 +963,7 @@ impl PipTreeSettings {
|
|||
/// Resolve the [`PipTreeSettings`] from the CLI and workspace configuration.
|
||||
pub(crate) fn resolve(args: PipTreeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let PipTreeArgs {
|
||||
no_dedupe,
|
||||
strict,
|
||||
no_strict,
|
||||
python,
|
||||
|
@ -970,6 +972,7 @@ impl PipTreeSettings {
|
|||
} = args;
|
||||
|
||||
Self {
|
||||
no_dedupe,
|
||||
// Shared settings.
|
||||
shared: PipSettings::combine(
|
||||
PipOptions {
|
||||
|
|
|
@ -328,8 +328,8 @@ fn cyclic_dependency() {
|
|||
uv-cyclic-dependencies-c v0.1.0
|
||||
└── uv-cyclic-dependencies-a v0.1.0
|
||||
└── uv-cyclic-dependencies-b v0.1.0
|
||||
└── uv-cyclic-dependencies-a v0.1.0 (*)
|
||||
(*) Package tree already displayed
|
||||
└── uv-cyclic-dependencies-a v0.1.0 (#)
|
||||
(#) Dependency cycle
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
|
@ -541,6 +541,202 @@ fn multiple_packages_shared_descendant() {
|
|||
);
|
||||
}
|
||||
|
||||
// Ensure that --no-dedupe behaves as expected
|
||||
// in the presence of dependency cycles.
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn no_dedupe_and_cycle() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt
|
||||
.write_str(
|
||||
r"
|
||||
pendulum==3.0.0
|
||||
boto3==1.34.69
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
uv_snapshot!(install_command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 10 packages in [TIME]
|
||||
Prepared 10 packages in [TIME]
|
||||
Installed 10 packages in [TIME]
|
||||
+ boto3==1.34.69
|
||||
+ botocore==1.34.69
|
||||
+ jmespath==1.0.1
|
||||
+ pendulum==3.0.0
|
||||
+ python-dateutil==2.9.0.post0
|
||||
+ s3transfer==0.10.1
|
||||
+ six==1.16.0
|
||||
+ time-machine==2.14.1
|
||||
+ tzdata==2024.1
|
||||
+ urllib3==2.2.1
|
||||
|
||||
"###
|
||||
);
|
||||
|
||||
let mut command = Command::new(get_bin());
|
||||
command
|
||||
.arg("pip")
|
||||
.arg("install")
|
||||
.arg("uv-cyclic-dependencies-c==0.1.0")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.arg("--index-url")
|
||||
.arg("https://test.pypi.org/simple/")
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.env("UV_NO_WRAP", "1")
|
||||
.current_dir(&context.temp_dir);
|
||||
if cfg!(all(windows, debug_assertions)) {
|
||||
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
|
||||
// default windows stack of 1MB
|
||||
command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string());
|
||||
}
|
||||
|
||||
uv_snapshot!(context.filters(), command, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ uv-cyclic-dependencies-a==0.1.0
|
||||
+ uv-cyclic-dependencies-b==0.1.0
|
||||
+ uv-cyclic-dependencies-c==0.1.0
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.arg("--no-dedupe")
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.env("UV_NO_WRAP", "1")
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
boto3 v1.34.69
|
||||
├── botocore v1.34.69
|
||||
│ ├── jmespath v1.0.1
|
||||
│ └── python-dateutil v2.9.0.post0
|
||||
│ └── six v1.16.0
|
||||
├── jmespath v1.0.1
|
||||
└── s3transfer v0.10.1
|
||||
└── botocore v1.34.69
|
||||
├── jmespath v1.0.1
|
||||
└── python-dateutil v2.9.0.post0
|
||||
└── six v1.16.0
|
||||
pendulum v3.0.0
|
||||
├── python-dateutil v2.9.0.post0
|
||||
│ └── six v1.16.0
|
||||
└── tzdata v2024.1
|
||||
time-machine v2.14.1
|
||||
└── python-dateutil v2.9.0.post0
|
||||
└── six v1.16.0
|
||||
urllib3 v2.2.1
|
||||
uv-cyclic-dependencies-c v0.1.0
|
||||
└── uv-cyclic-dependencies-a v0.1.0
|
||||
└── uv-cyclic-dependencies-b v0.1.0
|
||||
└── uv-cyclic-dependencies-a v0.1.0 (#)
|
||||
(#) Dependency cycle
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn no_dedupe() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt
|
||||
.write_str(
|
||||
r"
|
||||
pendulum==3.0.0
|
||||
boto3==1.34.69
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
uv_snapshot!(install_command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 10 packages in [TIME]
|
||||
Prepared 10 packages in [TIME]
|
||||
Installed 10 packages in [TIME]
|
||||
+ boto3==1.34.69
|
||||
+ botocore==1.34.69
|
||||
+ jmespath==1.0.1
|
||||
+ pendulum==3.0.0
|
||||
+ python-dateutil==2.9.0.post0
|
||||
+ s3transfer==0.10.1
|
||||
+ six==1.16.0
|
||||
+ time-machine==2.14.1
|
||||
+ tzdata==2024.1
|
||||
+ urllib3==2.2.1
|
||||
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.arg("--no-dedupe")
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.env("UV_NO_WRAP", "1")
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
boto3 v1.34.69
|
||||
├── botocore v1.34.69
|
||||
│ ├── jmespath v1.0.1
|
||||
│ └── python-dateutil v2.9.0.post0
|
||||
│ └── six v1.16.0
|
||||
├── jmespath v1.0.1
|
||||
└── s3transfer v0.10.1
|
||||
└── botocore v1.34.69
|
||||
├── jmespath v1.0.1
|
||||
└── python-dateutil v2.9.0.post0
|
||||
└── six v1.16.0
|
||||
pendulum v3.0.0
|
||||
├── python-dateutil v2.9.0.post0
|
||||
│ └── six v1.16.0
|
||||
└── tzdata v2024.1
|
||||
time-machine v2.14.1
|
||||
└── python-dateutil v2.9.0.post0
|
||||
└── six v1.16.0
|
||||
urllib3 v2.2.1
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_editable() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue