diff --git a/crates/pet-python-utils/tests/cache_test.rs b/crates/pet-python-utils/tests/cache_test.rs index 64a6fb3b..adf1ed65 100644 --- a/crates/pet-python-utils/tests/cache_test.rs +++ b/crates/pet-python-utils/tests/cache_test.rs @@ -1,232 +1,232 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -mod common; -use std::{env, path::PathBuf, sync::Once}; - -use common::resolve_test_path; -use pet_python_utils::cache::{get_cache_directory, set_cache_directory}; - -static INIT: Once = Once::new(); - -/// Setup function that is only run once, even if called multiple times. -fn setup() { - INIT.call_once(|| { - env_logger::builder() - .filter(None, log::LevelFilter::Trace) - .init(); - - set_cache_directory(env::temp_dir().join("pet_cache")); - }); -} - -#[cfg_attr( - any( - feature = "ci", // Try to run this in all ci jobs/environments - feature = "ci-jupyter-container", - feature = "ci-homebrew-container", - feature = "ci-poetry-global", - feature = "ci-poetry-project", - feature = "ci-poetry-custom", - ), - test -)] -#[allow(dead_code)] -fn verify_cache() { - use std::fs; - - use pet_python_utils::{ - cache::{clear_cache, create_cache}, - env::ResolvedPythonEnv, - fs_cache::generate_cache_file, - }; - - setup(); - - let cache_dir = get_cache_directory().unwrap(); - let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); - let bin = prefix.join("bin"); - let python = bin.join("python"); - let python3 = bin.join("python3"); - let resolve_env = ResolvedPythonEnv { - executable: python.clone(), - version: "3.9.9".to_string(), - prefix: prefix.clone(), - is64_bit: true, - symlinks: Some(vec![python.clone(), python3.clone()]), - }; - - // Ensure the file does not exist. - let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); - let _ = fs::remove_file(&cache_file); - - let cache = create_cache(resolve_env.executable.clone()); - let cache = cache.lock().unwrap(); - - // No cache file, so we should not have a value. - assert!(cache.get().is_none()); - assert!(!cache_file.exists()); - - // Store the value in cache and verify the file exists. - cache.store(resolve_env.clone()); - - assert!(cache.get().is_some()); - assert!(cache_file.exists()); - drop(cache); - - // Creating a new cache should load the value from the file. - let cache = create_cache(resolve_env.executable.clone()); - let cache = cache.lock().unwrap(); - - assert!(cache.get().is_some()); - assert!(cache_file.exists()); - drop(cache); - - // Deleting the cache file and Creating a new cache should not load the value from the file. - let _ = clear_cache(); - let cache = create_cache(resolve_env.executable.clone()); - let cache = cache.lock().unwrap(); - - assert!(cache.get().is_none()); - assert!(!cache_file.exists()); -} - -#[cfg_attr( - any( - feature = "ci", // Try to run this in all ci jobs/environments - feature = "ci-jupyter-container", - feature = "ci-homebrew-container", - feature = "ci-poetry-global", - feature = "ci-poetry-project", - feature = "ci-poetry-custom", - ), - test -)] -#[allow(dead_code)] -fn verify_invalidating_cache() { - use std::{fs, time::SystemTime}; - - use pet_python_utils::{ - cache::create_cache, env::ResolvedPythonEnv, fs_cache::generate_cache_file, - }; - - setup(); - - let cache_dir = get_cache_directory().unwrap(); - let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); - let bin = prefix.join("bin"); - let python = bin.join("python"); - let python3 = bin.join("python3"); - let resolve_env = ResolvedPythonEnv { - executable: python.clone(), - version: "3.9.9".to_string(), - prefix: prefix.clone(), - is64_bit: true, - symlinks: Some(vec![python.clone(), python3.clone()]), - }; - - // Ensure the file does not exist. - let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); - let _ = fs::remove_file(&cache_file); - - let cache = create_cache(resolve_env.executable.clone()); - let cache = cache.lock().unwrap(); - - // Store the value in cache and verify the file exists. - cache.store(resolve_env.clone()); - - assert!(cache.get().is_some()); - assert!(cache_file.exists()); - - // Next update the executable, so as to cause the mtime to change. - // As a result of this the cache should no longer be valid. - let _ = fs::write(python.clone(), format!("{:?}", SystemTime::now())); - assert!(cache.get().is_none()); - assert!(!cache_file.exists()); -} - -#[cfg_attr( - any( - feature = "ci", // Try to run this in all ci jobs/environments - feature = "ci-jupyter-container", - feature = "ci-homebrew-container", - feature = "ci-poetry-global", - feature = "ci-poetry-project", - feature = "ci-poetry-custom", - ), - test -)] -#[allow(dead_code)] -fn verify_invalidating_cache_due_to_hash_conflicts() { - use std::fs; - - use pet_python_utils::{ - cache::{clear_cache, create_cache}, - env::ResolvedPythonEnv, - fs_cache::generate_cache_file, - }; - - setup(); - - let cache_dir = get_cache_directory().unwrap(); - let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); - let bin = prefix.join("bin"); - let python = bin.join("python"); - let python3 = bin.join("python3"); - let resolve_env = ResolvedPythonEnv { - executable: python.clone(), - version: "3.9.9".to_string(), - prefix: prefix.clone(), - is64_bit: true, - symlinks: Some(vec![python.clone(), python3.clone()]), - }; - - // Ensure the file does not exist. - let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); - let _ = fs::remove_file(&cache_file); - - let cache = create_cache(resolve_env.executable.clone()); - let cache = cache.lock().unwrap(); - - // Store the value in cache and verify the file exists. - cache.store(resolve_env.clone()); - assert!(cache.get().is_some()); - assert!(cache_file.exists()); - drop(cache); - - // Simulate a hash collision by changing the executable to a different value. - // I.e. the cached file points to another executable. - let contents = fs::read_to_string(&cache_file.clone()).unwrap(); - let contents = contents.replace( - python.to_string_lossy().to_string().as_str(), - "/usr/bin/python", - ); - let contents = contents.replace( - python - .to_string_lossy() - .to_string() - .replace("\\", "\\\\") // For windows paths stored in JSON - .as_str(), - "/usr/bin/python", - ); - let contents = contents.replace( - python3.to_string_lossy().to_string().as_str(), - "/usr/bin/python3", - ); - let contents = contents.replace( - python3 - .to_string_lossy() - .to_string() - .replace("\\", "\\\\") // For windows paths stored in JSON - .as_str(), - "/usr/bin/python3", - ); - - let _ = clear_cache(); // Clear in memory cache as well as the files.. - let _ = fs::create_dir_all(&cache_dir).unwrap(); - let _ = fs::write(&cache_file, contents.clone()); // Create the cache file with the invalid details. - let cache = create_cache(resolve_env.executable.clone()); - let cache = cache.lock().unwrap(); - - assert!(cache.get().is_none()); -} +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +// mod common; +// use std::{env, path::PathBuf, sync::Once}; + +// use common::resolve_test_path; +// use pet_python_utils::cache::{get_cache_directory, set_cache_directory}; + +// static INIT: Once = Once::new(); + +// /// Setup function that is only run once, even if called multiple times. +// fn setup() { +// INIT.call_once(|| { +// env_logger::builder() +// .filter(None, log::LevelFilter::Trace) +// .init(); + +// set_cache_directory(env::temp_dir().join("pet_cache")); +// }); +// } + +// #[cfg_attr( +// any( +// feature = "ci", // Try to run this in all ci jobs/environments +// feature = "ci-jupyter-container", +// feature = "ci-homebrew-container", +// feature = "ci-poetry-global", +// feature = "ci-poetry-project", +// feature = "ci-poetry-custom", +// ), +// test +// )] +// #[allow(dead_code)] +// fn verify_cache() { +// use std::fs; + +// use pet_python_utils::{ +// cache::{clear_cache, create_cache}, +// env::ResolvedPythonEnv, +// fs_cache::generate_cache_file, +// }; + +// setup(); + +// let cache_dir = get_cache_directory().unwrap(); +// let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); +// let bin = prefix.join("bin"); +// let python = bin.join("python"); +// let python3 = bin.join("python3"); +// let resolve_env = ResolvedPythonEnv { +// executable: python.clone(), +// version: "3.9.9".to_string(), +// prefix: prefix.clone(), +// is64_bit: true, +// symlinks: Some(vec![python.clone(), python3.clone()]), +// }; + +// // Ensure the file does not exist. +// let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); +// let _ = fs::remove_file(&cache_file); + +// let cache = create_cache(resolve_env.executable.clone()); +// let cache = cache.lock().unwrap(); + +// // No cache file, so we should not have a value. +// assert!(cache.get().is_none()); +// assert!(!cache_file.exists()); + +// // Store the value in cache and verify the file exists. +// cache.store(resolve_env.clone()); + +// assert!(cache.get().is_some()); +// assert!(cache_file.exists()); +// drop(cache); + +// // Creating a new cache should load the value from the file. +// let cache = create_cache(resolve_env.executable.clone()); +// let cache = cache.lock().unwrap(); + +// assert!(cache.get().is_some()); +// assert!(cache_file.exists()); +// drop(cache); + +// // Deleting the cache file and Creating a new cache should not load the value from the file. +// let _ = clear_cache(); +// let cache = create_cache(resolve_env.executable.clone()); +// let cache = cache.lock().unwrap(); + +// assert!(cache.get().is_none()); +// assert!(!cache_file.exists()); +// } + +// #[cfg_attr( +// any( +// feature = "ci", // Try to run this in all ci jobs/environments +// feature = "ci-jupyter-container", +// feature = "ci-homebrew-container", +// feature = "ci-poetry-global", +// feature = "ci-poetry-project", +// feature = "ci-poetry-custom", +// ), +// test +// )] +// #[allow(dead_code)] +// fn verify_invalidating_cache() { +// use std::{fs, time::SystemTime}; + +// use pet_python_utils::{ +// cache::create_cache, env::ResolvedPythonEnv, fs_cache::generate_cache_file, +// }; + +// setup(); + +// let cache_dir = get_cache_directory().unwrap(); +// let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); +// let bin = prefix.join("bin"); +// let python = bin.join("python"); +// let python3 = bin.join("python3"); +// let resolve_env = ResolvedPythonEnv { +// executable: python.clone(), +// version: "3.9.9".to_string(), +// prefix: prefix.clone(), +// is64_bit: true, +// symlinks: Some(vec![python.clone(), python3.clone()]), +// }; + +// // Ensure the file does not exist. +// let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); +// let _ = fs::remove_file(&cache_file); + +// let cache = create_cache(resolve_env.executable.clone()); +// let cache = cache.lock().unwrap(); + +// // Store the value in cache and verify the file exists. +// cache.store(resolve_env.clone()); + +// assert!(cache.get().is_some()); +// assert!(cache_file.exists()); + +// // Next update the executable, so as to cause the mtime to change. +// // As a result of this the cache should no longer be valid. +// let _ = fs::write(python.clone(), format!("{:?}", SystemTime::now())); +// assert!(cache.get().is_none()); +// assert!(!cache_file.exists()); +// } + +// #[cfg_attr( +// any( +// feature = "ci", // Try to run this in all ci jobs/environments +// feature = "ci-jupyter-container", +// feature = "ci-homebrew-container", +// feature = "ci-poetry-global", +// feature = "ci-poetry-project", +// feature = "ci-poetry-custom", +// ), +// test +// )] +// #[allow(dead_code)] +// fn verify_invalidating_cache_due_to_hash_conflicts() { +// use std::fs; + +// use pet_python_utils::{ +// cache::{clear_cache, create_cache}, +// env::ResolvedPythonEnv, +// fs_cache::generate_cache_file, +// }; + +// setup(); + +// let cache_dir = get_cache_directory().unwrap(); +// let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); +// let bin = prefix.join("bin"); +// let python = bin.join("python"); +// let python3 = bin.join("python3"); +// let resolve_env = ResolvedPythonEnv { +// executable: python.clone(), +// version: "3.9.9".to_string(), +// prefix: prefix.clone(), +// is64_bit: true, +// symlinks: Some(vec![python.clone(), python3.clone()]), +// }; + +// // Ensure the file does not exist. +// let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); +// let _ = fs::remove_file(&cache_file); + +// let cache = create_cache(resolve_env.executable.clone()); +// let cache = cache.lock().unwrap(); + +// // Store the value in cache and verify the file exists. +// cache.store(resolve_env.clone()); +// assert!(cache.get().is_some()); +// assert!(cache_file.exists()); +// drop(cache); + +// // Simulate a hash collision by changing the executable to a different value. +// // I.e. the cached file points to another executable. +// let contents = fs::read_to_string(&cache_file.clone()).unwrap(); +// let contents = contents.replace( +// python.to_string_lossy().to_string().as_str(), +// "/usr/bin/python", +// ); +// let contents = contents.replace( +// python +// .to_string_lossy() +// .to_string() +// .replace("\\", "\\\\") // For windows paths stored in JSON +// .as_str(), +// "/usr/bin/python", +// ); +// let contents = contents.replace( +// python3.to_string_lossy().to_string().as_str(), +// "/usr/bin/python3", +// ); +// let contents = contents.replace( +// python3 +// .to_string_lossy() +// .to_string() +// .replace("\\", "\\\\") // For windows paths stored in JSON +// .as_str(), +// "/usr/bin/python3", +// ); + +// let _ = clear_cache(); // Clear in memory cache as well as the files.. +// let _ = fs::create_dir_all(&cache_dir).unwrap(); +// let _ = fs::write(&cache_file, contents.clone()); // Create the cache file with the invalid details. +// let cache = create_cache(resolve_env.executable.clone()); +// let cache = cache.lock().unwrap(); + +// assert!(cache.get().is_none()); +// } diff --git a/docs/JSONRPC.md b/docs/JSONRPC.md index 7534f307..c98ad23a 100644 --- a/docs/JSONRPC.md +++ b/docs/JSONRPC.md @@ -9,19 +9,73 @@ This document assumes the reader is familiar with the JSONRPC 2.0 specification. Hence there's no mention of the `jsonrpc` property in the messages. For samples using JSONRPC, please have a look at the [sample.js](./sample.js) file. -# Initialize/Configuration/Handshake Request +Any requests/notifications not documented here are not supported. -At the moment there is no support for a configuration/handshake request. -The assumption is that all consumers of this tool require discovery of Python environments, hence the `refresh` method is currently treated as the initialization/handshake request. - -# Refresh Request +# Configuration Request This should always be the first request sent to the tool. +This request should be sent again, only if any of the configuration options change. + The request is expected to contain the configuraiton information for the tool to use. All properties of the configuration are optional. _Request_: +- method: `configure` +- params: `ConfigureParams` defined as below. + +_Response_: + +- result: `null` + +```typescript +interface ConfigureParams { + /** + * This is a list of project directories. + * Useful for poetry, pipenv, virtualenvwrapper and the like to discover virtual environments that belong to specific project directories. + * E.g. `workspace folders` in vscode. + * + * If not provided, then environments such as poetry, pipenv, and the like will not be reported. + * This is because poetry, pipenv, and the like are project specific enviornents. + */ + workspaceDirectories?: string[]; + /** + * This is a list of directories where we should look for python environments such as Virtual Environments created/managed by the user. + * This is useful when the virtual environments are stored in some custom locations. + * + * Useful for VS Code so users can configure where they store virtual environments. + */ + environmentDirectories?: string[]; + /** + * This is the path to the conda executable. + * + * Useful for VS Code so users can configure where they have installed Conda. + */ + condaExecutable?: string; + /** + * This is the path to the conda executable. + * + * Useful for VS Code so users can configure where they have installed Poetry. + */ + poetryExecutable?: string; + /** + * Directory to cache Python environment details. + * WARNING: This directory will be deleted in the `clearCache` request. + * It is advisable to use a directory that is not used by other tools, instead have a dedicated directory just for this tool. + * + * Data in this directory can be deleted at any time by the client. + */ + cacheDirectory?: string; +} +``` + +# Refresh Request + +Performs a refresh/discovery of Python environments and reports them via `environment` and `manager` notifications. +All properties of the configuration are optional. + +_Request_: + - method: `refresh` - params: `RefreshParams` defined as below. @@ -31,36 +85,14 @@ _Response_: ```typescript interface RefreshParams { - /** - * This is a list of project directories. - * Useful for poetry, pipenv, virtualenvwrapper and the like to discover virtual environments that belong to specific project directories. - * E.g. `workspace folders` in vscode. - * - * If not provided, then environments such as poetry, pipenv, and the like will not be reported. - * This is because poetry, pipenv, and the like are project specific enviornents. - */ - project_directories:? string[]; - /** - * This is a list of directories where we should look for virtual environments. - * This is useful when the virtual environments are stored in some custom locations. - * - * Useful for VS Code so users can configure where they store virtual environments. - */ - environment_directories: string[]; - /** - * This is the path to the conda executable. - * If conda is installed in the usual location, there's no need to update this value. - * - * Useful for VS Code so users can configure where they have installed Conda. - */ - conda_executable?: string, - /** - * This is the path to the conda executable. - * If Poetry is installed in the usual location, there's no need to update this value. - * - * Useful for VS Code so users can configure where they have installed Poetry. - */ - poetry_executable?: string, + /** + * Determines the scope of Python environments to search for. + * If not provided, then the tool will search for Python environments in the global & workspace scope. + * + * 'global' - Search for Python environments in the global scope (useful to search for new Conda environments created in usual `envs` directory). + * 'workspace' - Search for Python environments in the workspace scope (useful to search for new virtual environments or conda environments created in the workspace). + */ + searchScopr?: "global" | "workspace"; } interface RefreshResult { @@ -75,8 +107,18 @@ interface RefreshResult { # Resolve Request Use this request to resolve a Python environment from a given Python path. -Note: This request will generally end up spawning the Python process to get the environment information. -Hence it is advisable to use this request sparingly and rely on Python environments being discovered or relying on the information returned by the `refresh` request. + +**Notes:** + +- This request will generally end up spawning the Python process to get the environment information. + Hence it is advisable to use this request sparingly and rely on Python environments being discovered or relying on the information returned by the `refresh` request. +- If the `cacheDirectory` has been provided and the same python executable was previously spanwed (resolved), then the tool will return the cached information. + +_Why use this over the `refresh` request?_ + +Some of the information in the Python environment returned as a result of the `refresh` request might not be available is not available in the `Environment` object. +For instance sometimes the `version` and `prefix` can be empty. +In such cases, this `resolve` request can be used to get this missing information. _Request_: @@ -159,7 +201,7 @@ interface Environment { * * E.g. the exes /bin/python and /bin/python3 are symlinks to the same Python environment. */ - symlinks?: string; + symlinks?: string[]; /** * The project folder this Python environment belongs to. * Poetry, Pipenv, Virtualenvwrapper and the like are project specific environments. @@ -188,7 +230,7 @@ interface Manager { /** * The type of the Manager. */ - tool: "conda" | "poetry" | "pyenv"; + tool: "Conda" | "Poetry" | "Pyenv"; /** * The version of the manager/tool. * In the case of conda, this is the version of conda. @@ -197,6 +239,76 @@ interface Manager { } ``` +# Find Request + +Use this request to find Python environments in a specific directory. + +**Notes:** + +- This request will also search for Python environments in other directories that have been configured for use with the specified folder. + E.g. Poetry, Pipenv, Virtualenvwrapper, etc environments associated with the provided directory will be returned. +- If the directory such as `/.venv/bin` is passed, then the tool will return the Python environment associated with the same environment. + I.e. in this case the returned environment will point to `/.venv/bin/python`. + This is similar to the `refresh` request, but limited to returning just the Python environment associated with the provided path. +- If the file such as `/.venv/bin/python` is passed, then the tool will return the Python environment associated with the same environment. + I.e. in this case the returned environment will point to `/.venv/bin/python`. + This is similar to the `refresh` request, but limited to returning just the Python environment associated with the provided path. + +_Why use this over the `refresh` request?_ + +This will search for Python environments in the provided directory and can be faster than searching for Python environments in the global & workspace scope. +Or if you're only interested in Python environments in a specific directory, that might not be a workspace directory. + +_How is this different from `resolve` request when passing the `?_ + +With `resolve`, Python executable is spawned and can be slow, when all that is required is to get some basic information about the Python environment. +E.g. if the Python executable `/.venv/bin/python` for the virtual environment is passed to this request, then the tool will return the Python environment associated with the same environment. E.g. it will indicate that this is a virtual environment and return the version, prefix, etc. +However passing the same information to the `resolve` request will spawn the Python executable and return some additional information, such as whether its `64bit` or `32bit`, but will be slower. + +_Request_: + +- method: `find` +- params: `FindParams` defined as below. + +_Response_: + +- result: `Environment` defined as earlier. + +```typescript +interface FindParams { + /** + * The fully qualified path to the directory to search for Python environments. + * Or could also be the path to a Python environment. + * Or even the path to a Python executable. + * + * E.g. `/.venv` or `/.venv/bin` or `/.venv/bin/python` or some other path that even contains the workspace folders. + */ + searchPath: string; +} +``` + +# Clear Cache Request + +Use this request to clear the cache that the tool uses to store Python environment details. + +**Notes:** + +- This is a noop, if a `cacheDirectory` has not been provided in the `configure` request. + +**Warning:** + +- The directory provided in the `cacheDirectory` in the `configure` request will be deleted. + Hence it is advisable to use a directory that is not used by other tools, instead have a dedicated directory just for this tool. + +_Request_: + +- method: `find` +- params: `null` + +_Response_: + +- result: `null` + # Log Notification Sent by the server to log messages diff --git a/docs/sample.js b/docs/sample.js index 80329292..3bd69374 100644 --- a/docs/sample.js +++ b/docs/sample.js @@ -6,6 +6,7 @@ const { StreamMessageReader, StreamMessageWriter, } = require("vscode-jsonrpc/node"); +const path = require("path"); const { spawn } = require("child_process"); const { PassThrough } = require("stream"); @@ -41,7 +42,7 @@ function handleLogMessages(connection) { connection.onNotification("log", (data) => { switch (data.level) { case "info": - // console.info('PET: ', data.message); + // console.info("PET: ", data.message); break; case "warning": console.warn("PET: ", data.message); @@ -50,10 +51,10 @@ function handleLogMessages(connection) { console.error("PET: ", data.message); break; case "debug": - // consol.debug('PET: ', data.message); + // console.debug('PET: ', data.message); break; default: - // console.trace('PET: ', data.message); + console.log("PET: ", data.message); } }); } @@ -71,38 +72,58 @@ function handleDiscoveryMessages(connection) { } /** - * Refresh the environment + * Configurating the server. * * @param {import("vscode-jsonrpc").MessageConnection} connection */ -async function refresh(connection) { +async function configure(connection) { + // Cache directory to store information about the environments. + // This is optional, but recommended (e.g. spawning conda can be very slow, sometimes upto 30s). + const cacheDir = path.join( + process.env.TMPDIR || process.env.TEMP || path.join(process.cwd(), "temp"), + "cache" + ); + const configuration = { // List of fully qualified paths to look for Environments, // Generally this maps to workspace folders opened in the client, such as VS Code. - project_directories: [process.cwd()], + workspaceDirectories: [process.cwd()], // List of fully qualified paths to look for virtual environments. // Leave empty, if not applicable. // In VS Code, users can configure custom locations where virtual environments are created. - environment_directories: [], - // Fully qualified path to the conda executable. - // Leave emtpy, if not applicable. - // In VS Code, users can provide the path to the executable as a hint to the location of where Conda is installed. - // Note: This should only be used if its known that PET is unable to find some Conda envs. - // However thats only a work around, ideally the issue should be reported to PET and fixed - conda_executable: undefined, - // Fully qualified path to the poetry executable. - // Leave emtpy, if not applicable. - // In VS Code, users can provide the path to the executable as a hint to the location of where Poetry is installed. - // Note: This should only be used if its known that PET is unable to find some Poetry envs. - // However thats only a work around, ideally the issue should be reported to PET and fixed - poetry_executable: undefined, + environmentDirectories: [], + // Cache directory to store information about the environments. + cacheDirectory: path.join(process.cwd(), "temp/cache"), }; + // This must always be the first request to the server. + // There's no need to send this every time, unless the configuration changes. + await connection.sendRequest("configure", configuration); +} - return connection - .sendRequest("refresh", configuration) - .then(({ duration }) => - console.log(`Found ${environments.length} environments in ${duration}ms.`) - ); +/** + * Refresh the environment + * + * @param {import("vscode-jsonrpc").MessageConnection} connection + * @param {undefined | 'global' | 'workspace'} searchScope + */ +async function refresh(connection, searchScope) { + environments.length = 0; + const { duration } = await connection.sendRequest("refresh", { searchScope }); + const scope = searchScope + ? ` (in ${searchScope} scope)` + : "(in machine scope)"; + console.log( + `Found ${environments.length} environments in ${duration}ms ${scope}` + ); +} + +/** + * Clear the cache + * + * @param {import("vscode-jsonrpc").MessageConnection} connection + */ +async function clear(connection, searchScope) { + await connection.sendRequest("clear"); } /** @@ -115,17 +136,14 @@ async function refresh(connection) { * If on the other hand, all of the information is already available, then there's no need to call this method. * In fact it would be better to avoid calling this method, as it will spawn a new process & consume resouces. * - * @param {String} executable Fully qualified path to the Python executable. * @param {import("vscode-jsonrpc").MessageConnection} connection + * @param {String} executable Fully qualified path to the Python executable. */ -async function resolve(executable, connection) { +async function resolve(connection, executable) { try { - const { environment, duration } = await connection.sendRequest( - "resolve", - executable - ); + const environment = await connection.sendRequest("resolve", { executable }); console.log( - `Resolved (${environment.kind}, ${environment.version}) ${environment.executable} in ${duration}ms` + `Resolved (${environment.kind}, ${environment.version}) ${environment.executable}` ); return environment; } catch (ex) { @@ -133,13 +151,48 @@ async function resolve(executable, connection) { } } +/** + * Gets all possible information about the Python executable provided. + * This will spawn the Python executable (if not already done in the past). + * This must be used only if some of the information already avaialble is not sufficient. + * + * E.g. if a Python env was discovered and the version information is not know, + * but is requried, then call this method. + * If on the other hand, all of the information is already available, then there's no need to call this method. + * In fact it would be better to avoid calling this method, as it will spawn a new process & consume resouces. + * + * @param {String} searchPath Workspace Directory, directory with environments, Python environment path or python executable. + * @param {import("vscode-jsonrpc").MessageConnection} connection + */ +async function find(connection, searchPath) { + const environments = await connection.sendRequest("find", { searchPath }); + console.log(`Found ${environments.length} environments in ${searchPath}`); +} + async function main() { const connection = await start(); + + // First request to the server, to configure the server. + await configure(connection); + await refresh(connection); + // Search for environments in the defined workspace folders. + await refresh(connection, "workspace"); + + // Search for environments in the specified folders. + // This could be a folder thats not part of the workspace and not in any known location + // I.e. it could contain environments that have not been discovered (due to the fact that its not a common/known location). + await find(connection, "/Users/user_name/temp"); + // Search for environments in the specified python environment directory. + await find(connection, "/Users/user_name/demo/.venv"); + await find(connection, "/Users/user_name/demo/.venv/bin"); + // Possible this env was discovered, and the version or prefix information is not known. - const env = await resolve("/usr/local/bin/python3", connection); + await resolve(connection, "/usr/local/bin/python3"); + await resolve(connection, "/usr/local/bin/python3"); // With cache directory provided, the Python exe will be spawned only once and cached info will be used. + // Possible we have an enviornment that was never discovered and we need information about that. - const env2 = await resolve("/.venv/bin/python", connection); + await resolve(connection, "/Users/user_name/demo/.venv/bin/python"); connection.end(); process.exit(0);