Skip to content

Commit b9210a9

Browse files
Copilotanthonykim1
andcommitted
Add JSON output support to PET
Co-authored-by: anthonykim1 <62267334+anthonykim1@users.noreply.github.com>
1 parent fdbceff commit b9210a9

File tree

5 files changed

+170
-69
lines changed

5 files changed

+170
-69
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ PET can be run directly from the command line to discover all Python environment
6868
pet find --list --verbose
6969
```
7070

71+
- **Find all environments and output as JSON**:
72+
```bash
73+
pet find --json
74+
```
75+
7176
- **Search only in workspace/project directories**:
7277
```bash
7378
pet find --list --workspace

crates/pet-reporter/src/json.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use pet_core::{
5+
manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter,
6+
telemetry::TelemetryEvent,
7+
};
8+
use serde::Serialize;
9+
use std::sync::{Arc, Mutex};
10+
11+
#[derive(Serialize)]
12+
#[serde(rename_all = "camelCase")]
13+
pub struct JsonOutput {
14+
pub managers: Vec<EnvManager>,
15+
pub environments: Vec<PythonEnvironment>,
16+
}
17+
18+
/// Reporter that collects environments and managers for JSON output
19+
pub struct JsonReporter {
20+
managers: Arc<Mutex<Vec<EnvManager>>>,
21+
environments: Arc<Mutex<Vec<PythonEnvironment>>>,
22+
}
23+
24+
impl JsonReporter {
25+
pub fn new() -> Self {
26+
JsonReporter {
27+
managers: Arc::new(Mutex::new(vec![])),
28+
environments: Arc::new(Mutex::new(vec![])),
29+
}
30+
}
31+
32+
pub fn output_json(&self) {
33+
let managers = self.managers.lock().unwrap().clone();
34+
let environments = self.environments.lock().unwrap().clone();
35+
36+
let output = JsonOutput {
37+
managers,
38+
environments,
39+
};
40+
41+
match serde_json::to_string_pretty(&output) {
42+
Ok(json) => println!("{}", json),
43+
Err(e) => eprintln!("Error serializing to JSON: {}", e),
44+
}
45+
}
46+
}
47+
48+
impl Reporter for JsonReporter {
49+
fn report_telemetry(&self, _event: &TelemetryEvent) {
50+
// No telemetry in JSON output
51+
}
52+
53+
fn report_manager(&self, manager: &EnvManager) {
54+
self.managers.lock().unwrap().push(manager.clone());
55+
}
56+
57+
fn report_environment(&self, env: &PythonEnvironment) {
58+
self.environments.lock().unwrap().push(env.clone());
59+
}
60+
}
61+
62+
pub fn create_reporter() -> JsonReporter {
63+
JsonReporter::new()
64+
}

crates/pet-reporter/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
pub mod cache;
55
pub mod collect;
66
pub mod environment;
7+
pub mod json;
78
pub mod jsonrpc;
89
pub mod stdio;

crates/pet/src/lib.rs

Lines changed: 93 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@ pub struct FindOptions {
3232
pub workspace_only: bool,
3333
pub cache_directory: Option<PathBuf>,
3434
pub kind: Option<PythonEnvironmentKind>,
35+
pub json: bool,
3536
}
3637

3738
pub fn find_and_report_envs_stdio(options: FindOptions) {
38-
stdio::initialize_logger(if options.verbose {
39-
log::LevelFilter::Trace
40-
} else {
41-
log::LevelFilter::Warn
42-
});
39+
// Don't initialize logger if JSON output is requested to avoid polluting JSON
40+
if !options.json {
41+
stdio::initialize_logger(if options.verbose {
42+
log::LevelFilter::Trace
43+
} else {
44+
log::LevelFilter::Warn
45+
});
46+
}
4347
let now = SystemTime::now();
4448
let config = create_config(&options);
4549
let search_scope = if options.workspace_only {
@@ -70,9 +74,12 @@ pub fn find_and_report_envs_stdio(options: FindOptions) {
7074
search_scope,
7175
);
7276

73-
println!("Completed in {}ms", now.elapsed().unwrap().as_millis())
77+
if !options.json {
78+
println!("Completed in {}ms", now.elapsed().unwrap().as_millis())
79+
}
7480
}
7581

82+
7683
fn create_config(options: &FindOptions) -> Configuration {
7784
let mut config = Configuration::default();
7885

@@ -120,77 +127,94 @@ fn find_envs(
120127
Some(SearchScope::Global(kind)) => Some(kind),
121128
_ => None,
122129
};
123-
let stdio_reporter = Arc::new(stdio::create_reporter(options.print_list, kind));
124-
let reporter = CacheReporter::new(stdio_reporter.clone());
125130

126-
let summary = find_and_report_envs(&reporter, config, locators, environment, search_scope);
127-
if options.report_missing {
128-
// By now all conda envs have been found
129-
// Spawn conda
130-
// & see if we can find more environments by spawning conda.
131-
let _ = conda_locator.find_and_report_missing_envs(&reporter, None);
132-
let _ = poetry_locator.find_and_report_missing_envs(&reporter, None);
133-
}
131+
if options.json {
132+
// Use JSON reporter
133+
let json_reporter = Arc::new(pet_reporter::json::create_reporter());
134+
let reporter = CacheReporter::new(json_reporter.clone());
134135

135-
if options.print_summary {
136-
let summary = summary.lock().unwrap();
137-
if !summary.locators.is_empty() {
138-
println!();
139-
println!("Breakdown by each locator:");
140-
println!("--------------------------");
141-
for locator in summary.locators.iter() {
142-
println!("{:<20} : {:?}", format!("{:?}", locator.0), locator.1);
143-
}
144-
println!()
136+
let _ = find_and_report_envs(&reporter, config, locators, environment, search_scope);
137+
if options.report_missing {
138+
let _ = conda_locator.find_and_report_missing_envs(&reporter, None);
139+
let _ = poetry_locator.find_and_report_missing_envs(&reporter, None);
145140
}
146141

147-
if !summary.breakdown.is_empty() {
148-
println!("Breakdown for finding Environments:");
149-
println!("-----------------------------------");
150-
for item in summary.breakdown.iter() {
151-
println!("{:<20} : {:?}", item.0, item.1);
152-
}
153-
println!();
142+
// Output JSON
143+
json_reporter.output_json();
144+
} else {
145+
// Use stdio reporter
146+
let stdio_reporter = Arc::new(stdio::create_reporter(options.print_list, kind));
147+
let reporter = CacheReporter::new(stdio_reporter.clone());
148+
149+
let summary = find_and_report_envs(&reporter, config, locators, environment, search_scope);
150+
if options.report_missing {
151+
// By now all conda envs have been found
152+
// Spawn conda
153+
// & see if we can find more environments by spawning conda.
154+
let _ = conda_locator.find_and_report_missing_envs(&reporter, None);
155+
let _ = poetry_locator.find_and_report_missing_envs(&reporter, None);
154156
}
155157

156-
let summary = stdio_reporter.get_summary();
157-
if !summary.managers.is_empty() {
158-
println!("Managers:");
159-
println!("---------");
160-
for (k, v) in summary
161-
.managers
162-
.clone()
163-
.into_iter()
164-
.map(|(k, v)| (format!("{k:?}"), v))
165-
.collect::<BTreeMap<String, u16>>()
166-
{
167-
println!("{k:<20} : {v:?}");
158+
if options.print_summary {
159+
let summary = summary.lock().unwrap();
160+
if !summary.locators.is_empty() {
161+
println!();
162+
println!("Breakdown by each locator:");
163+
println!("--------------------------");
164+
for locator in summary.locators.iter() {
165+
println!("{:<20} : {:?}", format!("{:?}", locator.0), locator.1);
166+
}
167+
println!()
168168
}
169-
println!()
170-
}
171-
if !summary.environments.is_empty() {
172-
let total = summary
173-
.environments
174-
.clone()
175-
.iter()
176-
.fold(0, |total, b| total + b.1);
177-
println!("Environments ({total}):");
178-
println!("------------------");
179-
for (k, v) in summary
180-
.environments
181-
.clone()
182-
.into_iter()
183-
.map(|(k, v)| {
184-
(
185-
k.map(|v| format!("{v:?}")).unwrap_or("Unknown".to_string()),
186-
v,
187-
)
188-
})
189-
.collect::<BTreeMap<String, u16>>()
190-
{
191-
println!("{k:<20} : {v:?}");
169+
170+
if !summary.breakdown.is_empty() {
171+
println!("Breakdown for finding Environments:");
172+
println!("-----------------------------------");
173+
for item in summary.breakdown.iter() {
174+
println!("{:<20} : {:?}", item.0, item.1);
175+
}
176+
println!();
177+
}
178+
179+
let summary = stdio_reporter.get_summary();
180+
if !summary.managers.is_empty() {
181+
println!("Managers:");
182+
println!("---------");
183+
for (k, v) in summary
184+
.managers
185+
.clone()
186+
.into_iter()
187+
.map(|(k, v)| (format!("{k:?}"), v))
188+
.collect::<BTreeMap<String, u16>>()
189+
{
190+
println!("{k:<20} : {v:?}");
191+
}
192+
println!()
193+
}
194+
if !summary.environments.is_empty() {
195+
let total = summary
196+
.environments
197+
.clone()
198+
.iter()
199+
.fold(0, |total, b| total + b.1);
200+
println!("Environments ({total}):");
201+
println!("------------------");
202+
for (k, v) in summary
203+
.environments
204+
.clone()
205+
.into_iter()
206+
.map(|(k, v)| {
207+
(
208+
k.map(|v| format!("{v:?}")).unwrap_or("Unknown".to_string()),
209+
v,
210+
)
211+
})
212+
.collect::<BTreeMap<String, u16>>()
213+
{
214+
println!("{k:<20} : {v:?}");
215+
}
216+
println!()
192217
}
193-
println!()
194218
}
195219
}
196220
}

crates/pet/src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ enum Commands {
5353
/// Will not search in the workspace directories.
5454
#[arg(short, long, conflicts_with = "workspace")]
5555
kind: Option<PythonEnvironmentKind>,
56+
57+
/// Output results in JSON format.
58+
#[arg(short, long)]
59+
json: bool,
5660
},
5761
/// Resolves & reports the details of the the environment to the standard output.
5862
Resolve {
@@ -83,6 +87,7 @@ fn main() {
8387
workspace: false,
8488
cache_directory: None,
8589
kind: None,
90+
json: false,
8691
}) {
8792
Commands::Find {
8893
list,
@@ -92,6 +97,7 @@ fn main() {
9297
workspace,
9398
cache_directory,
9499
kind,
100+
json,
95101
} => {
96102
let mut workspace_only = workspace;
97103
if search_paths.clone().is_some()
@@ -113,6 +119,7 @@ fn main() {
113119
workspace_only,
114120
cache_directory,
115121
kind,
122+
json,
116123
});
117124
}
118125
Commands::Resolve {

0 commit comments

Comments
 (0)