mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-30 16:43:45 +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.
|
/// 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)]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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};
|
||||||
|
|
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 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();
|
||||||
|
|
|
@ -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)
|
||||||
|
|
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::{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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue