mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Implement --invert
for pip tree
(#4621)
## Summary Part of https://github.com/astral-sh/uv/issues/4439. ## Test Plan Existing tests pass + added a couple of new tests with `--invert`.
This commit is contained in:
parent
0ee4a2cc6e
commit
5715def24b
5 changed files with 342 additions and 27 deletions
|
@ -1436,6 +1436,10 @@ pub struct PipTreeArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_dedupe: bool,
|
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.
|
||||||
#[arg(long, overrides_with("no_strict"))]
|
#[arg(long, overrides_with("no_strict"))]
|
||||||
|
|
|
@ -21,10 +21,12 @@ use crate::commands::ExitStatus;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
||||||
/// Display the installed packages in the current environment as a dependency tree.
|
/// Display the installed packages in the current environment as a dependency tree.
|
||||||
|
#[allow(clippy::fn_params_excessive_bools)]
|
||||||
pub(crate) fn pip_tree(
|
pub(crate) fn pip_tree(
|
||||||
depth: u8,
|
depth: u8,
|
||||||
prune: Vec<PackageName>,
|
prune: Vec<PackageName>,
|
||||||
no_dedupe: bool,
|
no_dedupe: bool,
|
||||||
|
invert: bool,
|
||||||
strict: bool,
|
strict: bool,
|
||||||
python: Option<&str>,
|
python: Option<&str>,
|
||||||
system: bool,
|
system: bool,
|
||||||
|
@ -52,6 +54,7 @@ pub(crate) fn pip_tree(
|
||||||
depth.into(),
|
depth.into(),
|
||||||
prune,
|
prune,
|
||||||
no_dedupe,
|
no_dedupe,
|
||||||
|
invert,
|
||||||
environment.interpreter().markers(),
|
environment.interpreter().markers(),
|
||||||
)?
|
)?
|
||||||
.render()?
|
.render()?
|
||||||
|
@ -112,18 +115,14 @@ struct DisplayDependencyGraph<'a> {
|
||||||
site_packages: &'a SitePackages,
|
site_packages: &'a SitePackages,
|
||||||
/// Map from package name to the installed distribution.
|
/// Map from package name to the installed distribution.
|
||||||
dist_by_package_name: HashMap<&'a PackageName, &'a InstalledDist>,
|
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>,
|
|
||||||
/// Maximum display depth of the dependency tree
|
/// Maximum display depth of the dependency tree
|
||||||
depth: usize,
|
depth: usize,
|
||||||
/// Prune the given package from the display of the dependency tree.
|
/// Prune the given package from the display of the dependency tree.
|
||||||
prune: Vec<PackageName>,
|
prune: Vec<PackageName>,
|
||||||
/// Whether to de-duplicate the displayed dependencies.
|
/// Whether to de-duplicate the displayed dependencies.
|
||||||
no_dedupe: bool,
|
no_dedupe: bool,
|
||||||
/// The marker environment for the current interpreter.
|
/// Map from package name to the list of required (reversed if --invert is given) packages.
|
||||||
markers: &'a MarkerEnvironment,
|
requires_map: HashMap<PackageName, Vec<PackageName>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DisplayDependencyGraph<'a> {
|
impl<'a> DisplayDependencyGraph<'a> {
|
||||||
|
@ -133,27 +132,37 @@ impl<'a> DisplayDependencyGraph<'a> {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
prune: Vec<PackageName>,
|
prune: Vec<PackageName>,
|
||||||
no_dedupe: bool,
|
no_dedupe: bool,
|
||||||
|
invert: bool,
|
||||||
markers: &'a MarkerEnvironment,
|
markers: &'a MarkerEnvironment,
|
||||||
) -> Result<DisplayDependencyGraph<'a>> {
|
) -> Result<DisplayDependencyGraph<'a>> {
|
||||||
let mut dist_by_package_name = HashMap::new();
|
let mut dist_by_package_name = HashMap::new();
|
||||||
let mut required_packages = HashSet::new();
|
let mut requires_map = HashMap::new();
|
||||||
for site_package in site_packages.iter() {
|
for site_package in site_packages.iter() {
|
||||||
dist_by_package_name.insert(site_package.name(), site_package);
|
dist_by_package_name.insert(site_package.name(), site_package);
|
||||||
}
|
}
|
||||||
for site_package in site_packages.iter() {
|
for site_package in site_packages.iter() {
|
||||||
for required in filtered_requirements(site_package, markers)? {
|
for required in filtered_requirements(site_package, markers)? {
|
||||||
required_packages.insert(required.name.clone());
|
if invert {
|
||||||
|
requires_map
|
||||||
|
.entry(required.name.clone())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(site_package.name().clone());
|
||||||
|
} else {
|
||||||
|
requires_map
|
||||||
|
.entry(site_package.name().clone())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(required.name.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
site_packages,
|
site_packages,
|
||||||
dist_by_package_name,
|
dist_by_package_name,
|
||||||
required_packages,
|
|
||||||
depth,
|
depth,
|
||||||
prune,
|
prune,
|
||||||
no_dedupe,
|
no_dedupe,
|
||||||
markers,
|
requires_map,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +170,7 @@ impl<'a> DisplayDependencyGraph<'a> {
|
||||||
fn visit(
|
fn visit(
|
||||||
&self,
|
&self,
|
||||||
installed_dist: &InstalledDist,
|
installed_dist: &InstalledDist,
|
||||||
visited: &mut FxHashMap<PackageName, Vec<Requirement<VerbatimParsedUrl>>>,
|
visited: &mut FxHashMap<PackageName, Vec<PackageName>>,
|
||||||
path: &mut Vec<PackageName>,
|
path: &mut Vec<PackageName>,
|
||||||
) -> Result<Vec<String>> {
|
) -> Result<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.
|
||||||
|
@ -185,21 +194,22 @@ impl<'a> DisplayDependencyGraph<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let requirements = filtered_requirements(installed_dist, self.markers)?
|
let requirements_before_filtering = self.requires_map.get(installed_dist.name());
|
||||||
.into_iter()
|
let requirements = match requirements_before_filtering {
|
||||||
.filter(|req| !self.prune.contains(&req.name))
|
Some(requirements) => requirements
|
||||||
.collect::<Vec<_>>();
|
.iter()
|
||||||
|
.filter(|req| {
|
||||||
|
// Skip if the current package is not one of the installed distributions.
|
||||||
|
!self.prune.contains(req) && self.dist_by_package_name.contains_key(req)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
let mut lines = vec![line];
|
let mut lines = vec![line];
|
||||||
|
|
||||||
visited.insert(package_name.clone(), requirements.clone());
|
visited.insert(package_name.clone(), requirements.clone());
|
||||||
path.push(package_name.clone());
|
path.push(package_name.clone());
|
||||||
for (index, req) in requirements.iter().enumerate() {
|
for (index, req) in requirements.iter().enumerate() {
|
||||||
// Skip if the current package is not one of the installed distributions.
|
|
||||||
if !self.dist_by_package_name.contains_key(&req.name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For sub-visited packages, add the prefix to make the tree display user-friendly.
|
// 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
|
// The key observation here is you can group the tree as follows when you're at the
|
||||||
// root of the tree:
|
// root of the tree:
|
||||||
|
@ -227,7 +237,7 @@ impl<'a> DisplayDependencyGraph<'a> {
|
||||||
|
|
||||||
let mut prefixed_lines = Vec::new();
|
let mut prefixed_lines = Vec::new();
|
||||||
for (visited_index, visited_line) in self
|
for (visited_index, visited_line) in self
|
||||||
.visit(self.dist_by_package_name[&req.name], visited, path)?
|
.visit(self.dist_by_package_name[req], visited, path)?
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
|
@ -250,16 +260,19 @@ impl<'a> DisplayDependencyGraph<'a> {
|
||||||
|
|
||||||
/// Depth-first traverse the nodes to render the tree.
|
/// Depth-first traverse the nodes to render the tree.
|
||||||
fn render(&self) -> Result<Vec<String>> {
|
fn render(&self) -> Result<Vec<String>> {
|
||||||
let mut visited: FxHashMap<PackageName, Vec<Requirement<VerbatimParsedUrl>>> =
|
let mut visited: FxHashMap<PackageName, Vec<PackageName>> = FxHashMap::default();
|
||||||
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();
|
||||||
|
|
||||||
// The starting nodes are the ones without incoming edges.
|
// The starting nodes are those that are not required by any other package.
|
||||||
|
let mut non_starting_nodes = HashSet::new();
|
||||||
|
for children in self.requires_map.values() {
|
||||||
|
non_starting_nodes.extend(children);
|
||||||
|
}
|
||||||
for site_package in self.site_packages.iter() {
|
for site_package in self.site_packages.iter() {
|
||||||
// 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 !self.required_packages.contains(site_package.name()) {
|
if !non_starting_nodes.contains(site_package.name()) {
|
||||||
lines.extend(self.visit(site_package, &mut visited, &mut path)?);
|
lines.extend(self.visit(site_package, &mut visited, &mut path)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -551,6 +551,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.depth,
|
args.depth,
|
||||||
args.prune,
|
args.prune,
|
||||||
args.no_dedupe,
|
args.no_dedupe,
|
||||||
|
args.invert,
|
||||||
args.shared.strict,
|
args.shared.strict,
|
||||||
args.shared.python.as_deref(),
|
args.shared.python.as_deref(),
|
||||||
args.shared.system,
|
args.shared.system,
|
||||||
|
|
|
@ -1067,6 +1067,7 @@ pub(crate) struct PipTreeSettings {
|
||||||
pub(crate) depth: u8,
|
pub(crate) depth: u8,
|
||||||
pub(crate) prune: Vec<PackageName>,
|
pub(crate) prune: Vec<PackageName>,
|
||||||
pub(crate) no_dedupe: bool,
|
pub(crate) no_dedupe: bool,
|
||||||
|
pub(crate) invert: bool,
|
||||||
// CLI-only settings.
|
// CLI-only settings.
|
||||||
pub(crate) shared: PipSettings,
|
pub(crate) shared: PipSettings,
|
||||||
}
|
}
|
||||||
|
@ -1078,6 +1079,7 @@ impl PipTreeSettings {
|
||||||
depth,
|
depth,
|
||||||
prune,
|
prune,
|
||||||
no_dedupe,
|
no_dedupe,
|
||||||
|
invert,
|
||||||
strict,
|
strict,
|
||||||
no_strict,
|
no_strict,
|
||||||
python,
|
python,
|
||||||
|
@ -1090,6 +1092,7 @@ impl PipTreeSettings {
|
||||||
depth,
|
depth,
|
||||||
prune,
|
prune,
|
||||||
no_dedupe,
|
no_dedupe,
|
||||||
|
invert,
|
||||||
// Shared settings.
|
// Shared settings.
|
||||||
shared: PipSettings::combine(
|
shared: PipSettings::combine(
|
||||||
PipOptions {
|
PipOptions {
|
||||||
|
|
|
@ -212,6 +212,103 @@ fn nested_dependencies() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// identical test as `--invert` since `--reverse` is simply an alias for `--invert`.
|
||||||
|
#[test]
|
||||||
|
fn reverse() {
|
||||||
|
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!(context
|
||||||
|
.pip_install()
|
||||||
|
.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(), tree_command(&context).arg("--reverse"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
joblib v1.3.2
|
||||||
|
└── scikit-learn v1.4.1.post1
|
||||||
|
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
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert() {
|
||||||
|
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!(context
|
||||||
|
.pip_install()
|
||||||
|
.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(), tree_command(&context).arg("--invert"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
joblib v1.3.2
|
||||||
|
└── scikit-learn v1.4.1.post1
|
||||||
|
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
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn depth() {
|
fn depth() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
@ -410,6 +507,128 @@ fn prune() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn nested_dependencies_more_complex_inverted() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||||
|
requirements_txt.write_str("packse").unwrap();
|
||||||
|
|
||||||
|
uv_snapshot!(context
|
||||||
|
.pip_install()
|
||||||
|
.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(), tree_command(&context).arg("--invert"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
certifi v2024.2.2
|
||||||
|
└── requests v2.31.0
|
||||||
|
├── requests-toolbelt v1.0.0
|
||||||
|
│ └── twine v4.0.2
|
||||||
|
│ └── packse v0.3.12
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
charset-normalizer v3.3.2
|
||||||
|
└── requests v2.31.0 (*)
|
||||||
|
chevron-blue v0.2.1
|
||||||
|
└── packse v0.3.12
|
||||||
|
docutils v0.20.1
|
||||||
|
└── readme-renderer v43.0
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
idna v3.6
|
||||||
|
└── requests v2.31.0 (*)
|
||||||
|
jaraco-context v4.3.0
|
||||||
|
└── keyring v25.0.0
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
mdurl v0.1.2
|
||||||
|
└── markdown-it-py v3.0.0
|
||||||
|
└── rich v13.7.1
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
more-itertools v10.2.0
|
||||||
|
├── jaraco-classes v3.3.1
|
||||||
|
│ └── keyring v25.0.0 (*)
|
||||||
|
└── jaraco-functools v4.0.0
|
||||||
|
└── keyring v25.0.0 (*)
|
||||||
|
msgspec v0.18.6
|
||||||
|
└── packse v0.3.12
|
||||||
|
nh3 v0.2.15
|
||||||
|
└── readme-renderer v43.0 (*)
|
||||||
|
packaging v24.0
|
||||||
|
└── hatchling v1.22.4
|
||||||
|
└── packse v0.3.12
|
||||||
|
pathspec v0.12.1
|
||||||
|
└── hatchling v1.22.4 (*)
|
||||||
|
pkginfo v1.10.0
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
pluggy v1.4.0
|
||||||
|
└── hatchling v1.22.4 (*)
|
||||||
|
pygments v2.17.2
|
||||||
|
├── readme-renderer v43.0 (*)
|
||||||
|
└── rich v13.7.1 (*)
|
||||||
|
rfc3986 v2.0.0
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
setuptools v69.2.0
|
||||||
|
└── packse v0.3.12
|
||||||
|
trove-classifiers v2024.3.3
|
||||||
|
└── hatchling v1.22.4 (*)
|
||||||
|
urllib3 v2.2.1
|
||||||
|
├── requests v2.31.0 (*)
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
zipp v3.18.1
|
||||||
|
└── importlib-metadata v7.1.0
|
||||||
|
└── twine v4.0.2 (*)
|
||||||
|
(*) Package tree already displayed
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn nested_dependencies_more_complex() {
|
fn nested_dependencies_more_complex() {
|
||||||
|
@ -852,6 +1071,81 @@ fn multiple_packages_shared_descendant() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test the interaction between `--no-dedupe` and `--invert`.
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn no_dedupe_and_invert() {
|
||||||
|
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!(context
|
||||||
|
.pip_install()
|
||||||
|
.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(), tree_command(&context).arg("--no-dedupe").arg("--invert"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
jmespath v1.0.1
|
||||||
|
├── boto3 v1.34.69
|
||||||
|
└── botocore v1.34.69
|
||||||
|
├── boto3 v1.34.69
|
||||||
|
└── s3transfer v0.10.1
|
||||||
|
└── boto3 v1.34.69
|
||||||
|
six v1.16.0
|
||||||
|
└── python-dateutil v2.9.0.post0
|
||||||
|
├── botocore v1.34.69
|
||||||
|
│ ├── boto3 v1.34.69
|
||||||
|
│ └── s3transfer v0.10.1
|
||||||
|
│ └── boto3 v1.34.69
|
||||||
|
├── pendulum v3.0.0
|
||||||
|
└── time-machine v2.14.1
|
||||||
|
└── pendulum v3.0.0
|
||||||
|
tzdata v2024.1
|
||||||
|
└── pendulum v3.0.0
|
||||||
|
urllib3 v2.2.1
|
||||||
|
└── botocore v1.34.69
|
||||||
|
├── boto3 v1.34.69
|
||||||
|
└── s3transfer v0.10.1
|
||||||
|
└── boto3 v1.34.69
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure that --no-dedupe behaves as expected
|
// Ensure that --no-dedupe behaves as expected
|
||||||
// in the presence of dependency cycles.
|
// in the presence of dependency cycles.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue