Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uv tree --outdated #8893

Merged
merged 1 commit into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
7 changes: 7 additions & 0 deletions crates/uv-distribution-filename/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
37 changes: 37 additions & 0 deletions crates/uv-resolver/src/lock/map.rs
Original file line number Diff line number Diff line change
@@ -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)));
}
}
20 changes: 20 additions & 0 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
15 changes: 14 additions & 1 deletion crates/uv-resolver/src/lock/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ 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;
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> {
/// The constructed dependency graph.
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.
Expand All @@ -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],
Expand Down Expand Up @@ -242,6 +247,7 @@ impl<'env> TreeDisplay<'env> {
Self {
graph,
roots,
latest,
depth,
no_dedupe,
}
Expand Down Expand Up @@ -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)
Expand Down
124 changes: 124 additions & 0 deletions crates/uv/src/commands/pip/latest.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading