Add uv tree --outdated (#8893)

## Summary

Similar to `pip list --outdated`, but for `uv tree`.

## Test Plan

Looks like:

```
foo v0.1.0
└── flask v2.0.0 (latest: v3.0.3)
    ├── click v8.1.7
    ├── itsdangerous v2.2.0
    ├── jinja2 v3.1.4
    │   └── markupsafe v3.0.2
    └── werkzeug v3.1.2
        └── markupsafe v3.0.2
```

With `(latest: v3.0.3)` in bold cyan.
This commit is contained in:
Charlie Marsh 2024-11-07 15:10:46 -05:00 committed by GitHub
parent 88331e756e
commit 29e1b15473
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 328 additions and 118 deletions

View file

@ -4802,6 +4802,10 @@ pub struct DisplayTreeArgs {
/// display the packages that depend on the given package. /// display the packages that depend on the given package.
#[arg(long, alias = "reverse")] #[arg(long, alias = "reverse")]
pub invert: bool, pub invert: bool,
/// Show the latest available version of each package in the tree.
#[arg(long)]
pub outdated: bool,
} }
#[derive(Args, Debug)] #[derive(Args, Debug)]

View file

@ -68,6 +68,13 @@ impl DistFilename {
} }
} }
pub fn into_version(self) -> Version {
match self {
Self::SourceDistFilename(filename) => filename.version,
Self::WheelFilename(filename) => filename.version,
}
}
/// Whether the file is a `bdist_wheel` or an `sdist`. /// Whether the file is a `bdist_wheel` or an `sdist`.
pub fn filetype(&self) -> &'static str { pub fn filetype(&self) -> &'static str {
match self { match self {

View file

@ -4,8 +4,8 @@ pub use exclude_newer::ExcludeNewer;
pub use exclusions::Exclusions; pub use exclusions::Exclusions;
pub use flat_index::{FlatDistributions, FlatIndex}; pub use flat_index::{FlatDistributions, FlatIndex};
pub use lock::{ pub use lock::{
InstallTarget, Lock, LockError, LockVersion, RequirementsTxtExport, ResolverManifest, InstallTarget, Lock, LockError, LockVersion, PackageMap, RequirementsTxtExport,
SatisfiesResult, TreeDisplay, VERSION, ResolverManifest, SatisfiesResult, TreeDisplay, VERSION,
}; };
pub use manifest::Manifest; pub use manifest::Manifest;
pub use options::{Flexibility, Options, OptionsBuilder}; pub use options::{Flexibility, Options, OptionsBuilder};

View file

@ -0,0 +1,37 @@
use rustc_hash::FxHashMap;
use crate::lock::{Package, PackageId};
/// A map from package to values, indexed by [`PackageId`].
#[derive(Debug, Clone)]
pub struct PackageMap<T>(FxHashMap<PackageId, T>);
impl<T> Default for PackageMap<T> {
fn default() -> Self {
Self(FxHashMap::default())
}
}
impl<T> PackageMap<T> {
/// Get a value by [`PackageId`].
pub(crate) fn get(&self, package_id: &PackageId) -> Option<&T> {
self.0.get(package_id)
}
}
impl<T> FromIterator<(Package, T)> for PackageMap<T> {
fn from_iter<I: IntoIterator<Item = (Package, T)>>(iter: I) -> Self {
Self(
iter.into_iter()
.map(|(package, value)| (package.id, value))
.collect(),
)
}
}
impl<T> Extend<(Package, T)> for PackageMap<T> {
fn extend<I: IntoIterator<Item = (Package, T)>>(&mut self, iter: I) {
self.0
.extend(iter.into_iter().map(|(package, value)| (package.id, value)));
}
}

View file

@ -14,6 +14,7 @@ use std::sync::{Arc, LazyLock};
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
use url::Url; use url::Url;
pub use crate::lock::map::PackageMap;
pub use crate::lock::requirements_txt::RequirementsTxtExport; pub use crate::lock::requirements_txt::RequirementsTxtExport;
pub use crate::lock::target::InstallTarget; pub use crate::lock::target::InstallTarget;
pub use crate::lock::tree::TreeDisplay; pub use crate::lock::tree::TreeDisplay;
@ -47,6 +48,7 @@ use uv_types::{BuildContext, HashStrategy};
use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::Workspace; use uv_workspace::Workspace;
mod map;
mod requirements_txt; mod requirements_txt;
mod target; mod target;
mod tree; mod tree;
@ -2206,6 +2208,24 @@ impl Package {
self.fork_markers.as_slice() self.fork_markers.as_slice()
} }
/// Returns the [`IndexUrl`] for the package, if it is a registry source.
pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
match &self.id.source {
Source::Registry(RegistrySource::Url(url)) => {
let index = IndexUrl::from(VerbatimUrl::from_url(url.to_url()));
Ok(Some(index))
}
Source::Registry(RegistrySource::Path(path)) => {
let index = IndexUrl::from(
VerbatimUrl::from_absolute_path(root.join(path))
.map_err(LockErrorKind::RegistryVerbatimUrl)?,
);
Ok(Some(index))
}
_ => Ok(None),
}
}
/// Returns all the hashes associated with this [`Package`]. /// Returns all the hashes associated with this [`Package`].
fn hashes(&self) -> Vec<HashDigest> { fn hashes(&self) -> Vec<HashDigest> {
let mut hashes = Vec::new(); let mut hashes = Vec::new();

View file

@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::VecDeque; use std::collections::VecDeque;
use itertools::Itertools; use itertools::Itertools;
use owo_colors::OwoColorize;
use petgraph::graph::{EdgeIndex, NodeIndex}; use petgraph::graph::{EdgeIndex, NodeIndex};
use petgraph::prelude::EdgeRef; use petgraph::prelude::EdgeRef;
use petgraph::Direction; use petgraph::Direction;
@ -9,10 +10,11 @@ use rustc_hash::{FxHashMap, FxHashSet};
use uv_configuration::DevGroupsManifest; use uv_configuration::DevGroupsManifest;
use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::Version;
use uv_pypi_types::ResolverMarkerEnvironment; use uv_pypi_types::ResolverMarkerEnvironment;
use crate::lock::{Dependency, PackageId}; use crate::lock::{Dependency, PackageId};
use crate::Lock; use crate::{Lock, PackageMap};
#[derive(Debug)] #[derive(Debug)]
pub struct TreeDisplay<'env> { pub struct TreeDisplay<'env> {
@ -20,6 +22,8 @@ pub struct TreeDisplay<'env> {
graph: petgraph::graph::Graph<&'env PackageId, Edge<'env>, petgraph::Directed>, graph: petgraph::graph::Graph<&'env PackageId, Edge<'env>, petgraph::Directed>,
/// The packages considered as roots of the dependency tree. /// The packages considered as roots of the dependency tree.
roots: Vec<NodeIndex>, roots: Vec<NodeIndex>,
/// The latest known version of each package.
latest: &'env PackageMap<Version>,
/// Maximum display depth of the dependency tree. /// Maximum display depth of the dependency tree.
depth: usize, depth: usize,
/// Whether to de-duplicate the displayed dependencies. /// Whether to de-duplicate the displayed dependencies.
@ -31,6 +35,7 @@ impl<'env> TreeDisplay<'env> {
pub fn new( pub fn new(
lock: &'env Lock, lock: &'env Lock,
markers: Option<&'env ResolverMarkerEnvironment>, markers: Option<&'env ResolverMarkerEnvironment>,
latest: &'env PackageMap<Version>,
depth: usize, depth: usize,
prune: &[PackageName], prune: &[PackageName],
packages: &[PackageName], packages: &[PackageName],
@ -242,6 +247,7 @@ impl<'env> TreeDisplay<'env> {
Self { Self {
graph, graph,
roots, roots,
latest,
depth, depth,
no_dedupe, no_dedupe,
} }
@ -306,6 +312,13 @@ impl<'env> TreeDisplay<'env> {
} }
} }
// Incorporate the latest version of the package, if known.
let line = if let Some(version) = self.latest.get(package_id) {
format!("{line} {}", format!("(latest: v{version})").bold().cyan())
} else {
line
};
let mut dependencies = self let mut dependencies = self
.graph .graph
.edges_directed(cursor.node(), Direction::Outgoing) .edges_directed(cursor.node(), Direction::Outgoing)

View file

@ -0,0 +1,124 @@
use uv_client::{RegistryClient, VersionFiles};
use uv_distribution_filename::DistFilename;
use uv_distribution_types::{IndexCapabilities, IndexUrl};
use uv_normalize::PackageName;
use uv_platform_tags::Tags;
use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython};
use uv_warnings::warn_user_once;
/// A client to fetch the latest version of a package from an index.
///
/// The returned distribution is guaranteed to be compatible with the provided tags and Python
/// requirement.
#[derive(Debug)]
pub(crate) struct LatestClient<'env> {
pub(crate) client: &'env RegistryClient,
pub(crate) capabilities: &'env IndexCapabilities,
pub(crate) prerelease: PrereleaseMode,
pub(crate) exclude_newer: Option<ExcludeNewer>,
pub(crate) tags: Option<&'env Tags>,
pub(crate) requires_python: &'env RequiresPython,
}
impl<'env> LatestClient<'env> {
/// Find the latest version of a package from an index.
pub(crate) async fn find_latest(
&self,
package: &PackageName,
index: Option<&IndexUrl>,
) -> anyhow::Result<Option<DistFilename>, uv_client::Error> {
let mut latest: Option<DistFilename> = None;
for (_, archive) in self
.client
.simple(package, index, self.capabilities)
.await?
{
for datum in archive.iter().rev() {
// Find the first compatible distribution.
let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(&datum.files)
.expect("archived version files always deserializes");
// Determine whether there's a compatible wheel and/or source distribution.
let mut best = None;
for (filename, file) in files.all() {
// Skip distributions uploaded after the cutoff.
if let Some(exclude_newer) = self.exclude_newer {
match file.upload_time_utc_ms.as_ref() {
Some(&upload_time)
if upload_time >= exclude_newer.timestamp_millis() =>
{
continue;
}
None => {
warn_user_once!(
"{} is missing an upload date, but user provided: {exclude_newer}",
file.filename,
);
}
_ => {}
}
}
// Skip pre-release distributions.
if !filename.version().is_stable() {
if !matches!(self.prerelease, PrereleaseMode::Allow) {
continue;
}
}
// Skip distributions that are yanked.
if file.yanked.is_some_and(|yanked| yanked.is_yanked()) {
continue;
}
// Skip distributions that are incompatible with the Python requirement.
if file
.requires_python
.as_ref()
.is_some_and(|requires_python| {
!self.requires_python.is_contained_by(requires_python)
})
{
continue;
}
// Skip distributions that are incompatible with the current platform.
if let DistFilename::WheelFilename(filename) = &filename {
if self
.tags
.is_some_and(|tags| !filename.compatibility(tags).is_compatible())
{
continue;
}
}
match filename {
DistFilename::WheelFilename(_) => {
best = Some(filename);
break;
}
DistFilename::SourceDistFilename(_) => {
if best.is_none() {
best = Some(filename);
}
}
}
}
match (latest.as_ref(), best) {
(Some(current), Some(best)) => {
if best.version() > current.version() {
latest = Some(best);
}
}
(None, Some(best)) => {
latest = Some(best);
}
_ => {}
}
}
}
Ok(latest)
}
}

View file

@ -14,7 +14,7 @@ use unicode_width::UnicodeWidthStr;
use uv_cache::{Cache, Refresh}; use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp; use uv_cache_info::Timestamp;
use uv_cli::ListFormat; use uv_cli::ListFormat;
use uv_client::{Connectivity, RegistryClient, RegistryClientBuilder, VersionFiles}; use uv_client::{Connectivity, RegistryClientBuilder};
use uv_configuration::{IndexStrategy, KeyringProviderType, TrustedHost}; use uv_configuration::{IndexStrategy, KeyringProviderType, TrustedHost};
use uv_distribution_filename::DistFilename; use uv_distribution_filename::DistFilename;
use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name}; use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name};
@ -22,12 +22,11 @@ use uv_fs::Simplified;
use uv_installer::SitePackages; use uv_installer::SitePackages;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_platform_tags::Tags; use uv_python::PythonRequest;
use uv_python::{EnvironmentPreference, PythonEnvironment}; use uv_python::{EnvironmentPreference, PythonEnvironment};
use uv_python::{Interpreter, PythonRequest}; use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython};
use uv_resolver::{ExcludeNewer, PrereleaseMode};
use uv_warnings::warn_user_once;
use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::operations::report_target_environment; use crate::commands::pip::operations::report_target_environment;
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
use crate::printer::Printer; use crate::printer::Printer;
@ -98,6 +97,8 @@ pub(crate) async fn pip_list(
// Determine the platform tags. // Determine the platform tags.
let interpreter = environment.interpreter(); let interpreter = environment.interpreter();
let tags = interpreter.tags()?; 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. // Initialize the client to fetch the latest version of each package.
let client = LatestClient { let client = LatestClient {
@ -105,15 +106,15 @@ pub(crate) async fn pip_list(
capabilities: &capabilities, capabilities: &capabilities,
prerelease, prerelease,
exclude_newer, exclude_newer,
tags, tags: Some(tags),
interpreter, requires_python: &requires_python,
}; };
// Fetch the latest version for each package. // Fetch the latest version for each package.
results results
.iter() .iter()
.map(|dist| async { .map(|dist| async {
let latest = client.find_latest(dist.name()).await?; let latest = client.find_latest(dist.name(), None).await?;
Ok::<(&PackageName, Option<DistFilename>), uv_client::Error>((dist.name(), latest)) Ok::<(&PackageName, Option<DistFilename>), uv_client::Error>((dist.name(), latest))
}) })
.collect::<FuturesUnordered<_>>() .collect::<FuturesUnordered<_>>()
@ -363,107 +364,3 @@ where
self.0.iter_mut().map(Iterator::next).collect() self.0.iter_mut().map(Iterator::next).collect()
} }
} }
/// A client to fetch the latest version of a package from an index.
///
/// The returned distribution is guaranteed to be compatible with the current interpreter.
#[derive(Debug)]
struct LatestClient<'env> {
client: &'env RegistryClient,
capabilities: &'env IndexCapabilities,
prerelease: PrereleaseMode,
exclude_newer: Option<ExcludeNewer>,
tags: &'env Tags,
interpreter: &'env Interpreter,
}
impl<'env> LatestClient<'env> {
/// Find the latest version of a package from an index.
async fn find_latest(
&self,
package: &PackageName,
) -> Result<Option<DistFilename>, uv_client::Error> {
let mut latest: Option<DistFilename> = None;
for (_, archive) in self.client.simple(package, None, self.capabilities).await? {
for datum in archive.iter().rev() {
// Find the first compatible distribution.
let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(&datum.files)
.expect("archived version files always deserializes");
// Determine whether there's a compatible wheel and/or source distribution.
let mut best = None;
for (filename, file) in files.all() {
// Skip distributions uploaded after the cutoff.
if let Some(exclude_newer) = self.exclude_newer {
match file.upload_time_utc_ms.as_ref() {
Some(&upload_time)
if upload_time >= exclude_newer.timestamp_millis() =>
{
continue;
}
None => {
warn_user_once!(
"{} is missing an upload date, but user provided: {exclude_newer}",
file.filename,
);
}
_ => {}
}
}
// Skip pre-release distributions.
if !filename.version().is_stable() {
if !matches!(self.prerelease, PrereleaseMode::Allow) {
continue;
}
}
// Skip distributions that are yanked.
if file.yanked.is_some_and(|yanked| yanked.is_yanked()) {
continue;
}
// Skip distributions that are incompatible with the current interpreter.
if file.requires_python.is_some_and(|requires_python| {
!requires_python.contains(self.interpreter.python_full_version())
}) {
continue;
}
// Skip distributions that are incompatible with the current platform.
if let DistFilename::WheelFilename(filename) = &filename {
if !filename.compatibility(self.tags).is_compatible() {
continue;
}
}
match filename {
DistFilename::WheelFilename(_) => {
best = Some(filename);
break;
}
DistFilename::SourceDistFilename(_) => {
if best.is_none() {
best = Some(filename);
}
}
}
}
match (latest.as_ref(), best) {
(Some(current), Some(best)) => {
if best.version() > current.version() {
latest = Some(best);
}
}
(None, Some(best)) => {
latest = Some(best);
}
_ => {}
}
}
}
Ok(latest)
}
}

View file

@ -9,6 +9,7 @@ pub(crate) mod check;
pub(crate) mod compile; pub(crate) mod compile;
pub(crate) mod freeze; pub(crate) mod freeze;
pub(crate) mod install; pub(crate) mod install;
pub(crate) mod latest;
pub(crate) mod list; pub(crate) mod list;
pub(crate) mod loggers; pub(crate) mod loggers;
pub(crate) mod operations; pub(crate) mod operations;

View file

@ -2,15 +2,20 @@ use std::path::Path;
use anstream::print; use anstream::print;
use anyhow::Result; use anyhow::Result;
use futures::{stream, StreamExt};
use uv_cache::Cache; use uv_cache::{Cache, Refresh};
use uv_client::Connectivity; use uv_cache_info::Timestamp;
use uv_client::{Connectivity, RegistryClientBuilder};
use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTriple}; use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTriple};
use uv_distribution_types::IndexCapabilities;
use uv_pep440::Version;
use uv_pep508::PackageName; use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
use uv_resolver::TreeDisplay; use uv_resolver::{PackageMap, TreeDisplay};
use uv_workspace::{DiscoveryOptions, Workspace}; use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::resolution_markers; use crate::commands::pip::resolution_markers;
use crate::commands::project::lock::LockMode; use crate::commands::project::lock::LockMode;
@ -34,6 +39,7 @@ pub(crate) async fn tree(
package: Vec<PackageName>, package: Vec<PackageName>,
no_dedupe: bool, no_dedupe: bool,
invert: bool, invert: bool,
outdated: bool,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>, python_platform: Option<TargetTriple>,
python: Option<String>, python: Option<String>,
@ -116,10 +122,72 @@ pub(crate) async fn tree(
) )
}); });
// If necessary, look up the latest version of each package.
let latest = if outdated {
let ResolverSettings {
index_locations: _,
index_strategy: _,
keyring_provider,
allow_insecure_host,
resolution: _,
prerelease: _,
dependency_metadata: _,
config_setting: _,
no_build_isolation: _,
no_build_isolation_package: _,
exclude_newer: _,
link_mode: _,
upgrade: _,
build_options: _,
sources: _,
} = &settings;
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)
.keyring(*keyring_provider)
.allow_insecure_host(allow_insecure_host.clone())
.build();
// Initialize the client to fetch the latest version of each package.
let client = LatestClient {
client: &client,
capabilities: &capabilities,
prerelease: lock.prerelease_mode(),
exclude_newer: lock.exclude_newer(),
requires_python: lock.requires_python(),
tags: None,
};
// Fetch the latest version for each package.
stream::iter(lock.packages())
.filter_map(|package| async {
let index = package.index(workspace.install_path()).ok()??;
let filename = client
.find_latest(package.name(), Some(&index))
.await
.ok()??;
if filename.version() == package.version() {
None
} else {
Some((package.clone(), filename.into_version()))
}
})
.collect::<PackageMap<Version>>()
.await
} else {
PackageMap::default()
};
// Render the tree. // Render the tree.
let tree = TreeDisplay::new( let tree = TreeDisplay::new(
&lock, &lock,
markers.as_ref(), markers.as_ref(),
&latest,
depth.into(), depth.into(),
&prune, &prune,
&package, &package,

View file

@ -1509,6 +1509,7 @@ async fn run_project(
args.package, args.package,
args.no_dedupe, args.no_dedupe,
args.invert, args.invert,
args.outdated,
args.python_version, args.python_version,
args.python_platform, args.python_platform,
args.python, args.python,

View file

@ -1057,6 +1057,7 @@ pub(crate) struct TreeSettings {
pub(crate) package: Vec<PackageName>, pub(crate) package: Vec<PackageName>,
pub(crate) no_dedupe: bool, pub(crate) no_dedupe: bool,
pub(crate) invert: bool, pub(crate) invert: bool,
pub(crate) outdated: bool,
pub(crate) python_version: Option<PythonVersion>, pub(crate) python_version: Option<PythonVersion>,
pub(crate) python_platform: Option<TargetTriple>, pub(crate) python_platform: Option<TargetTriple>,
pub(crate) python: Option<String>, pub(crate) python: Option<String>,
@ -1096,6 +1097,7 @@ impl TreeSettings {
package: tree.package, package: tree.package,
no_dedupe: tree.no_dedupe, no_dedupe: tree.no_dedupe,
invert: tree.invert, invert: tree.invert,
outdated: tree.outdated,
python_version, python_version,
python_platform, python_platform,
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),

View file

@ -238,6 +238,38 @@ fn frozen() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn outdated() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.0.0"]
"#,
)?;
uv_snapshot!(context.filters(), context.tree().arg("--outdated").arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
project v0.1.0
anyio v3.0.0 (latest: v4.3.0)
idna v3.6
sniffio v1.3.1
----- stderr -----
Resolved 4 packages in [TIME]
"###
);
Ok(())
}
#[test] #[test]
fn platform_dependencies() -> Result<()> { fn platform_dependencies() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");

View file

@ -2618,6 +2618,8 @@ uv tree [OPTIONS]
<p>The project itself will also be omitted.</p> <p>The project itself will also be omitted.</p>
</dd><dt><code>--outdated</code></dt><dd><p>Show the latest available version of each package in the tree</p>
</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Display only the specified packages</p> </dd><dt><code>--package</code> <i>package</i></dt><dd><p>Display only the specified packages</p>
</dd><dt><code>--prerelease</code> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p> </dd><dt><code>--prerelease</code> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
@ -6830,6 +6832,8 @@ uv pip tree [OPTIONS]
<p>When disabled, uv will only use locally cached data and locally available files.</p> <p>When disabled, uv will only use locally cached data and locally available files.</p>
</dd><dt><code>--outdated</code></dt><dd><p>Show the latest available version of each package in the tree</p>
</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Display only the specified packages</p> </dd><dt><code>--package</code> <i>package</i></dt><dd><p>Display only the specified packages</p>
</dd><dt><code>--project</code> <i>project</i></dt><dd><p>Run the command within the given project directory.</p> </dd><dt><code>--project</code> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>