Skip to content

Commit

Permalink
Add --outdated support to uv pip tree (#10199)
Browse files Browse the repository at this point in the history
## Summary

Closes #10181.
  • Loading branch information
charliermarsh authored Dec 27, 2024
1 parent 49a2b6f commit 1fb7f35
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 11 deletions.
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 @@ -1989,8 +1989,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 @@ -2001,6 +2001,7 @@ impl PipTreeSettings {
tree,
strict,
no_strict,
fetch,
python,
system,
no_system,
Expand All @@ -2014,13 +2015,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 @@ -7450,6 +7450,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 @@ -7459,10 +7466,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 @@ -7480,6 +7548,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

0 comments on commit 1fb7f35

Please sign in to comment.