Skip to content

Commit 1a405f5

Browse files
authored
feat: add --json flag to find and resolve commands (#351)
Adds a `--json` / `-j` flag to the `find` and `resolve` CLI commands for simple one-shot JSON output without needing the JSONRPC server. - `pet find --json` outputs `{ "managers": [...], "environments": [...] }` - `pet resolve --json <exe>` outputs the resolved `PythonEnvironment` object (or `null`) - All other flags (`--list`, `--verbose`, `--report-missing`, etc.) continue to work as before - JSON serialization uses `camelCase` field naming consistent with the JSONRPC API - Summary/timing output is suppressed in JSON mode to keep stdout clean Fixes #87
1 parent ce922d6 commit 1a405f5

File tree

2 files changed

+114
-23
lines changed

2 files changed

+114
-23
lines changed

crates/pet/src/lib.rs

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ use pet_core::{os_environment::EnvironmentApi, reporter::Reporter, Configuration
1313
use pet_poetry::Poetry;
1414
use pet_poetry::PoetryLocator;
1515
use pet_python_utils::cache::set_cache_directory;
16-
use pet_reporter::{self, cache::CacheReporter, stdio};
16+
use pet_reporter::{self, cache::CacheReporter, collect, stdio};
1717
use resolve::resolve_environment;
18+
use serde::Serialize;
1819
use std::path::PathBuf;
1920
use std::{collections::BTreeMap, env, sync::Arc, time::SystemTime};
2021
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
@@ -73,6 +74,7 @@ pub struct FindOptions {
7374
pub workspace_only: bool,
7475
pub cache_directory: Option<PathBuf>,
7576
pub kind: Option<PythonEnvironmentKind>,
77+
pub json: bool,
7678
}
7779

7880
pub fn find_and_report_envs_stdio(options: FindOptions) {
@@ -103,17 +105,29 @@ pub fn find_and_report_envs_stdio(options: FindOptions) {
103105
locator.configure(&config);
104106
}
105107

106-
find_envs(
107-
&options,
108-
&locators,
109-
config,
110-
conda_locator.as_ref(),
111-
poetry_locator.as_ref(),
112-
&environment,
113-
search_scope,
114-
);
115-
116-
println!("Completed in {}ms", now.elapsed().unwrap().as_millis())
108+
if options.json {
109+
find_envs_json(
110+
&options,
111+
&locators,
112+
config,
113+
conda_locator.as_ref(),
114+
poetry_locator.as_ref(),
115+
&environment,
116+
search_scope,
117+
);
118+
} else {
119+
find_envs(
120+
&options,
121+
&locators,
122+
config,
123+
conda_locator.as_ref(),
124+
poetry_locator.as_ref(),
125+
&environment,
126+
search_scope,
127+
);
128+
129+
println!("Completed in {}ms", now.elapsed().unwrap().as_millis())
130+
}
117131
}
118132

119133
fn create_config(options: &FindOptions) -> Configuration {
@@ -257,7 +271,62 @@ fn find_envs(
257271
}
258272
}
259273

260-
pub fn resolve_report_stdio(executable: PathBuf, verbose: bool, cache_directory: Option<PathBuf>) {
274+
#[derive(Serialize)]
275+
#[serde(rename_all = "camelCase")]
276+
struct JsonOutput {
277+
managers: Vec<pet_core::manager::EnvManager>,
278+
environments: Vec<pet_core::python_environment::PythonEnvironment>,
279+
}
280+
281+
fn find_envs_json(
282+
options: &FindOptions,
283+
locators: &Arc<Vec<Arc<dyn Locator>>>,
284+
config: Configuration,
285+
conda_locator: &Conda,
286+
poetry_locator: &Poetry,
287+
environment: &dyn Environment,
288+
search_scope: Option<SearchScope>,
289+
) {
290+
let collect_reporter = Arc::new(collect::create_reporter());
291+
let reporter = CacheReporter::new(collect_reporter.clone());
292+
293+
find_and_report_envs(&reporter, config, locators, environment, search_scope);
294+
if options.report_missing {
295+
let _ = conda_locator.find_and_report_missing_envs(&reporter, None);
296+
let _ = poetry_locator.find_and_report_missing_envs(&reporter, None);
297+
}
298+
299+
let managers = collect_reporter
300+
.managers
301+
.lock()
302+
.expect("managers mutex poisoned")
303+
.clone();
304+
let mut environments = collect_reporter
305+
.environments
306+
.lock()
307+
.expect("environments mutex poisoned")
308+
.clone();
309+
310+
if let Some(kind) = options.kind {
311+
environments.retain(|e| e.kind == Some(kind));
312+
}
313+
314+
let output = JsonOutput {
315+
managers,
316+
environments,
317+
};
318+
println!(
319+
"{}",
320+
serde_json::to_string_pretty(&output).expect("failed to serialize environments as JSON")
321+
);
322+
}
323+
324+
pub fn resolve_report_stdio(
325+
executable: PathBuf,
326+
verbose: bool,
327+
cache_directory: Option<PathBuf>,
328+
json: bool,
329+
) {
261330
// Initialize tracing for performance profiling (includes log compatibility)
262331
initialize_tracing(verbose);
263332

@@ -287,19 +356,29 @@ pub fn resolve_report_stdio(executable: PathBuf, verbose: bool, cache_directory:
287356
}
288357

289358
if let Some(result) = resolve_environment(&executable, &locators, &environment) {
290-
//
291-
println!("Environment found for {executable:?}");
292359
let env = &result.resolved.unwrap_or(result.discovered);
293-
if let Some(manager) = &env.manager {
294-
reporter.report_manager(manager);
360+
if json {
361+
println!(
362+
"{}",
363+
serde_json::to_string_pretty(env).expect("failed to serialize environment as JSON")
364+
);
365+
} else {
366+
println!("Environment found for {executable:?}");
367+
if let Some(manager) = &env.manager {
368+
reporter.report_manager(manager);
369+
}
370+
reporter.report_environment(env);
295371
}
296-
reporter.report_environment(env);
372+
} else if json {
373+
println!("null");
297374
} else {
298375
println!("No environment found for {executable:?}");
299376
}
300377

301-
println!(
302-
"Resolve completed in {}ms",
303-
now.elapsed().unwrap().as_millis()
304-
)
378+
if !json {
379+
println!(
380+
"Resolve completed in {}ms",
381+
now.elapsed().unwrap().as_millis()
382+
)
383+
}
305384
}

crates/pet/src/main.rs

Lines changed: 13 additions & 1 deletion
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 as JSON.
58+
#[arg(short, long)]
59+
json: bool,
5660
},
5761
/// Resolves & reports the details of the the environment to the standard output.
5862
Resolve {
@@ -67,6 +71,10 @@ enum Commands {
6771
/// Whether to display verbose output (defaults to warnings).
6872
#[arg(short, long)]
6973
verbose: bool,
74+
75+
/// Output results as JSON.
76+
#[arg(short, long)]
77+
json: bool,
7078
},
7179
/// Starts the JSON RPC Server.
7280
Server,
@@ -83,6 +91,7 @@ fn main() {
8391
workspace: false,
8492
cache_directory: None,
8593
kind: None,
94+
json: false,
8695
}) {
8796
Commands::Find {
8897
list,
@@ -92,6 +101,7 @@ fn main() {
92101
workspace,
93102
cache_directory,
94103
kind,
104+
json,
95105
} => {
96106
let mut workspace_only = workspace;
97107
if search_paths.clone().is_some()
@@ -113,13 +123,15 @@ fn main() {
113123
workspace_only,
114124
cache_directory,
115125
kind,
126+
json,
116127
});
117128
}
118129
Commands::Resolve {
119130
executable,
120131
verbose,
121132
cache_directory,
122-
} => resolve_report_stdio(executable, verbose, cache_directory),
133+
json,
134+
} => resolve_report_stdio(executable, verbose, cache_directory, json),
123135
Commands::Server => start_jsonrpc_server(),
124136
}
125137
}

0 commit comments

Comments
 (0)