mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-01 09:32:18 +00:00
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:
parent
88331e756e
commit
29e1b15473
14 changed files with 328 additions and 118 deletions
|
@ -4802,6 +4802,10 @@ pub struct DisplayTreeArgs {
|
|||
/// display the packages that depend on the given package.
|
||||
#[arg(long, alias = "reverse")]
|
||||
pub invert: bool,
|
||||
|
||||
/// Show the latest available version of each package in the tree.
|
||||
#[arg(long)]
|
||||
pub outdated: bool,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
|
|
|
@ -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`.
|
||||
pub fn filetype(&self) -> &'static str {
|
||||
match self {
|
||||
|
|
|
@ -4,8 +4,8 @@ pub use exclude_newer::ExcludeNewer;
|
|||
pub use exclusions::Exclusions;
|
||||
pub use flat_index::{FlatDistributions, FlatIndex};
|
||||
pub use lock::{
|
||||
InstallTarget, Lock, LockError, LockVersion, RequirementsTxtExport, ResolverManifest,
|
||||
SatisfiesResult, TreeDisplay, VERSION,
|
||||
InstallTarget, Lock, LockError, LockVersion, PackageMap, RequirementsTxtExport,
|
||||
ResolverManifest, SatisfiesResult, TreeDisplay, VERSION,
|
||||
};
|
||||
pub use manifest::Manifest;
|
||||
pub use options::{Flexibility, Options, OptionsBuilder};
|
||||
|
|
37
crates/uv-resolver/src/lock/map.rs
Normal file
37
crates/uv-resolver/src/lock/map.rs
Normal 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)));
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ use std::sync::{Arc, LazyLock};
|
|||
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
|
||||
use url::Url;
|
||||
|
||||
pub use crate::lock::map::PackageMap;
|
||||
pub use crate::lock::requirements_txt::RequirementsTxtExport;
|
||||
pub use crate::lock::target::InstallTarget;
|
||||
pub use crate::lock::tree::TreeDisplay;
|
||||
|
@ -47,6 +48,7 @@ use uv_types::{BuildContext, HashStrategy};
|
|||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
use uv_workspace::Workspace;
|
||||
|
||||
mod map;
|
||||
mod requirements_txt;
|
||||
mod target;
|
||||
mod tree;
|
||||
|
@ -2206,6 +2208,24 @@ impl Package {
|
|||
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`].
|
||||
fn hashes(&self) -> Vec<HashDigest> {
|
||||
let mut hashes = Vec::new();
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::borrow::Cow;
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use petgraph::graph::{EdgeIndex, NodeIndex};
|
||||
use petgraph::prelude::EdgeRef;
|
||||
use petgraph::Direction;
|
||||
|
@ -9,10 +10,11 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
|||
|
||||
use uv_configuration::DevGroupsManifest;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::Version;
|
||||
use uv_pypi_types::ResolverMarkerEnvironment;
|
||||
|
||||
use crate::lock::{Dependency, PackageId};
|
||||
use crate::Lock;
|
||||
use crate::{Lock, PackageMap};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TreeDisplay<'env> {
|
||||
|
@ -20,6 +22,8 @@ pub struct TreeDisplay<'env> {
|
|||
graph: petgraph::graph::Graph<&'env PackageId, Edge<'env>, petgraph::Directed>,
|
||||
/// The packages considered as roots of the dependency tree.
|
||||
roots: Vec<NodeIndex>,
|
||||
/// The latest known version of each package.
|
||||
latest: &'env PackageMap<Version>,
|
||||
/// Maximum display depth of the dependency tree.
|
||||
depth: usize,
|
||||
/// Whether to de-duplicate the displayed dependencies.
|
||||
|
@ -31,6 +35,7 @@ impl<'env> TreeDisplay<'env> {
|
|||
pub fn new(
|
||||
lock: &'env Lock,
|
||||
markers: Option<&'env ResolverMarkerEnvironment>,
|
||||
latest: &'env PackageMap<Version>,
|
||||
depth: usize,
|
||||
prune: &[PackageName],
|
||||
packages: &[PackageName],
|
||||
|
@ -242,6 +247,7 @@ impl<'env> TreeDisplay<'env> {
|
|||
Self {
|
||||
graph,
|
||||
roots,
|
||||
latest,
|
||||
depth,
|
||||
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
|
||||
.graph
|
||||
.edges_directed(cursor.node(), Direction::Outgoing)
|
||||
|
|
124
crates/uv/src/commands/pip/latest.rs
Normal file
124
crates/uv/src/commands/pip/latest.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ use unicode_width::UnicodeWidthStr;
|
|||
use uv_cache::{Cache, Refresh};
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_cli::ListFormat;
|
||||
use uv_client::{Connectivity, RegistryClient, RegistryClientBuilder, VersionFiles};
|
||||
use uv_client::{Connectivity, RegistryClientBuilder};
|
||||
use uv_configuration::{IndexStrategy, KeyringProviderType, TrustedHost};
|
||||
use uv_distribution_filename::DistFilename;
|
||||
use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name};
|
||||
|
@ -22,12 +22,11 @@ use uv_fs::Simplified;
|
|||
use uv_installer::SitePackages;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_python::PythonRequest;
|
||||
use uv_python::{EnvironmentPreference, PythonEnvironment};
|
||||
use uv_python::{Interpreter, PythonRequest};
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython};
|
||||
|
||||
use crate::commands::pip::latest::LatestClient;
|
||||
use crate::commands::pip::operations::report_target_environment;
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
@ -98,6 +97,8 @@ pub(crate) async fn pip_list(
|
|||
// 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 {
|
||||
|
@ -105,15 +106,15 @@ pub(crate) async fn pip_list(
|
|||
capabilities: &capabilities,
|
||||
prerelease,
|
||||
exclude_newer,
|
||||
tags,
|
||||
interpreter,
|
||||
tags: Some(tags),
|
||||
requires_python: &requires_python,
|
||||
};
|
||||
|
||||
// Fetch the latest version for each package.
|
||||
results
|
||||
.iter()
|
||||
.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))
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
|
@ -363,107 +364,3 @@ where
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ pub(crate) mod check;
|
|||
pub(crate) mod compile;
|
||||
pub(crate) mod freeze;
|
||||
pub(crate) mod install;
|
||||
pub(crate) mod latest;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod loggers;
|
||||
pub(crate) mod operations;
|
||||
|
|
|
@ -2,15 +2,20 @@ use std::path::Path;
|
|||
|
||||
use anstream::print;
|
||||
use anyhow::Result;
|
||||
use futures::{stream, StreamExt};
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_cache::{Cache, Refresh};
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_client::{Connectivity, RegistryClientBuilder};
|
||||
use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTriple};
|
||||
use uv_distribution_types::IndexCapabilities;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::PackageName;
|
||||
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
|
||||
use uv_resolver::TreeDisplay;
|
||||
use uv_resolver::{PackageMap, TreeDisplay};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace};
|
||||
|
||||
use crate::commands::pip::latest::LatestClient;
|
||||
use crate::commands::pip::loggers::DefaultResolveLogger;
|
||||
use crate::commands::pip::resolution_markers;
|
||||
use crate::commands::project::lock::LockMode;
|
||||
|
@ -34,6 +39,7 @@ pub(crate) async fn tree(
|
|||
package: Vec<PackageName>,
|
||||
no_dedupe: bool,
|
||||
invert: bool,
|
||||
outdated: bool,
|
||||
python_version: Option<PythonVersion>,
|
||||
python_platform: Option<TargetTriple>,
|
||||
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.
|
||||
let tree = TreeDisplay::new(
|
||||
&lock,
|
||||
markers.as_ref(),
|
||||
&latest,
|
||||
depth.into(),
|
||||
&prune,
|
||||
&package,
|
||||
|
|
|
@ -1509,6 +1509,7 @@ async fn run_project(
|
|||
args.package,
|
||||
args.no_dedupe,
|
||||
args.invert,
|
||||
args.outdated,
|
||||
args.python_version,
|
||||
args.python_platform,
|
||||
args.python,
|
||||
|
|
|
@ -1057,6 +1057,7 @@ pub(crate) struct TreeSettings {
|
|||
pub(crate) package: Vec<PackageName>,
|
||||
pub(crate) no_dedupe: bool,
|
||||
pub(crate) invert: bool,
|
||||
pub(crate) outdated: bool,
|
||||
pub(crate) python_version: Option<PythonVersion>,
|
||||
pub(crate) python_platform: Option<TargetTriple>,
|
||||
pub(crate) python: Option<String>,
|
||||
|
@ -1096,6 +1097,7 @@ impl TreeSettings {
|
|||
package: tree.package,
|
||||
no_dedupe: tree.no_dedupe,
|
||||
invert: tree.invert,
|
||||
outdated: tree.outdated,
|
||||
python_version,
|
||||
python_platform,
|
||||
python: python.and_then(Maybe::into_option),
|
||||
|
|
|
@ -238,6 +238,38 @@ fn frozen() -> Result<()> {
|
|||
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]
|
||||
fn platform_dependencies() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
@ -2618,6 +2618,8 @@ uv tree [OPTIONS]
|
|||
|
||||
<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>--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>
|
||||
|
||||
</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>--project</code> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue