implement uv pip tree (#3859)
## Summary resolves https://github.com/astral-sh/uv/issues/3272 added it as a new subcommand rather than a flag on an existing command since that seems more consistent with `cargo tree` + cleaner code organization, but can make changes if it's preferred the other way.
This commit is contained in:
@@ -227,6 +227,8 @@ pub(crate) enum PipCommand {
|
||||
List(PipListArgs),
|
||||
/// Show information about one or more installed packages.
|
||||
Show(PipShowArgs),
|
||||
/// Display the dependency tree.
|
||||
Tree(PipTreeArgs),
|
||||
/// Verify installed packages have compatible dependencies.
|
||||
Check(PipCheckArgs),
|
||||
}
|
||||
@@ -1344,6 +1346,52 @@ pub(crate) struct PipShowArgs {
|
||||
pub(crate) no_system: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub(crate) struct PipTreeArgs {
|
||||
/// Validate the virtual environment, to detect packages with missing dependencies or other
|
||||
/// issues.
|
||||
#[arg(long, overrides_with("no_strict"))]
|
||||
pub(crate) strict: bool,
|
||||
|
||||
#[arg(long, overrides_with("strict"), hide = true)]
|
||||
pub(crate) no_strict: bool,
|
||||
|
||||
/// The Python interpreter for which packages should be listed.
|
||||
///
|
||||
/// By default, `uv` lists packages in the currently activated virtual environment, or a virtual
|
||||
/// environment (`.venv`) located in the current working directory or any parent directory,
|
||||
/// falling back to the system Python if no virtual environment is found.
|
||||
///
|
||||
/// 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(crate) python: Option<String>,
|
||||
|
||||
/// List packages for the system Python.
|
||||
///
|
||||
/// By default, `uv` lists packages in the currently activated virtual environment, or a virtual
|
||||
/// environment (`.venv`) located in the current working directory or any parent directory,
|
||||
/// falling back to the system Python if no virtual environment is found. The `--system` option
|
||||
/// instructs `uv` to use the first Python found in the system `PATH`.
|
||||
///
|
||||
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and
|
||||
/// should be used with caution.
|
||||
#[arg(
|
||||
long,
|
||||
env = "UV_SYSTEM_PYTHON",
|
||||
value_parser = clap::builder::BoolishValueParser::new(),
|
||||
overrides_with("no_system")
|
||||
)]
|
||||
pub(crate) system: bool,
|
||||
|
||||
#[arg(long, overrides_with("system"))]
|
||||
pub(crate) no_system: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub(crate) struct VenvArgs {
|
||||
|
||||
@@ -15,6 +15,7 @@ pub(crate) use pip::install::pip_install;
|
||||
pub(crate) use pip::list::pip_list;
|
||||
pub(crate) use pip::show::pip_show;
|
||||
pub(crate) use pip::sync::pip_sync;
|
||||
pub(crate) use pip::tree::pip_tree;
|
||||
pub(crate) use pip::uninstall::pip_uninstall;
|
||||
pub(crate) use project::add::add;
|
||||
pub(crate) use project::lock::lock;
|
||||
|
||||
@@ -13,6 +13,7 @@ pub(crate) mod list;
|
||||
pub(crate) mod operations;
|
||||
pub(crate) mod show;
|
||||
pub(crate) mod sync;
|
||||
pub(crate) mod tree;
|
||||
pub(crate) mod uninstall;
|
||||
|
||||
// Determine the tags, markers, and interpreter to use for resolution.
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use distribution_types::{Diagnostic, InstalledDist, Name};
|
||||
use owo_colors::OwoColorize;
|
||||
use tracing::debug;
|
||||
use uv_cache::Cache;
|
||||
use uv_configuration::PreviewMode;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_toolchain::EnvironmentPreference;
|
||||
use uv_toolchain::PythonEnvironment;
|
||||
use uv_toolchain::ToolchainRequest;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
|
||||
/// Display the installed packages in the current environment as a dependency tree.
|
||||
pub(crate) fn pip_tree(
|
||||
strict: bool,
|
||||
python: Option<&str>,
|
||||
system: bool,
|
||||
_preview: PreviewMode,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<ExitStatus> {
|
||||
// Detect the current Python interpreter.
|
||||
let environment = PythonEnvironment::find(
|
||||
&python.map(ToolchainRequest::parse).unwrap_or_default(),
|
||||
EnvironmentPreference::from_system_flag(system, false),
|
||||
cache,
|
||||
)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
environment.interpreter().python_version(),
|
||||
environment.python_executable().user_display().cyan()
|
||||
);
|
||||
|
||||
// Build the installed index.
|
||||
let site_packages = SitePackages::from_executable(&environment)?;
|
||||
|
||||
let rendered_tree = DisplayDependencyGraph::new(&site_packages)
|
||||
.render()
|
||||
.join("\n");
|
||||
writeln!(printer.stdout(), "{rendered_tree}").unwrap();
|
||||
if rendered_tree.contains('*') {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
r#"{}: (*) indicates the package has been `de-duplicated`.
|
||||
The dependencies for the package have already been shown elsewhere in the graph, and so are not repeated."#,
|
||||
"Note".yellow().bold()
|
||||
)?;
|
||||
}
|
||||
|
||||
// Validate that the environment is consistent.
|
||||
if strict {
|
||||
for diagnostic in site_packages.diagnostics()? {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
diagnostic.message().bold()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
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 required_with_no_extra(dist: &InstalledDist) -> Vec<pep508_rs::Requirement<VerbatimParsedUrl>> {
|
||||
let metadata = dist.metadata().unwrap();
|
||||
return metadata
|
||||
.requires_dist
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
r.marker.is_none()
|
||||
|| !r
|
||||
.marker
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.evaluate_optional_environment(None, &metadata.provides_extras[..])
|
||||
})
|
||||
.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,
|
||||
// Map from package name to the installed distribution.
|
||||
dist_by_package_name: HashMap<&'a PackageName, &'a InstalledDist>,
|
||||
// Set of package names that are required by at least one installed distribution.
|
||||
// It is used to determine the starting nodes when recursing the
|
||||
// dependency graph.
|
||||
required_packages: HashSet<PackageName>,
|
||||
}
|
||||
|
||||
impl<'a> DisplayDependencyGraph<'a> {
|
||||
/// Create a new [`DisplayDependencyGraph`] for the set of installed distributions.
|
||||
fn new(site_packages: &'a SitePackages) -> DisplayDependencyGraph<'a> {
|
||||
let mut dist_by_package_name = HashMap::new();
|
||||
let mut required_packages = HashSet::new();
|
||||
for site_package in site_packages.iter() {
|
||||
dist_by_package_name.insert(site_package.name(), site_package);
|
||||
}
|
||||
for site_package in site_packages.iter() {
|
||||
for required in required_with_no_extra(site_package) {
|
||||
required_packages.insert(required.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
site_packages,
|
||||
dist_by_package_name,
|
||||
required_packages,
|
||||
}
|
||||
}
|
||||
|
||||
// Depth-first traversal of the given distribution and its dependencies.
|
||||
fn visit(
|
||||
&self,
|
||||
installed_dist: &InstalledDist,
|
||||
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;
|
||||
}
|
||||
|
||||
path.push(package_name.clone());
|
||||
visited.insert(package_name.clone());
|
||||
let required_packages = required_with_no_extra(installed_dist);
|
||||
for (index, required_package) in required_packages.iter().enumerate() {
|
||||
// Skip if the current package is not one of the installed distributions.
|
||||
if !self
|
||||
.dist_by_package_name
|
||||
.contains_key(&required_package.name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// For sub-visited packages, add the prefix to make the tree display user-friendly.
|
||||
// The key observation here is you can group the tree as follows when you're at the
|
||||
// root of the tree:
|
||||
// root_package
|
||||
// ├── level_1_0 // Group 1
|
||||
// │ ├── level_2_0 ...
|
||||
// │ │ ├── level_3_0 ...
|
||||
// │ │ └── level_3_1 ...
|
||||
// │ └── level_2_1 ...
|
||||
// ├── level_1_1 // Group 2
|
||||
// │ ├── level_2_2 ...
|
||||
// │ └── level_2_3 ...
|
||||
// └── level_1_2 // Group 3
|
||||
// └── level_2_4 ...
|
||||
//
|
||||
// The lines in Group 1 and 2 have `├── ` at the top and `| ` at the rest while
|
||||
// those in Group 3 have `└── ` at the top and ` ` at the rest.
|
||||
// This observation is true recursively even when looking at the subtree rooted
|
||||
// at `level_1_0`.
|
||||
let (prefix_top, prefix_rest) = if required_packages.len() - 1 == index {
|
||||
("└── ", " ")
|
||||
} else {
|
||||
("├── ", "│ ")
|
||||
};
|
||||
|
||||
let mut prefixed_lines = Vec::new();
|
||||
for (visited_index, visited_line) in self
|
||||
.visit(
|
||||
self.dist_by_package_name[&required_package.name],
|
||||
visited,
|
||||
path,
|
||||
)
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
prefixed_lines.push(format!(
|
||||
"{}{}",
|
||||
if visited_index == 0 {
|
||||
prefix_top
|
||||
} else {
|
||||
prefix_rest
|
||||
},
|
||||
visited_line
|
||||
));
|
||||
}
|
||||
lines.extend(prefixed_lines);
|
||||
}
|
||||
path.pop();
|
||||
lines
|
||||
}
|
||||
|
||||
// Depth-first traverse the nodes to render the tree.
|
||||
// The starting nodes are the ones without incoming edges.
|
||||
fn render(&self) -> Vec<String> {
|
||||
let mut visited: HashSet<String> = HashSet::new();
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
for site_package in self.site_packages.iter() {
|
||||
// If the current package is not required by any other package, start the traversal
|
||||
// with the current package as the root.
|
||||
if !self.required_packages.contains(site_package.name()) {
|
||||
lines.extend(self.visit(site_package, &mut visited, &mut Vec::new()));
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use anyhow::Result;
|
||||
use clap::error::{ContextKind, ContextValue};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use owo_colors::OwoColorize;
|
||||
use settings::PipTreeSettings;
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use cli::{ToolCommand, ToolNamespace, ToolchainCommand, ToolchainNamespace};
|
||||
@@ -105,6 +106,12 @@ async fn run() -> Result<ExitStatus> {
|
||||
ContextValue::String("uv pip show".to_string()),
|
||||
);
|
||||
}
|
||||
"tree" => {
|
||||
err.insert(
|
||||
ContextKind::SuggestedSubcommand,
|
||||
ContextValue::String("uv pip tree".to_string()),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -530,6 +537,24 @@ async fn run() -> Result<ExitStatus> {
|
||||
printer,
|
||||
)
|
||||
}
|
||||
Commands::Pip(PipNamespace {
|
||||
command: PipCommand::Tree(args),
|
||||
}) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = PipTreeSettings::resolve(args, filesystem);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
|
||||
commands::pip_tree(
|
||||
args.shared.strict,
|
||||
args.shared.python.as_deref(),
|
||||
args.shared.system,
|
||||
globals.preview,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
}
|
||||
Commands::Pip(PipNamespace {
|
||||
command: PipCommand::Check(args),
|
||||
}) => {
|
||||
|
||||
@@ -27,9 +27,9 @@ use uv_toolchain::{Prefix, PythonVersion, Target, ToolchainPreference};
|
||||
use crate::cli::{
|
||||
AddArgs, BuildArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, IndexArgs,
|
||||
InstallerArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs,
|
||||
PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RefreshArgs, RemoveArgs, ResolverArgs,
|
||||
ResolverInstallerArgs, RunArgs, SyncArgs, ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs,
|
||||
ToolchainListArgs, VenvArgs,
|
||||
PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, RefreshArgs, RemoveArgs,
|
||||
ResolverArgs, ResolverInstallerArgs, RunArgs, SyncArgs, ToolRunArgs, ToolchainFindArgs,
|
||||
ToolchainInstallArgs, ToolchainListArgs, VenvArgs,
|
||||
};
|
||||
use crate::commands::pip::operations::Modifications;
|
||||
use crate::commands::ListFormat;
|
||||
@@ -948,6 +948,40 @@ impl PipShowSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `pip show` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PipTreeSettings {
|
||||
// CLI-only settings.
|
||||
pub(crate) shared: PipSettings,
|
||||
}
|
||||
|
||||
impl PipTreeSettings {
|
||||
/// Resolve the [`PipTreeSettings`] from the CLI and workspace configuration.
|
||||
pub(crate) fn resolve(args: PipTreeArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let PipTreeArgs {
|
||||
strict,
|
||||
no_strict,
|
||||
python,
|
||||
system,
|
||||
no_system,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
// Shared settings.
|
||||
shared: PipSettings::combine(
|
||||
PipOptions {
|
||||
python,
|
||||
system: flag(system, no_system),
|
||||
strict: flag(strict, no_strict),
|
||||
..PipOptions::default()
|
||||
},
|
||||
filesystem,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `pip check` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
use std::process::Command;
|
||||
|
||||
use assert_fs::fixture::FileWriteStr;
|
||||
use assert_fs::fixture::PathChild;
|
||||
|
||||
use common::uv_snapshot;
|
||||
|
||||
use crate::common::{get_bin, TestContext, EXCLUDE_NEWER};
|
||||
|
||||
mod common;
|
||||
|
||||
/// Create a `pip install` command with options shared across scenarios.
|
||||
fn install_command(context: &TestContext) -> Command {
|
||||
let mut command = Command::new(get_bin());
|
||||
command
|
||||
.arg("pip")
|
||||
.arg("install")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.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());
|
||||
}
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_package() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_package() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str("requests==2.31.0").unwrap();
|
||||
|
||||
uv_snapshot!(install_command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ certifi==2024.2.2
|
||||
+ charset-normalizer==3.3.2
|
||||
+ idna==3.6
|
||||
+ requests==2.31.0
|
||||
+ urllib3==2.2.1
|
||||
"###
|
||||
);
|
||||
|
||||
context.assert_command("import requests").success();
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
requests v2.31.0
|
||||
├── charset-normalizer v3.3.2
|
||||
├── idna v3.6
|
||||
├── urllib3 v2.2.1
|
||||
└── certifi v2024.2.2
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_dependencies() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt
|
||||
.write_str("scikit-learn==1.4.1.post1")
|
||||
.unwrap();
|
||||
|
||||
uv_snapshot!(install_command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ joblib==1.3.2
|
||||
+ numpy==1.26.4
|
||||
+ scikit-learn==1.4.1.post1
|
||||
+ scipy==1.12.0
|
||||
+ threadpoolctl==3.4.0
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
scikit-learn v1.4.1.post1
|
||||
├── numpy v1.26.4
|
||||
├── scipy v1.12.0
|
||||
│ └── numpy v1.26.4 (*)
|
||||
├── joblib v1.3.2
|
||||
└── threadpoolctl v3.4.0
|
||||
Note: (*) indicates the package has been `de-duplicated`.
|
||||
The dependencies for the package have already been shown elsewhere in the graph, and so are not repeated.
|
||||
|
||||
----- stderr -----
|
||||
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn nested_dependencies_more_complex() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str("packse").unwrap();
|
||||
|
||||
uv_snapshot!(install_command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 32 packages in [TIME]
|
||||
Prepared 32 packages in [TIME]
|
||||
Installed 32 packages in [TIME]
|
||||
+ certifi==2024.2.2
|
||||
+ charset-normalizer==3.3.2
|
||||
+ chevron-blue==0.2.1
|
||||
+ docutils==0.20.1
|
||||
+ hatchling==1.22.4
|
||||
+ idna==3.6
|
||||
+ importlib-metadata==7.1.0
|
||||
+ jaraco-classes==3.3.1
|
||||
+ jaraco-context==4.3.0
|
||||
+ jaraco-functools==4.0.0
|
||||
+ keyring==25.0.0
|
||||
+ markdown-it-py==3.0.0
|
||||
+ mdurl==0.1.2
|
||||
+ more-itertools==10.2.0
|
||||
+ msgspec==0.18.6
|
||||
+ nh3==0.2.15
|
||||
+ packaging==24.0
|
||||
+ packse==0.3.12
|
||||
+ pathspec==0.12.1
|
||||
+ pkginfo==1.10.0
|
||||
+ pluggy==1.4.0
|
||||
+ pygments==2.17.2
|
||||
+ readme-renderer==43.0
|
||||
+ requests==2.31.0
|
||||
+ requests-toolbelt==1.0.0
|
||||
+ rfc3986==2.0.0
|
||||
+ rich==13.7.1
|
||||
+ setuptools==69.2.0
|
||||
+ trove-classifiers==2024.3.3
|
||||
+ twine==4.0.2
|
||||
+ urllib3==2.2.1
|
||||
+ zipp==3.18.1
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
packse v0.3.12
|
||||
├── chevron-blue v0.2.1
|
||||
├── hatchling v1.22.4
|
||||
│ ├── packaging v24.0
|
||||
│ ├── pathspec v0.12.1
|
||||
│ ├── pluggy v1.4.0
|
||||
│ └── trove-classifiers v2024.3.3
|
||||
├── msgspec v0.18.6
|
||||
├── setuptools v69.2.0
|
||||
└── twine v4.0.2
|
||||
├── pkginfo v1.10.0
|
||||
├── readme-renderer v43.0
|
||||
│ ├── nh3 v0.2.15
|
||||
│ ├── docutils v0.20.1
|
||||
│ └── pygments v2.17.2
|
||||
├── requests v2.31.0
|
||||
│ ├── charset-normalizer v3.3.2
|
||||
│ ├── idna v3.6
|
||||
│ ├── urllib3 v2.2.1
|
||||
│ └── certifi v2024.2.2
|
||||
├── requests-toolbelt v1.0.0
|
||||
│ └── requests v2.31.0 (*)
|
||||
├── urllib3 v2.2.1 (*)
|
||||
├── importlib-metadata v7.1.0
|
||||
│ └── zipp v3.18.1
|
||||
├── keyring v25.0.0
|
||||
│ ├── jaraco-classes v3.3.1
|
||||
│ │ └── more-itertools v10.2.0
|
||||
│ ├── jaraco-functools v4.0.0
|
||||
│ │ └── more-itertools v10.2.0 (*)
|
||||
│ └── jaraco-context v4.3.0
|
||||
├── rfc3986 v2.0.0
|
||||
└── rich v13.7.1
|
||||
├── markdown-it-py v3.0.0
|
||||
│ └── mdurl v0.1.2
|
||||
└── pygments v2.17.2 (*)
|
||||
Note: (*) indicates the package has been `de-duplicated`.
|
||||
The dependencies for the package have already been shown elsewhere in the graph, and so are not repeated.
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure `pip tree` behaves correctly with a package that has a cyclic dependency.
|
||||
// package `uv-cyclic-dependencies-a` and `uv-cyclic-dependencies-b` depend on each other,
|
||||
// which creates a dependency cycle.
|
||||
// Additionally, package `uv-cyclic-dependencies-c` is included (depends on `uv-cyclic-dependencies-a`)
|
||||
// to make this test case more realistic and meaningful.
|
||||
#[test]
|
||||
fn cyclic_dependency() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt
|
||||
.write_str("uv-cyclic-dependencies-c")
|
||||
.unwrap();
|
||||
|
||||
let mut command = Command::new(get_bin());
|
||||
command
|
||||
.arg("pip")
|
||||
.arg("install")
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.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())
|
||||
.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 -----
|
||||
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 (*)
|
||||
Note: (*) indicates the package has been `de-duplicated`.
|
||||
The dependencies for the package have already been shown elsewhere in the graph, and so are not repeated.
|
||||
|
||||
----- stderr -----
|
||||
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure `pip tree` behaves correctly after a package has been removed.
|
||||
#[test]
|
||||
fn removed_dependency() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str("requests==2.31.0").unwrap();
|
||||
|
||||
uv_snapshot!(install_command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ certifi==2024.2.2
|
||||
+ charset-normalizer==3.3.2
|
||||
+ idna==3.6
|
||||
+ requests==2.31.0
|
||||
+ urllib3==2.2.1
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("uninstall")
|
||||
.arg("requests")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
|
||||
----- stderr -----
|
||||
Uninstalled 1 package in [TIME]
|
||||
- requests==2.31.0
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
certifi v2024.2.2
|
||||
charset-normalizer v3.3.2
|
||||
idna v3.6
|
||||
urllib3 v2.2.1
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_packages() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt
|
||||
.write_str(
|
||||
r"
|
||||
requests==2.31.0
|
||||
click==8.1.7
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
uv_snapshot!(install_command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 6 packages in [TIME]
|
||||
Installed 6 packages in [TIME]
|
||||
+ certifi==2024.2.2
|
||||
+ charset-normalizer==3.3.2
|
||||
+ click==8.1.7
|
||||
+ idna==3.6
|
||||
+ requests==2.31.0
|
||||
+ urllib3==2.2.1
|
||||
"###
|
||||
);
|
||||
|
||||
let mut filters = context.filters();
|
||||
if cfg!(windows) {
|
||||
filters.push(("colorama v0.4.6\n", ""));
|
||||
}
|
||||
context.assert_command("import requests").success();
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
click v8.1.7
|
||||
requests v2.31.0
|
||||
├── charset-normalizer v3.3.2
|
||||
├── idna v3.6
|
||||
├── urllib3 v2.2.1
|
||||
└── certifi v2024.2.2
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
// Both `pendulum` and `boto3` depend on `python-dateutil`.
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn multiple_packages_shared_descendant() {
|
||||
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())
|
||||
.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 (*)
|
||||
pendulum v3.0.0
|
||||
├── python-dateutil v2.9.0.post0 (*)
|
||||
└── tzdata v2024.1
|
||||
time-machine v2.14.1
|
||||
└── python-dateutil v2.9.0.post0 (*)
|
||||
urllib3 v2.2.1
|
||||
Note: (*) indicates the package has been `de-duplicated`.
|
||||
The dependencies for the package have already been shown elsewhere in the graph, and so are not repeated.
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_editable() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Install the editable package.
|
||||
uv_snapshot!(context.filters(), install_command(&context)
|
||||
.arg("-e")
|
||||
.arg(context.workspace_root.join("scripts/packages/hatchling_editable")), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ hatchling-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/hatchling_editable)
|
||||
+ iniconfig==2.0.1.dev6+g9cae431 (from git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4)
|
||||
"###
|
||||
);
|
||||
|
||||
let filters = context
|
||||
.filters()
|
||||
.into_iter()
|
||||
.chain(vec![(r"\-\-\-\-\-\-+.*", "[UNDERLINE]"), (" +", " ")])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("tree")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.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 -----
|
||||
hatchling-editable v0.1.0
|
||||
└── iniconfig v2.0.1.dev6+g9cae431
|
||||
|
||||
----- stderr -----
|
||||
|
||||
"###
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user