mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Add --outdated
support to uv pip tree
(#10199)
## Summary Closes https://github.com/astral-sh/uv/issues/10181.
This commit is contained in:
parent
49a2b6f85c
commit
1fb7f352b1
6 changed files with 238 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
@ -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(),
|
||||
|
@ -71,6 +149,7 @@ pub(crate) fn pip_tree(
|
|||
show_version_specifiers,
|
||||
&markers,
|
||||
&packages,
|
||||
&latest,
|
||||
)
|
||||
.render()
|
||||
.join("\n");
|
||||
|
@ -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.
|
||||
|
@ -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::<
|
||||
|
@ -258,6 +340,7 @@ impl<'env> DisplayDependencyGraph<'env> {
|
|||
Self {
|
||||
graph,
|
||||
roots,
|
||||
latest,
|
||||
depth,
|
||||
no_dedupe,
|
||||
invert,
|
||||
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
@ -2001,6 +2001,7 @@ impl PipTreeSettings {
|
|||
tree,
|
||||
strict,
|
||||
no_strict,
|
||||
fetch,
|
||||
python,
|
||||
system,
|
||||
no_system,
|
||||
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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 -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: <https://pypi.org/simple>).</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>
|
||||
|
@ -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’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 "dependency confusion" 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 "best" 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: <https://pypi.org/simple>).</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’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>
|
||||
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue