Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2112,6 +2112,9 @@ pub struct PipTreeArgs {
#[arg(long, overrides_with("strict"), hide = true)]
pub no_strict: bool,

#[command(flatten)]
pub fetch: FetchArgs,

/// The Python interpreter for which packages should be listed.
///
/// By default, uv lists packages in a virtual environment but will show
Expand Down
100 changes: 97 additions & 3 deletions crates/uv/src/commands/pip/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,54 @@ use std::collections::VecDeque;
use std::fmt::Write;

use anyhow::Result;
use futures::StreamExt;
use owo_colors::OwoColorize;
use petgraph::graph::{EdgeIndex, NodeIndex};
use petgraph::prelude::EdgeRef;
use petgraph::Direction;
use rustc_hash::{FxHashMap, FxHashSet};

use uv_cache::Cache;
use uv_distribution_types::{Diagnostic, Name};
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{Connectivity, RegistryClientBuilder};
use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType, TrustedHost};
use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, Name};
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_pep508::{Requirement, VersionOrUrl};
use uv_pypi_types::{ResolutionMetadata, ResolverMarkerEnvironment, VerbatimParsedUrl};
use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};
use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython};

use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::operations::report_target_environment;
use crate::commands::reporters::LatestVersionReporter;
use crate::commands::ExitStatus;
use crate::printer::Printer;

/// 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) async fn pip_tree(
show_version_specifiers: bool,
depth: u8,
prune: &[PackageName],
package: &[PackageName],
no_dedupe: bool,
invert: bool,
outdated: bool,
prerelease: PrereleaseMode,
index_locations: IndexLocations,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
allow_insecure_host: Vec<TrustedHost>,
connectivity: Connectivity,
concurrency: Concurrency,
strict: bool,
exclude_newer: Option<ExcludeNewer>,
python: Option<&str>,
system: bool,
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
Expand Down Expand Up @@ -61,6 +79,66 @@ pub(crate) fn pip_tree(
// Determine the markers to use for the resolution.
let markers = environment.interpreter().resolver_marker_environment();

// Determine the latest version for each package.
let latest = if outdated && !packages.is_empty() {
let capabilities = IndexCapabilities::default();

// Initialize the registry client.
let client =
RegistryClientBuilder::new(cache.clone().with_refresh(Refresh::All(Timestamp::now())))
.native_tls(native_tls)
.connectivity(connectivity)
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
.keyring(keyring_provider)
.allow_insecure_host(allow_insecure_host.clone())
.markers(environment.interpreter().markers())
.platform(environment.interpreter().platform())
.build();

// Determine the platform tags.
let interpreter = environment.interpreter();
let tags = interpreter.tags()?;
let requires_python =
RequiresPython::greater_than_equal_version(interpreter.python_full_version());

// Initialize the client to fetch the latest version of each package.
let client = LatestClient {
client: &client,
capabilities: &capabilities,
prerelease,
exclude_newer,
tags: Some(tags),
requires_python: &requires_python,
};

let reporter = LatestVersionReporter::from(printer).with_length(packages.len() as u64);

// Fetch the latest version for each package.
let mut fetches = futures::stream::iter(&packages)
.map(|(name, ..)| async {
let Some(filename) = client.find_latest(name, None).await? else {
return Ok(None);
};
Ok::<Option<_>, uv_client::Error>(Some((*name, filename.into_version())))
})
.buffer_unordered(concurrency.downloads);

let mut map = FxHashMap::default();
while let Some(entry) = fetches.next().await.transpose()? {
let Some((name, version)) = entry else {
reporter.on_fetch_progress();
continue;
};
reporter.on_fetch_version(name, &version);
map.insert(name, version);
}
reporter.on_fetch_complete();
map
} else {
FxHashMap::default()
};

// Render the tree.
let rendered_tree = DisplayDependencyGraph::new(
depth.into(),
Expand All @@ -71,6 +149,7 @@ pub(crate) fn pip_tree(
show_version_specifiers,
&markers,
&packages,
&latest,
)
.render()
.join("\n");
Expand Down Expand Up @@ -112,6 +191,8 @@ pub(crate) struct DisplayDependencyGraph<'env> {
>,
/// The packages considered as roots of the dependency tree.
roots: Vec<NodeIndex>,
/// The latest known version of each package.
latest: &'env FxHashMap<&'env PackageName, Version>,
/// Maximum display depth of the dependency tree
depth: usize,
/// Whether to de-duplicate the displayed dependencies.
Expand All @@ -133,6 +214,7 @@ impl<'env> DisplayDependencyGraph<'env> {
show_version_specifiers: bool,
markers: &ResolverMarkerEnvironment,
packages: &'env FxHashMap<&PackageName, Vec<ResolutionMetadata>>,
latest: &'env FxHashMap<&PackageName, Version>,
) -> Self {
// Create a graph.
let mut graph = petgraph::graph::Graph::<
Expand Down Expand Up @@ -258,6 +340,7 @@ impl<'env> DisplayDependencyGraph<'env> {
Self {
graph,
roots,
latest,
depth,
no_dedupe,
invert,
Expand Down Expand Up @@ -318,6 +401,17 @@ impl<'env> DisplayDependencyGraph<'env> {
}
}

// Incorporate the latest version of the package, if known.
let line = if let Some(version) = self
.latest
.get(package_name)
.filter(|&version| *version > metadata.version)
{
format!("{line} {}", format!("(latest: v{version})").bold().cyan())
} else {
line
};

let mut dependencies = self
.graph
.edges_directed(cursor.node(), Direction::Outgoing)
Expand Down
17 changes: 14 additions & 3 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,12 +671,23 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&args.package,
args.no_dedupe,
args.invert,
args.shared.strict,
args.shared.python.as_deref(),
args.shared.system,
args.outdated,
args.settings.prerelease,
args.settings.index_locations,
args.settings.index_strategy,
args.settings.keyring_provider,
globals.allow_insecure_host,
globals.connectivity,
globals.concurrency,
args.settings.strict,
args.settings.exclude_newer,
args.settings.python.as_deref(),
args.settings.system,
globals.native_tls,
&cache,
printer,
)
.await
}
Commands::Pip(PipNamespace {
command: PipCommand::Check(args),
Expand Down
11 changes: 6 additions & 5 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1981,8 +1981,8 @@ pub(crate) struct PipTreeSettings {
pub(crate) package: Vec<PackageName>,
pub(crate) no_dedupe: bool,
pub(crate) invert: bool,
// CLI-only settings.
pub(crate) shared: PipSettings,
pub(crate) outdated: bool,
pub(crate) settings: PipSettings,
}

impl PipTreeSettings {
Expand All @@ -1993,6 +1993,7 @@ impl PipTreeSettings {
tree,
strict,
no_strict,
fetch,
python,
system,
no_system,
Expand All @@ -2006,13 +2007,13 @@ impl PipTreeSettings {
no_dedupe: tree.no_dedupe,
invert: tree.invert,
package: tree.package,
// Shared settings.
shared: PipSettings::combine(
outdated: tree.outdated,
settings: PipSettings::combine(
PipOptions {
python: python.and_then(Maybe::into_option),
system: flag(system, no_system),
strict: flag(strict, no_strict),
..PipOptions::default()
..PipOptions::from(fetch)
},
filesystem,
),
Expand Down
48 changes: 48 additions & 0 deletions crates/uv/tests/it/pip_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1092,3 +1092,51 @@ fn print_output_even_with_quite_flag() {
"###
);
}

#[test]
fn outdated() {
let context = TestContext::new("3.12");

let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("flask==2.0.0").unwrap();

uv_snapshot!(context
.pip_install()
.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]
+ click==8.1.7
+ flask==2.0.0
+ itsdangerous==2.1.2
+ jinja2==3.1.3
+ markupsafe==2.1.5
+ werkzeug==3.0.1
"###
);

uv_snapshot!(
context.filters(),
context.pip_tree().arg("--outdated"), @r###"
success: true
exit_code: 0
----- stdout -----
flask v2.0.0 (latest: v3.0.2)
├── click v8.1.7
├── itsdangerous v2.1.2
├── jinja2 v3.1.3
│ └── markupsafe v2.1.5
└── werkzeug v3.0.1
└── markupsafe v2.1.5

----- stderr -----
"###
);
}
70 changes: 70 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7446,6 +7446,13 @@ uv pip tree [OPTIONS]
<p>While uv configuration can be included in a <code>pyproject.toml</code> file, it is not allowed in this context.</p>

<p>May also be set with the <code>UV_CONFIG_FILE</code> environment variable.</p>
</dd><dt><code>--default-index</code> <i>default-index</i></dt><dd><p>The URL of the default package index (by default: &lt;https://pypi.org/simple&gt;).</p>

<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>

<p>The index given by this flag is given lower priority than all other indexes specified via the <code>--index</code> flag.</p>

<p>May also be set with the <code>UV_DEFAULT_INDEX</code> environment variable.</p>
</dd><dt><code>--depth</code>, <code>-d</code> <i>depth</i></dt><dd><p>Maximum display depth of the dependency tree</p>

<p>[default: 255]</p>
Expand All @@ -7455,10 +7462,71 @@ uv pip tree [OPTIONS]

<p>See <code>--project</code> to only change the project root directory.</p>

</dd><dt><code>--exclude-newer</code> <i>exclude-newer</i></dt><dd><p>Limit candidate packages to those that were uploaded prior to the given date.</p>

<p>Accepts both RFC 3339 timestamps (e.g., <code>2006-12-02T02:07:43Z</code>) and local dates in the same format (e.g., <code>2006-12-02</code>) in your system&#8217;s configured time zone.</p>

<p>May also be set with the <code>UV_EXCLUDE_NEWER</code> environment variable.</p>
</dd><dt><code>--extra-index-url</code> <i>extra-index-url</i></dt><dd><p>(Deprecated: use <code>--index</code> instead) Extra URLs of package indexes to use, in addition to <code>--index-url</code>.</p>

<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>

<p>All indexes provided via this flag take priority over the index specified by <code>--index-url</code> (which defaults to PyPI). When multiple <code>--extra-index-url</code> flags are provided, earlier values take priority.</p>

<p>May also be set with the <code>UV_EXTRA_INDEX_URL</code> environment variable.</p>
</dd><dt><code>--find-links</code>, <code>-f</code> <i>find-links</i></dt><dd><p>Locations to search for candidate distributions, in addition to those found in the registry indexes.</p>

<p>If a path, the target must be a directory that contains packages as wheel files (<code>.whl</code>) or source distributions (e.g., <code>.tar.gz</code> or <code>.zip</code>) at the top level.</p>

<p>If a URL, the page must contain a flat list of links to package files adhering to the formats described above.</p>

<p>May also be set with the <code>UV_FIND_LINKS</code> environment variable.</p>
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>

</dd><dt><code>--index</code> <i>index</i></dt><dd><p>The URLs to use when resolving dependencies, in addition to the default index.</p>

<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>

<p>All indexes provided via this flag take priority over the index specified by <code>--default-index</code> (which defaults to PyPI). When multiple <code>--index</code> flags are provided, earlier values take priority.</p>

<p>May also be set with the <code>UV_INDEX</code> environment variable.</p>
</dd><dt><code>--index-strategy</code> <i>index-strategy</i></dt><dd><p>The strategy to use when resolving against multiple index URLs.</p>

<p>By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (<code>first-index</code>). This prevents &quot;dependency confusion&quot; attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.</p>

<p>May also be set with the <code>UV_INDEX_STRATEGY</code> environment variable.</p>
<p>Possible values:</p>

<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>

<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>

<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
</ul>
</dd><dt><code>--index-url</code>, <code>-i</code> <i>index-url</i></dt><dd><p>(Deprecated: use <code>--default-index</code> instead) The URL of the Python package index (by default: &lt;https://pypi.org/simple&gt;).</p>

<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>

<p>The index given by this flag is given lower priority than all other indexes specified via the <code>--extra-index-url</code> flag.</p>

<p>May also be set with the <code>UV_INDEX_URL</code> environment variable.</p>
</dd><dt><code>--invert</code></dt><dd><p>Show the reverse dependencies for the given package. This flag will invert the tree and display the packages that depend on the given package</p>

</dd><dt><code>--keyring-provider</code> <i>keyring-provider</i></dt><dd><p>Attempt to use <code>keyring</code> for authentication for index URLs.</p>

<p>At present, only <code>--keyring-provider subprocess</code> is supported, which configures uv to use the <code>keyring</code> CLI to handle authentication.</p>

<p>Defaults to <code>disabled</code>.</p>

<p>May also be set with the <code>UV_KEYRING_PROVIDER</code> environment variable.</p>
<p>Possible values:</p>

<ul>
<li><code>disabled</code>: Do not use keyring for credential lookup</li>

<li><code>subprocess</code>: Use the <code>keyring</code> command for credential lookup</li>
</ul>
</dd><dt><code>--native-tls</code></dt><dd><p>Whether to load TLS certificates from the platform&#8217;s native certificate store.</p>

<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>
Expand All @@ -7476,6 +7544,8 @@ uv pip tree [OPTIONS]
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p>
</dd><dt><code>--no-dedupe</code></dt><dd><p>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</p>

</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>

</dd><dt><code>--no-progress</code></dt><dd><p>Hide all progress outputs.</p>

<p>For example, spinners or progress bars.</p>
Expand Down