mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Add JSON output for uv tree
This commit is contained in:
@@ -79,6 +79,15 @@ pub enum AuditOutputFormat {
|
||||
Sarif,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
|
||||
pub enum TreeFormat {
|
||||
/// Display the dependency graph as a human-readable tree.
|
||||
#[default]
|
||||
Text,
|
||||
/// Display the dependency graph as JSON.
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, clap::ValueEnum)]
|
||||
pub enum ListFormat {
|
||||
/// Display the list of packages in a human-readable table.
|
||||
@@ -4736,6 +4745,10 @@ pub struct TreeArgs {
|
||||
#[arg(long)]
|
||||
pub universal: bool,
|
||||
|
||||
/// The format in which to display the dependency graph.
|
||||
#[arg(long, value_enum, default_value_t = TreeFormat::default())]
|
||||
pub format: TreeFormat,
|
||||
|
||||
#[command(flatten)]
|
||||
pub tree: DisplayTreeArgs,
|
||||
|
||||
|
||||
@@ -8,15 +8,17 @@ use petgraph::graph::{EdgeIndex, NodeIndex};
|
||||
use petgraph::prelude::EdgeRef;
|
||||
use petgraph::{Direction, Graph};
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
use serde::Serialize;
|
||||
|
||||
use uv_configuration::DependencyGroupsWithDefaults;
|
||||
use uv_console::human_readable_bytes;
|
||||
use uv_fs::PortablePath;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::MarkerTree;
|
||||
use uv_pypi_types::ResolverMarkerEnvironment;
|
||||
|
||||
use crate::lock::{Package, PackageId};
|
||||
use crate::lock::{DirectSource, Package, PackageId, RegistrySource, Source};
|
||||
use crate::{Lock, PackageMap};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -647,6 +649,353 @@ impl<'env> TreeDisplay<'env> {
|
||||
.filter(|extra| package.optional_dependencies.contains_key(*extra))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Serialize the dependency graph in `NetworkX` node-link format.
|
||||
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string_pretty(&JsonGraph::from(self))
|
||||
}
|
||||
|
||||
/// Return the shortest distance from any displayed root to every package within the requested
|
||||
/// depth.
|
||||
fn json_distances(&self) -> FxHashMap<NodeIndex, usize> {
|
||||
let mut distances = FxHashMap::default();
|
||||
let mut queue = VecDeque::new();
|
||||
|
||||
for root in &self.roots {
|
||||
match self.graph[*root] {
|
||||
Node::Root => {
|
||||
for edge in self.graph.edges_directed(*root, Direction::Outgoing) {
|
||||
if matches!(self.graph[edge.target()], Node::Package(_))
|
||||
&& distances.insert(edge.target(), 0).is_none()
|
||||
{
|
||||
queue.push_back(edge.target());
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Package(_) => {
|
||||
if distances.insert(*root, 0).is_none() {
|
||||
queue.push_back(*root);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(source) = queue.pop_front() {
|
||||
let distance = distances[&source];
|
||||
if distance >= self.depth {
|
||||
continue;
|
||||
}
|
||||
|
||||
for edge in self.graph.edges_directed(source, Direction::Outgoing) {
|
||||
let target = edge.target();
|
||||
if !matches!(self.graph[target], Node::Package(_))
|
||||
|| distances.contains_key(&target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
distances.insert(target, distance + 1);
|
||||
queue.push_back(target);
|
||||
}
|
||||
}
|
||||
|
||||
distances
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct JsonGraph<'env> {
|
||||
directed: bool,
|
||||
multigraph: bool,
|
||||
graph: JsonGraphAttributes<'env>,
|
||||
nodes: Vec<JsonNode<'env>>,
|
||||
edges: Vec<JsonEdge<'env>>,
|
||||
}
|
||||
|
||||
impl<'env> From<&TreeDisplay<'env>> for JsonGraph<'env> {
|
||||
fn from(tree: &TreeDisplay<'env>) -> Self {
|
||||
let distances = tree.json_distances();
|
||||
|
||||
let mut package_nodes = distances.keys().copied().collect::<Vec<_>>();
|
||||
package_nodes.sort_by_key(|index| &tree.graph[*index]);
|
||||
|
||||
let node_ids = package_nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, index)| (*index, id))
|
||||
.collect::<FxHashMap<_, _>>();
|
||||
|
||||
let nodes = package_nodes
|
||||
.iter()
|
||||
.map(|index| {
|
||||
let Node::Package(package_id) = tree.graph[*index] else {
|
||||
unreachable!("JSON nodes only include packages");
|
||||
};
|
||||
let package = tree.lock.find_by_id(package_id);
|
||||
JsonNode {
|
||||
id: node_ids[index],
|
||||
name: &package_id.name,
|
||||
version: package_id.version.as_ref(),
|
||||
source: JsonSource::from(&package_id.source),
|
||||
latest_version: tree.latest.get(package_id),
|
||||
size_bytes: tree
|
||||
.show_sizes
|
||||
.then(|| package.wheels.iter().find_map(|wheel| wheel.size))
|
||||
.flatten(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut roots = tree
|
||||
.roots
|
||||
.iter()
|
||||
.flat_map(|root| match tree.graph[*root] {
|
||||
Node::Root => tree
|
||||
.graph
|
||||
.edges_directed(*root, Direction::Outgoing)
|
||||
.filter_map(|edge| {
|
||||
node_ids
|
||||
.get(&edge.target())
|
||||
.map(|id| JsonRoot::new(*id, Some(edge.weight())))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Node::Package(_) => node_ids
|
||||
.get(root)
|
||||
.map(|id| vec![JsonRoot::new(*id, None)])
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
roots.sort();
|
||||
roots.dedup();
|
||||
|
||||
let mut graph_edges = tree
|
||||
.graph
|
||||
.edge_references()
|
||||
.filter(|edge| {
|
||||
distances
|
||||
.get(&edge.source())
|
||||
.is_some_and(|distance| *distance < tree.depth)
|
||||
&& node_ids.contains_key(&edge.source())
|
||||
&& node_ids.contains_key(&edge.target())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
graph_edges.sort_by(|left, right| {
|
||||
(
|
||||
node_ids[&left.source()],
|
||||
node_ids[&left.target()],
|
||||
left.weight(),
|
||||
)
|
||||
.cmp(&(
|
||||
node_ids[&right.source()],
|
||||
node_ids[&right.target()],
|
||||
right.weight(),
|
||||
))
|
||||
});
|
||||
|
||||
let mut previous_endpoints = None;
|
||||
let mut key = 0;
|
||||
let edges = graph_edges
|
||||
.into_iter()
|
||||
.map(|edge| {
|
||||
let endpoints = (node_ids[&edge.source()], node_ids[&edge.target()]);
|
||||
if previous_endpoints == Some(endpoints) {
|
||||
key += 1;
|
||||
} else {
|
||||
previous_endpoints = Some(endpoints);
|
||||
key = 0;
|
||||
}
|
||||
JsonEdge::new(endpoints.0, endpoints.1, key, edge.weight())
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
directed: true,
|
||||
multigraph: true,
|
||||
graph: JsonGraphAttributes {
|
||||
schema: JsonSchema {
|
||||
version: JsonSchemaVersion::Preview,
|
||||
},
|
||||
roots,
|
||||
inverted: tree.invert,
|
||||
},
|
||||
nodes,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct JsonGraphAttributes<'env> {
|
||||
schema: JsonSchema,
|
||||
roots: Vec<JsonRoot<'env>>,
|
||||
inverted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct JsonSchema {
|
||||
version: JsonSchemaVersion,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum JsonSchemaVersion {
|
||||
Preview,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct JsonNode<'env> {
|
||||
id: usize,
|
||||
name: &'env PackageName,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
version: Option<&'env Version>,
|
||||
source: JsonSource,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
latest_version: Option<&'env Version>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
size_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum JsonSource {
|
||||
Registry {
|
||||
registry: String,
|
||||
},
|
||||
Git {
|
||||
git: String,
|
||||
},
|
||||
Direct {
|
||||
url: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
subdirectory: Option<String>,
|
||||
},
|
||||
Path {
|
||||
path: String,
|
||||
},
|
||||
Directory {
|
||||
directory: String,
|
||||
},
|
||||
Editable {
|
||||
editable: String,
|
||||
},
|
||||
Virtual {
|
||||
r#virtual: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&Source> for JsonSource {
|
||||
fn from(source: &Source) -> Self {
|
||||
match source {
|
||||
Source::Registry(source) => Self::Registry {
|
||||
registry: match source {
|
||||
RegistrySource::Url(url) => url.as_ref().to_string(),
|
||||
RegistrySource::Path(path) => PortablePath::from(path).to_string(),
|
||||
},
|
||||
},
|
||||
Source::Git(url, _) => Self::Git {
|
||||
git: url.as_ref().to_string(),
|
||||
},
|
||||
Source::Direct(url, DirectSource { subdirectory }) => Self::Direct {
|
||||
url: url.as_ref().to_string(),
|
||||
subdirectory: subdirectory
|
||||
.as_deref()
|
||||
.map(|path| PortablePath::from(path).to_string()),
|
||||
},
|
||||
Source::Path(path) => Self::Path {
|
||||
path: PortablePath::from(path).to_string(),
|
||||
},
|
||||
Source::Directory(path) => Self::Directory {
|
||||
directory: PortablePath::from(path).to_string(),
|
||||
},
|
||||
Source::Editable(path) => Self::Editable {
|
||||
editable: PortablePath::from(path).to_string(),
|
||||
},
|
||||
Source::Virtual(path) => Self::Virtual {
|
||||
r#virtual: PortablePath::from(path).to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct JsonRoot<'env> {
|
||||
id: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
kind: Option<JsonEdgeKind>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
extra: Option<&'env ExtraName>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
group: Option<&'env GroupName>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
requested_extras: Option<Vec<&'env ExtraName>>,
|
||||
}
|
||||
|
||||
impl<'env> JsonRoot<'env> {
|
||||
fn new(id: usize, edge: Option<&Edge<'env>>) -> Self {
|
||||
let kind = match edge {
|
||||
None | Some(Edge::Prod(_)) => None,
|
||||
Some(Edge::Optional(_, _)) => Some(JsonEdgeKind::Optional),
|
||||
Some(Edge::Dev(_, _)) => Some(JsonEdgeKind::Development),
|
||||
};
|
||||
let extra = match edge {
|
||||
Some(Edge::Optional(extra, _)) => Some(*extra),
|
||||
None | Some(Edge::Prod(_) | Edge::Dev(_, _)) => None,
|
||||
};
|
||||
let group = match edge {
|
||||
Some(Edge::Dev(group, _)) => Some(*group),
|
||||
None | Some(Edge::Prod(_) | Edge::Optional(_, _)) => None,
|
||||
};
|
||||
let requested_extras = edge
|
||||
.and_then(Edge::extras)
|
||||
.filter(|extras| !extras.is_empty())
|
||||
.map(|extras| extras.iter().collect());
|
||||
Self {
|
||||
id,
|
||||
kind,
|
||||
extra,
|
||||
group,
|
||||
requested_extras,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct JsonEdge<'env> {
|
||||
source: usize,
|
||||
target: usize,
|
||||
key: usize,
|
||||
kind: JsonEdgeKind,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
extra: Option<&'env ExtraName>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
group: Option<&'env GroupName>,
|
||||
requested_extras: Vec<&'env ExtraName>,
|
||||
}
|
||||
|
||||
impl<'env> JsonEdge<'env> {
|
||||
fn new(source: usize, target: usize, key: usize, edge: &Edge<'env>) -> Self {
|
||||
let (kind, extra, group) = match edge {
|
||||
Edge::Prod(_) => (JsonEdgeKind::Production, None, None),
|
||||
Edge::Optional(extra, _) => (JsonEdgeKind::Optional, Some(*extra), None),
|
||||
Edge::Dev(group, _) => (JsonEdgeKind::Development, None, Some(*group)),
|
||||
};
|
||||
Self {
|
||||
source,
|
||||
target,
|
||||
key,
|
||||
kind,
|
||||
extra,
|
||||
group,
|
||||
requested_extras: edge.extras().into_iter().flatten().collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum JsonEdgeKind {
|
||||
Production,
|
||||
Optional,
|
||||
Development,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use anstream::print;
|
||||
@@ -5,16 +6,18 @@ use anyhow::{Error, Result};
|
||||
use futures::StreamExt;
|
||||
use uv_cache::{Cache, Refresh};
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_cli::TreeFormat;
|
||||
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
|
||||
use uv_configuration::{Concurrency, DependencyGroups, TargetTriple};
|
||||
use uv_distribution_types::IndexCapabilities;
|
||||
use uv_normalize::DefaultGroups;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_preview::Preview;
|
||||
use uv_preview::{Preview, PreviewFeature};
|
||||
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
|
||||
use uv_resolver::{PackageMap, TreeDisplay};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
|
||||
|
||||
use crate::commands::pip::latest::LatestClient;
|
||||
@@ -41,6 +44,7 @@ pub(crate) async fn tree(
|
||||
lock_check: LockCheck,
|
||||
frozen: Option<FrozenSource>,
|
||||
universal: bool,
|
||||
format: TreeFormat,
|
||||
depth: u8,
|
||||
prune: Vec<PackageName>,
|
||||
package: Vec<PackageName>,
|
||||
@@ -63,6 +67,13 @@ pub(crate) async fn tree(
|
||||
printer: Printer,
|
||||
preview: Preview,
|
||||
) -> Result<ExitStatus> {
|
||||
if matches!(format, TreeFormat::Json) && !preview.is_enabled(PreviewFeature::JsonOutput) {
|
||||
warn_user!(
|
||||
"The `--format json` option is experimental and the schema may change without warning. Pass `--preview-features {}` to disable this warning.",
|
||||
PreviewFeature::JsonOutput
|
||||
);
|
||||
}
|
||||
|
||||
// Find the project requirements.
|
||||
let workspace_cache = WorkspaceCache::default();
|
||||
let workspace;
|
||||
@@ -304,7 +315,10 @@ pub(crate) async fn tree(
|
||||
show_sizes,
|
||||
);
|
||||
|
||||
print!("{tree}");
|
||||
match format {
|
||||
TreeFormat::Text => print!("{tree}"),
|
||||
TreeFormat::Json => writeln!(printer.stdout(), "{}", tree.to_json()?)?,
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
@@ -2667,6 +2667,7 @@ async fn run_project(
|
||||
args.lock_check,
|
||||
args.frozen,
|
||||
args.universal,
|
||||
args.format,
|
||||
args.depth,
|
||||
args.prune,
|
||||
args.package,
|
||||
|
||||
@@ -19,7 +19,7 @@ use uv_cli::{
|
||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
||||
PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs,
|
||||
SyncArgs, SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
|
||||
ToolUninstallArgs, TreeArgs, UpgradeArgs, VenvArgs, VersionArgs, VersionBumpSpec,
|
||||
ToolUninstallArgs, TreeArgs, TreeFormat, UpgradeArgs, VenvArgs, VersionArgs, VersionBumpSpec,
|
||||
VersionFormat,
|
||||
};
|
||||
use uv_cli::{
|
||||
@@ -2645,6 +2645,7 @@ pub(crate) struct TreeSettings {
|
||||
pub(crate) lock_check: LockCheck,
|
||||
pub(crate) frozen: Option<FrozenSource>,
|
||||
pub(crate) universal: bool,
|
||||
pub(crate) format: TreeFormat,
|
||||
pub(crate) depth: u8,
|
||||
pub(crate) prune: Vec<PackageName>,
|
||||
pub(crate) package: Vec<PackageName>,
|
||||
@@ -2671,6 +2672,7 @@ impl TreeSettings {
|
||||
let TreeArgs {
|
||||
tree,
|
||||
universal,
|
||||
format,
|
||||
dev,
|
||||
only_dev,
|
||||
no_dev,
|
||||
@@ -2728,6 +2730,7 @@ impl TreeSettings {
|
||||
lock_check: resolve_lock_check(locked),
|
||||
frozen: resolve_frozen(frozen),
|
||||
universal,
|
||||
format,
|
||||
depth: tree.depth,
|
||||
prune: tree.prune,
|
||||
package: tree.package,
|
||||
|
||||
@@ -49,6 +49,391 @@ fn nested_dependencies() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_output() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
context.temp_dir.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["package-a[feature]"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["package-c"]
|
||||
|
||||
[tool.uv.sources]
|
||||
package-a = { path = "packages/package-a" }
|
||||
package-c = { path = "packages/package-c" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let package_a = context.temp_dir.child("packages/package-a");
|
||||
package_a.create_dir_all()?;
|
||||
package_a.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "package-a"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.optional-dependencies]
|
||||
feature = ["package-b"]
|
||||
|
||||
[tool.uv.sources]
|
||||
package-b = { path = "../package-b" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
for package in ["package-b", "package-c"] {
|
||||
let directory = context.temp_dir.child(format!("packages/{package}"));
|
||||
directory.create_dir_all()?;
|
||||
directory
|
||||
.child("pyproject.toml")
|
||||
.write_str(&formatdoc! {r#"
|
||||
[project]
|
||||
name = "{package}"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.12"
|
||||
"#})?;
|
||||
}
|
||||
|
||||
uv_snapshot!(context.filters(), context.tree()
|
||||
.arg("--preview-features")
|
||||
.arg("json-output")
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.arg("--universal"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"directed": true,
|
||||
"multigraph": true,
|
||||
"graph": {
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": 3
|
||||
}
|
||||
],
|
||||
"inverted": false
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "package-a",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "package-b",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "package-c",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "project",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"virtual": "."
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 0,
|
||||
"target": 1,
|
||||
"key": 0,
|
||||
"kind": "optional",
|
||||
"extra": "feature",
|
||||
"requested_extras": []
|
||||
},
|
||||
{
|
||||
"source": 3,
|
||||
"target": 0,
|
||||
"key": 0,
|
||||
"kind": "production",
|
||||
"requested_extras": [
|
||||
"feature"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": 3,
|
||||
"target": 2,
|
||||
"key": 0,
|
||||
"kind": "development",
|
||||
"group": "dev",
|
||||
"requested_extras": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tree()
|
||||
.arg("--preview-features")
|
||||
.arg("json-output")
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.arg("--universal")
|
||||
.arg("--depth")
|
||||
.arg("1"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"directed": true,
|
||||
"multigraph": true,
|
||||
"graph": {
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
],
|
||||
"inverted": false
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "package-a",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "package-c",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "project",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"virtual": "."
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 2,
|
||||
"target": 0,
|
||||
"key": 0,
|
||||
"kind": "production",
|
||||
"requested_extras": [
|
||||
"feature"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"target": 1,
|
||||
"key": 0,
|
||||
"kind": "development",
|
||||
"group": "dev",
|
||||
"requested_extras": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tree()
|
||||
.arg("--preview-features")
|
||||
.arg("json-output")
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.arg("--universal")
|
||||
.arg("--invert")
|
||||
.arg("--depth")
|
||||
.arg("1"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"directed": true,
|
||||
"multigraph": true,
|
||||
"graph": {
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
],
|
||||
"inverted": true
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "package-a",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "package-b",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "package-c",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"directory": "packages/package-c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "project",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"virtual": "."
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 1,
|
||||
"target": 0,
|
||||
"key": 0,
|
||||
"kind": "optional",
|
||||
"extra": "feature",
|
||||
"requested_extras": []
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"target": 3,
|
||||
"key": 0,
|
||||
"kind": "development",
|
||||
"group": "dev",
|
||||
"requested_extras": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_output_virtual_root() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
context.temp_dir.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[dependency-groups]
|
||||
dev = ["package-a"]
|
||||
|
||||
[tool.uv.sources]
|
||||
package-a = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["package-a"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let package_a = context.temp_dir.child("package-a");
|
||||
package_a.create_dir_all()?;
|
||||
package_a.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "package-a"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.12"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tree()
|
||||
.arg("--preview-features")
|
||||
.arg("json-output")
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.arg("--universal")
|
||||
.arg("--only-group")
|
||||
.arg("dev"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"directed": true,
|
||||
"multigraph": true,
|
||||
"graph": {
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": 0
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"kind": "development",
|
||||
"group": "dev"
|
||||
}
|
||||
],
|
||||
"inverted": false
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "package-a",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"editable": "package-a"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_platform_dependencies() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
@@ -269,6 +654,23 @@ fn outdated() -> Result<()> {
|
||||
"
|
||||
);
|
||||
|
||||
let output = context
|
||||
.tree()
|
||||
.arg("--preview-features")
|
||||
.arg("json-output")
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.arg("--outdated")
|
||||
.arg("--universal")
|
||||
.output()?;
|
||||
output.clone().assert().success();
|
||||
let report: serde_json::Value = serde_json::from_slice(&output.stdout)?;
|
||||
let anyio = report["nodes"]
|
||||
.as_array()
|
||||
.and_then(|nodes| nodes.iter().find(|node| node["name"] == "anyio"))
|
||||
.expect("anyio should be included in the dependency graph");
|
||||
assert_eq!(anyio["latest_version"], "4.3.0");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2258,6 +2660,64 @@ fn show_sizes() -> Result<()> {
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tree()
|
||||
.arg("--preview-features")
|
||||
.arg("json-output")
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.arg("--show-sizes")
|
||||
.arg("--universal"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"directed": true,
|
||||
"multigraph": true,
|
||||
"graph": {
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": 1
|
||||
}
|
||||
],
|
||||
"inverted": false
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "iniconfig",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"registry": "https://pypi.org/simple"
|
||||
},
|
||||
"size_bytes": 5892
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "project",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"virtual": "."
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 1,
|
||||
"target": 0,
|
||||
"key": 0,
|
||||
"kind": "production",
|
||||
"requested_extras": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user