mirror of https://github.com/astral-sh/uv
Add JSON formatter.
This commit is contained in:
parent
a4ad184ad9
commit
060ee6d654
|
|
@ -6528,6 +6528,7 @@ dependencies = [
|
|||
"same-file",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"textwrap",
|
||||
"thiserror 2.0.17",
|
||||
|
|
|
|||
|
|
@ -6644,6 +6644,10 @@ pub struct DisplayTreeArgs {
|
|||
/// Show compressed wheel sizes for packages in the tree.
|
||||
#[arg(long)]
|
||||
pub show_sizes: bool,
|
||||
|
||||
/// Output the dependency tree as JSON.
|
||||
#[arg(long)]
|
||||
pub json: bool,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ rustc-hash = { workspace = true }
|
|||
same-file = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
textwrap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::collections::{BTreeSet, VecDeque};
|
|||
use either::Either;
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use serde_json::json;
|
||||
use petgraph::graph::{EdgeIndex, NodeIndex};
|
||||
use petgraph::prelude::EdgeRef;
|
||||
use petgraph::{Direction, Graph};
|
||||
|
|
@ -287,6 +288,137 @@ impl TreeFormatter for TextFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
/// A JSON tree formatter that produces structured JSON output.
|
||||
///
|
||||
/// This formatter produces output like:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "name": "package-name",
|
||||
/// "version": "1.0.0",
|
||||
/// "dependencies": [
|
||||
/// {
|
||||
/// "name": "dependency-1",
|
||||
/// "version": "2.0.0",
|
||||
/// "dependencies": []
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
struct JsonFormatter {
|
||||
/// Stack of JSON objects being built.
|
||||
/// The top of the stack is the current node being processed.
|
||||
stack: Vec<serde_json::Value>,
|
||||
/// The root nodes (top-level packages).
|
||||
roots: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl JsonFormatter {
|
||||
/// Create a new JSON formatter.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
stack: Vec::new(),
|
||||
roots: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeFormatter for JsonFormatter {
|
||||
type Output = serde_json::Value;
|
||||
|
||||
fn begin_tree(&mut self) {
|
||||
// Nothing to do for JSON output
|
||||
}
|
||||
|
||||
fn end_tree(&mut self) -> Self::Output {
|
||||
// Return all roots as a JSON array
|
||||
json!(self.roots)
|
||||
}
|
||||
|
||||
fn begin_node(&mut self, info: &NodeInfo, _position: NodePosition) {
|
||||
// Create a JSON object for this node
|
||||
let mut node = json!({
|
||||
"name": info.package_id.name.to_string(),
|
||||
});
|
||||
|
||||
// Add optional fields
|
||||
if let Some(version) = info.version {
|
||||
node["version"] = json!(version.to_string());
|
||||
}
|
||||
|
||||
if let Some(extras) = info.extras {
|
||||
if !extras.is_empty() {
|
||||
node["extras"] = json!(extras.iter().map(|e| e.to_string()).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(edge_type) = info.edge_type {
|
||||
match edge_type {
|
||||
EdgeType::Optional(extra) => {
|
||||
node["extra"] = json!(extra.to_string());
|
||||
}
|
||||
EdgeType::Dev(group) => {
|
||||
node["group"] = json!(group.to_string());
|
||||
}
|
||||
EdgeType::Prod => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(size) = info.size {
|
||||
node["size"] = json!(size);
|
||||
}
|
||||
|
||||
if let Some(latest) = info.latest_version {
|
||||
node["latest"] = json!(latest.to_string());
|
||||
}
|
||||
|
||||
// Initialize empty dependencies array
|
||||
node["dependencies"] = json!([]);
|
||||
|
||||
// Push onto stack
|
||||
self.stack.push(node);
|
||||
}
|
||||
|
||||
fn end_node(&mut self) {
|
||||
// Pop the current node from the stack
|
||||
let node = self.stack.pop().expect("Stack should not be empty");
|
||||
|
||||
if self.stack.is_empty() {
|
||||
// This is a root node - add to roots
|
||||
self.roots.push(node);
|
||||
} else {
|
||||
// This is a child node - add to parent's dependencies
|
||||
let parent = self.stack.last_mut().expect("Parent should exist");
|
||||
parent["dependencies"]
|
||||
.as_array_mut()
|
||||
.expect("Dependencies should be an array")
|
||||
.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_visited(&mut self) {
|
||||
// Mark the current node as deduplicated
|
||||
if let Some(node) = self.stack.last_mut() {
|
||||
node["deduplicated"] = json!(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_cycle(&mut self) {
|
||||
// Mark the current node as a cycle
|
||||
if let Some(node) = self.stack.last_mut() {
|
||||
node["cycle"] = json!(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_children(&mut self, _count: usize) {
|
||||
// Nothing to do for JSON output
|
||||
}
|
||||
|
||||
fn end_children(&mut self) {
|
||||
// Nothing to do for JSON output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TreeDisplay<'env> {
|
||||
/// The constructed dependency graph.
|
||||
|
|
@ -899,6 +1031,62 @@ impl<'env> TreeDisplay<'env> {
|
|||
|
||||
formatter.end_tree()
|
||||
}
|
||||
|
||||
/// Depth-first traverse the nodes to render the tree as JSON.
|
||||
pub fn render_json(&self) -> serde_json::Value {
|
||||
let mut path = Vec::new();
|
||||
let mut visited =
|
||||
FxHashMap::with_capacity_and_hasher(self.graph.node_count(), FxBuildHasher);
|
||||
let mut formatter = JsonFormatter::new();
|
||||
|
||||
formatter.begin_tree();
|
||||
|
||||
for node in &self.roots {
|
||||
match self.graph[*node] {
|
||||
Node::Root => {
|
||||
let edges: Vec<_> = self.graph.edges_directed(*node, Direction::Outgoing).collect();
|
||||
let total_siblings = edges.len();
|
||||
for (index, edge) in edges.into_iter().enumerate() {
|
||||
let node = edge.target();
|
||||
path.clear();
|
||||
let position = NodePosition {
|
||||
depth: 0,
|
||||
is_first_child: index == 0,
|
||||
is_last_child: index == total_siblings - 1,
|
||||
child_index: index,
|
||||
total_siblings,
|
||||
};
|
||||
self.visit_with_formatter(
|
||||
Cursor::new(node, edge.id()),
|
||||
&mut formatter,
|
||||
&mut visited,
|
||||
&mut path,
|
||||
position,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::Package(_) => {
|
||||
path.clear();
|
||||
let position = NodePosition {
|
||||
depth: 0,
|
||||
is_first_child: true,
|
||||
is_last_child: true,
|
||||
child_index: 0,
|
||||
total_siblings: 1,
|
||||
};
|
||||
self.visit_with_formatter(
|
||||
Cursor::root(*node),
|
||||
&mut formatter,
|
||||
&mut visited,
|
||||
&mut path,
|
||||
position,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formatter.end_tree()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub(crate) async fn tree(
|
|||
invert: bool,
|
||||
outdated: bool,
|
||||
show_sizes: bool,
|
||||
json: bool,
|
||||
python_version: Option<PythonVersion>,
|
||||
python_platform: Option<TargetTriple>,
|
||||
python: Option<String>,
|
||||
|
|
@ -283,7 +284,14 @@ pub(crate) async fn tree(
|
|||
show_sizes,
|
||||
);
|
||||
|
||||
if json {
|
||||
// Output JSON
|
||||
let json_output = tree.render_json();
|
||||
println!("{}", serde_json::to_string_pretty(&json_output)?);
|
||||
} else {
|
||||
// Output text
|
||||
print!("{tree}");
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2205,6 +2205,7 @@ async fn run_project(
|
|||
args.invert,
|
||||
args.outdated,
|
||||
args.show_sizes,
|
||||
args.json,
|
||||
args.python_version,
|
||||
args.python_platform,
|
||||
args.python,
|
||||
|
|
|
|||
|
|
@ -1874,6 +1874,7 @@ pub(crate) struct TreeSettings {
|
|||
pub(crate) invert: bool,
|
||||
pub(crate) outdated: bool,
|
||||
pub(crate) show_sizes: bool,
|
||||
pub(crate) json: bool,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) script: Option<PathBuf>,
|
||||
pub(crate) python_version: Option<PythonVersion>,
|
||||
|
|
@ -1941,6 +1942,7 @@ impl TreeSettings {
|
|||
invert: tree.invert,
|
||||
outdated: tree.outdated,
|
||||
show_sizes: tree.show_sizes,
|
||||
json: tree.json,
|
||||
script,
|
||||
python_version,
|
||||
python_platform,
|
||||
|
|
|
|||
Loading…
Reference in New Issue