diff --git a/README.md b/README.md index 64ca2e3e..94ca47db 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,90 @@ This project will be consumed by the [Python extension](https://marketplace.visu Our approach prioritizes performance and efficiency by leveraging Rust. We minimize I/O operations by collecting all necessary environment information at once, which reduces repeated I/O and the need to spawn additional processes, significantly enhancing overall performance. +## Debugging Python Environment Issues + +If you're experiencing issues with Python interpreter detection in VS Code (such as the Run button not working, Python not being recognized, or interpreters not persisting), you can use PET to diagnose the problem. + +### Running PET for Debugging + +PET can be run directly from the command line to discover all Python environments on your system. This helps identify whether the issue is with environment discovery or elsewhere. + +#### Quick Start + +1. **Download or build PET**: + - Download pre-built binaries from the [releases page](https://github.com/microsoft/python-environment-tools/releases) + - Or build from source: `cargo build --release` + +2. **Run PET to find all environments**: + ```bash + # On Linux/macOS + ./pet find --list --verbose + + # On Windows + pet.exe find --list --verbose + ``` + +3. **Share the output** with maintainers when reporting issues + +#### Common Commands + +- **Find all Python environments** (default behavior): + ```bash + pet + ``` + +- **Find all environments with detailed logging**: + ```bash + pet find --list --verbose + ``` + +- **Find all environments and output as JSON**: + ```bash + pet find --json + ``` + +- **Search only in workspace/project directories**: + ```bash + pet find --list --workspace + ``` + +- **Search for a specific environment type** (e.g., Conda): + ```bash + pet find --list --kind conda + ``` + +- **Resolve a specific Python executable**: + ```bash + pet resolve /path/to/python + ``` + +#### Understanding the Output + +The output includes: + +- **Discovered Environments**: List of Python installations found, including: + - Type (Conda, Venv, System Python, etc.) + - Executable path + - Version + - Prefix (sys.prefix) + - Architecture (x64/x86) + - Symlinks + +- **Timing Information**: How long each locator took to search + +- **Summary Statistics**: Count of environments by type + +#### Reporting Issues + +When reporting Python detection issues, please include: + +1. The full output from running `pet find --list --verbose` +2. Your operating system and version +3. VS Code and Python extension versions +4. Description of the issue + +This information helps maintainers diagnose whether the problem is with PET's discovery logic or elsewhere in the VS Code Python extension. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/crates/pet-reporter/src/json.rs b/crates/pet-reporter/src/json.rs new file mode 100644 index 00000000..596a721d --- /dev/null +++ b/crates/pet-reporter/src/json.rs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pet_core::{ + manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter, + telemetry::TelemetryEvent, +}; +use serde::Serialize; +use std::sync::{Arc, Mutex}; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JsonOutput { + pub managers: Vec, + pub environments: Vec, +} + +/// Reporter that collects environments and managers for JSON output +pub struct JsonReporter { + managers: Arc>>, + environments: Arc>>, +} + +impl Default for JsonReporter { + fn default() -> Self { + Self::new() + } +} + +impl JsonReporter { + pub fn new() -> Self { + JsonReporter { + managers: Arc::new(Mutex::new(vec![])), + environments: Arc::new(Mutex::new(vec![])), + } + } + + pub fn output_json(&self) { + let managers = self.managers.lock().unwrap().clone(); + let environments = self.environments.lock().unwrap().clone(); + + let output = JsonOutput { + managers, + environments, + }; + + match serde_json::to_string_pretty(&output) { + Ok(json) => println!("{}", json), + Err(e) => eprintln!("Error serializing to JSON: {}", e), + } + } +} + +impl Reporter for JsonReporter { + fn report_telemetry(&self, _event: &TelemetryEvent) { + // No telemetry in JSON output + } + + fn report_manager(&self, manager: &EnvManager) { + self.managers.lock().unwrap().push(manager.clone()); + } + + fn report_environment(&self, env: &PythonEnvironment) { + self.environments.lock().unwrap().push(env.clone()); + } +} + +pub fn create_reporter() -> JsonReporter { + JsonReporter::new() +} diff --git a/crates/pet-reporter/src/lib.rs b/crates/pet-reporter/src/lib.rs index 82280be9..828209ad 100644 --- a/crates/pet-reporter/src/lib.rs +++ b/crates/pet-reporter/src/lib.rs @@ -4,5 +4,6 @@ pub mod cache; pub mod collect; pub mod environment; +pub mod json; pub mod jsonrpc; pub mod stdio; diff --git a/crates/pet/src/lib.rs b/crates/pet/src/lib.rs index 68f9aed4..0b163ca8 100644 --- a/crates/pet/src/lib.rs +++ b/crates/pet/src/lib.rs @@ -32,14 +32,18 @@ pub struct FindOptions { pub workspace_only: bool, pub cache_directory: Option, pub kind: Option, + pub json: bool, } pub fn find_and_report_envs_stdio(options: FindOptions) { - stdio::initialize_logger(if options.verbose { - log::LevelFilter::Trace - } else { - log::LevelFilter::Warn - }); + // Don't initialize logger if JSON output is requested to avoid polluting JSON + if !options.json { + stdio::initialize_logger(if options.verbose { + log::LevelFilter::Trace + } else { + log::LevelFilter::Warn + }); + } let now = SystemTime::now(); let config = create_config(&options); let search_scope = if options.workspace_only { @@ -70,9 +74,12 @@ pub fn find_and_report_envs_stdio(options: FindOptions) { search_scope, ); - println!("Completed in {}ms", now.elapsed().unwrap().as_millis()) + if !options.json { + println!("Completed in {}ms", now.elapsed().unwrap().as_millis()) + } } + fn create_config(options: &FindOptions) -> Configuration { let mut config = Configuration::default(); @@ -120,77 +127,94 @@ fn find_envs( Some(SearchScope::Global(kind)) => Some(kind), _ => None, }; - let stdio_reporter = Arc::new(stdio::create_reporter(options.print_list, kind)); - let reporter = CacheReporter::new(stdio_reporter.clone()); - let summary = find_and_report_envs(&reporter, config, locators, environment, search_scope); - if options.report_missing { - // By now all conda envs have been found - // Spawn conda - // & see if we can find more environments by spawning conda. - let _ = conda_locator.find_and_report_missing_envs(&reporter, None); - let _ = poetry_locator.find_and_report_missing_envs(&reporter, None); - } + if options.json { + // Use JSON reporter + let json_reporter = Arc::new(pet_reporter::json::create_reporter()); + let reporter = CacheReporter::new(json_reporter.clone()); - if options.print_summary { - let summary = summary.lock().unwrap(); - if !summary.locators.is_empty() { - println!(); - println!("Breakdown by each locator:"); - println!("--------------------------"); - for locator in summary.locators.iter() { - println!("{:<20} : {:?}", format!("{:?}", locator.0), locator.1); - } - println!() + let _ = find_and_report_envs(&reporter, config, locators, environment, search_scope); + if options.report_missing { + let _ = conda_locator.find_and_report_missing_envs(&reporter, None); + let _ = poetry_locator.find_and_report_missing_envs(&reporter, None); } - if !summary.breakdown.is_empty() { - println!("Breakdown for finding Environments:"); - println!("-----------------------------------"); - for item in summary.breakdown.iter() { - println!("{:<20} : {:?}", item.0, item.1); - } - println!(); + // Output JSON + json_reporter.output_json(); + } else { + // Use stdio reporter + let stdio_reporter = Arc::new(stdio::create_reporter(options.print_list, kind)); + let reporter = CacheReporter::new(stdio_reporter.clone()); + + let summary = find_and_report_envs(&reporter, config, locators, environment, search_scope); + if options.report_missing { + // By now all conda envs have been found + // Spawn conda + // & see if we can find more environments by spawning conda. + let _ = conda_locator.find_and_report_missing_envs(&reporter, None); + let _ = poetry_locator.find_and_report_missing_envs(&reporter, None); } - let summary = stdio_reporter.get_summary(); - if !summary.managers.is_empty() { - println!("Managers:"); - println!("---------"); - for (k, v) in summary - .managers - .clone() - .into_iter() - .map(|(k, v)| (format!("{k:?}"), v)) - .collect::>() - { - println!("{k:<20} : {v:?}"); + if options.print_summary { + let summary = summary.lock().unwrap(); + if !summary.locators.is_empty() { + println!(); + println!("Breakdown by each locator:"); + println!("--------------------------"); + for locator in summary.locators.iter() { + println!("{:<20} : {:?}", format!("{:?}", locator.0), locator.1); + } + println!() } - println!() - } - if !summary.environments.is_empty() { - let total = summary - .environments - .clone() - .iter() - .fold(0, |total, b| total + b.1); - println!("Environments ({total}):"); - println!("------------------"); - for (k, v) in summary - .environments - .clone() - .into_iter() - .map(|(k, v)| { - ( - k.map(|v| format!("{v:?}")).unwrap_or("Unknown".to_string()), - v, - ) - }) - .collect::>() - { - println!("{k:<20} : {v:?}"); + + if !summary.breakdown.is_empty() { + println!("Breakdown for finding Environments:"); + println!("-----------------------------------"); + for item in summary.breakdown.iter() { + println!("{:<20} : {:?}", item.0, item.1); + } + println!(); + } + + let summary = stdio_reporter.get_summary(); + if !summary.managers.is_empty() { + println!("Managers:"); + println!("---------"); + for (k, v) in summary + .managers + .clone() + .into_iter() + .map(|(k, v)| (format!("{k:?}"), v)) + .collect::>() + { + println!("{k:<20} : {v:?}"); + } + println!() + } + if !summary.environments.is_empty() { + let total = summary + .environments + .clone() + .iter() + .fold(0, |total, b| total + b.1); + println!("Environments ({total}):"); + println!("------------------"); + for (k, v) in summary + .environments + .clone() + .into_iter() + .map(|(k, v)| { + ( + k.map(|v| format!("{v:?}")).unwrap_or("Unknown".to_string()), + v, + ) + }) + .collect::>() + { + println!("{k:<20} : {v:?}"); + } + println!() } - println!() } } } diff --git a/crates/pet/src/main.rs b/crates/pet/src/main.rs index f22bf369..6223e281 100644 --- a/crates/pet/src/main.rs +++ b/crates/pet/src/main.rs @@ -37,6 +37,7 @@ enum Commands { cache_directory: Option, /// Display verbose output (defaults to warnings). + /// Note: Has no effect when --json is used, as logging is disabled to avoid polluting JSON output. #[arg(short, long)] verbose: bool, @@ -53,6 +54,10 @@ enum Commands { /// Will not search in the workspace directories. #[arg(short, long, conflicts_with = "workspace")] kind: Option, + + /// Output results in JSON format. + #[arg(short, long)] + json: bool, }, /// Resolves & reports the details of the the environment to the standard output. Resolve { @@ -83,6 +88,7 @@ fn main() { workspace: false, cache_directory: None, kind: None, + json: false, }) { Commands::Find { list, @@ -92,6 +98,7 @@ fn main() { workspace, cache_directory, kind, + json, } => { let mut workspace_only = workspace; if search_paths.clone().is_some() @@ -113,6 +120,7 @@ fn main() { workspace_only, cache_directory, kind, + json, }); } Commands::Resolve {