Skip to content

Proposed API for discovered python environments #18314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
blank_issues_enabled: false
contact_links:
- name: "Bug 🐜"
- name: 'Bug 🐜'
url: https://aka.ms/pvsc-bug
about: "Use the `Python: Report Issue...` command (follow the link for instructions)"
- name: "Pylance"
about: 'Use the `Python: Report Issue...` command (follow the link for instructions)'
- name: 'Pylance'
url: https://github.com/microsoft/pylance-release/issues
about: "For issues relating to the Pylance language server extension"
- name: "Jupyter"
about: 'For issues relating to the Pylance language server extension'
- name: 'Jupyter'
url: https://github.com/microsoft/vscode-jupyter/issues
about: "For issues relating to the Jupyter extension (including the interactive window)"
- name: "Debugpy"
about: 'For issues relating to the Jupyter extension (including the interactive window)'
- name: 'Debugpy'
url: https://github.com/microsoft/debugpy/issues
about: "For issues relating to the debugpy debugger"
about: 'For issues relating to the debugpy debugger'
- name: Help/Support
url: https://github.com/microsoft/vscode-python/discussions/categories/q-a
about: "Having trouble with the extension? Need help getting something to work?"
- name: "Chat"
about: 'Having trouble with the extension? Need help getting something to work?'
- name: 'Chat'
url: https://aka.ms/python-discord
about: "You can ask for help or chat in the `#vscode` channel of our microsoft-python Discord server"
about: 'You can ask for help or chat in the `#vscode` channel of our microsoft-python Discord server'
1 change: 1 addition & 0 deletions news/1 Enhancements/17905.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Public API for environments (proposed).
85 changes: 1 addition & 84 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,98 +4,15 @@
'use strict';

import { noop } from 'lodash';
import { Event, Uri } from 'vscode';
import { IExtensionApi } from './apiTypes';
import { isTestExecution } from './common/constants';
import { IConfigurationService, Resource } from './common/types';
import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers';
import { IInterpreterService } from './interpreter/contracts';
import { IServiceContainer, IServiceManager } from './ioc/types';
import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration';
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
import { traceError } from './logging';

/*
* Do not introduce any breaking changes to this API.
* This is the public API for other extensions to interact with this extension.
*/

export interface IExtensionApi {
/**
* Promise indicating whether all parts of the extension have completed loading or not.
* @type {Promise<void>}
* @memberof IExtensionApi
*/
ready: Promise<void>;
jupyter: {
registerHooks(): void;
};
debug: {
/**
* Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging.
* Users can append another array of strings of what they want to execute along with relevant arguments to Python.
* E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']`
* @param {string} host
* @param {number} port
* @param {boolean} [waitUntilDebuggerAttaches=true]
* @returns {Promise<string[]>}
*/
getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise<string[]>;

/**
* Gets the path to the debugger package used by the extension.
* @returns {Promise<string>}
*/
getDebuggerPackagePath(): Promise<string | undefined>;
};
/**
* Return internal settings within the extension which are stored in VSCode storage
*/
settings: {
/**
* An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes.
*/
readonly onDidChangeExecutionDetails: Event<Uri | undefined>;
/**
* Returns all the details the consumer needs to execute code within the selected environment,
* corresponding to the specified resource taking into account any workspace-specific settings
* for the workspace to which this resource belongs.
* @param {Resource} [resource] A resource for which the setting is asked for.
* * When no resource is provided, the setting scoped to the first workspace folder is returned.
* * If no folder is present, it returns the global setting.
* @returns {({ execCommand: string[] | undefined })}
*/
getExecutionDetails(
resource?: Resource,
): {
/**
* E.g of execution commands returned could be,
* * `['<path to the interpreter set in settings>']`
* * `['<path to the interpreter selected by the extension when setting is not set>']`
* * `['conda', 'run', 'python']` which is used to run from within Conda environments.
* or something similar for some other Python environments.
*
* @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set.
* Otherwise, join the items returned using space to construct the full execution command.
*/
execCommand: string[] | undefined;
};
};

datascience: {
/**
* Launches Data Viewer component.
* @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data.
* @param {string} title Data Viewer title
*/
showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void>;
/**
* Registers a remote server provider component that's used to pick remote jupyter server URIs
* @param serverProvider object called back when picking jupyter server URI
*/
registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void;
};
}

export function buildApi(
ready: Promise<any>,
serviceManager: IServiceManager,
Expand Down
171 changes: 171 additions & 0 deletions src/client/apiTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Event, Uri } from 'vscode';
import { Resource } from './common/types';
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';

/*
* Do not introduce any breaking changes to this API.
* This is the public API for other extensions to interact with this extension.
*/

export interface IExtensionApi {
/**
* Promise indicating whether all parts of the extension have completed loading or not.
* @type {Promise<void>}
* @memberof IExtensionApi
*/
ready: Promise<void>;
jupyter: {
registerHooks(): void;
};
debug: {
/**
* Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging.
* Users can append another array of strings of what they want to execute along with relevant arguments to Python.
* E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']`
* @param {string} host
* @param {number} port
* @param {boolean} [waitUntilDebuggerAttaches=true]
* @returns {Promise<string[]>}
*/
getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise<string[]>;

/**
* Gets the path to the debugger package used by the extension.
* @returns {Promise<string>}
*/
getDebuggerPackagePath(): Promise<string | undefined>;
};
/**
* Return internal settings within the extension which are stored in VSCode storage
*/
settings: {
/**
* An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes.
*/
readonly onDidChangeExecutionDetails: Event<Uri | undefined>;
/**
* Returns all the details the consumer needs to execute code within the selected environment,
* corresponding to the specified resource taking into account any workspace-specific settings
* for the workspace to which this resource belongs.
* @param {Resource} [resource] A resource for which the setting is asked for.
* * When no resource is provided, the setting scoped to the first workspace folder is returned.
* * If no folder is present, it returns the global setting.
* @returns {({ execCommand: string[] | undefined })}
*/
getExecutionDetails(
resource?: Resource,
): {
/**
* E.g of execution commands returned could be,
* * `['<path to the interpreter set in settings>']`
* * `['<path to the interpreter selected by the extension when setting is not set>']`
* * `['conda', 'run', 'python']` which is used to run from within Conda environments.
* or something similar for some other Python environments.
*
* @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set.
* Otherwise, join the items returned using space to construct the full execution command.
*/
execCommand: string[] | undefined;
};
};

datascience: {
/**
* Launches Data Viewer component.
* @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data.
* @param {string} title Data Viewer title
*/
showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void>;
/**
* Registers a remote server provider component that's used to pick remote jupyter server URIs
* @param serverProvider object called back when picking jupyter server URI
*/
registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void;
};
}

export interface InterpreterDetailsOptions {
useCache: boolean;
}

export interface InterpreterDetails {
path: string;
version: string[];
environmentType: string[];
metadata: Record<string, unknown>;
}

export interface InterpretersChangedParams {
path?: string;
type: 'add' | 'remove' | 'update' | 'clear-all';
}

export interface ActiveInterpreterChangedParams {
interpreterPath?: string;
resource?: Uri;
}

export interface IProposedExtensionAPI {
environment: {
/**
* Returns the path to the python binary selected by the user or as in the settings.
* This is just the path to the python binary, this does not provide activation or any
* other activation command. The `resource` if provided will be used to determine the
* python binary in a multi-root scenario. If resource is `undefined` then the API
* returns what ever is set for the workspace.
* @param resource : Uri of a file or workspace
*/
getActiveInterpreterPath(resource?: Resource): Promise<string | undefined>;
/**
* Returns details for the given interpreter. Details such as absolute interpreter path,
* version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under
* metadata field.
* @param interpreterPath : Path of the interpreter whose details you need.
* @param options : [optional]
* * useCache : When true, cache is checked first for any data, returns even if there
* is partial data.
*/
getInterpreterDetails(
interpreterPath: string,
options?: InterpreterDetailsOptions,
): Promise<InterpreterDetails | undefined>;
/**
* Returns paths to interpreters found by the extension at the time of calling. This API
* will *not* trigger a refresh. If a refresh is going on it will *not* wait for the refresh
* to finish. This will return what is known so far. To get complete list `await` on promise
* returned by `getRefreshPromise()`.
*/
getInterpreterPaths(): Promise<string[] | undefined>;
/**
* Sets the active interpreter path for the python extension. Configuration target will
* always be the workspace.
* @param interpreterPath : Interpreter path to set for a given workspace.
* @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace
* folder.
*/
setActiveInterpreter(interpreterPath: string, resource?: Resource): Promise<void>;
/**
* This API will re-trigger environment discovery. Extensions can wait on the returned
* promise to get the updated interpreters list. If there is a refresh already going on
* then it returns the promise for that refresh.
*/
refreshInterpreters(): Promise<string[] | undefined>;
/**
* Returns a promise for the ongoing refresh. Returns `undefined` if there are no active
* refreshes going on.
*/
getRefreshPromise(): Promise<void> | undefined;
/**
* This event is triggered when the known interpreters list changes, like when a interpreter
* is found, existing interpreter is removed, or some details changed on an interpreter.
*/
onDidInterpretersChanged: Event<InterpretersChangedParams[]>;
/**
* This event is triggered when the active interpreter changes.
*/
onDidActiveInterpreterChanged: Event<ActiveInterpreterChangedParams>;
};
}
9 changes: 6 additions & 3 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ initializeFileLogging(logDispose);

import { ProgressLocation, ProgressOptions, window } from 'vscode';

import { buildApi, IExtensionApi } from './api';
import { buildApi } from './api';
import { IApplicationShell, IWorkspaceService } from './common/application/types';
import { IAsyncDisposableRegistry, IDisposableRegistry, IExperimentService, IExtensionContext } from './common/types';
import { createDeferred } from './common/utils/async';
Expand All @@ -42,6 +42,8 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry';
import { IStartupDurations } from './types';
import { runAfterActivation } from './common/utils/runAfterActivation';
import { IInterpreterService } from './interpreter/contracts';
import { IExtensionApi, IProposedExtensionAPI } from './apiTypes';
import { buildProposedApi } from './proposedApi';
import { WorkspaceService } from './common/application/workspace';

durations.codeLoadingTime = stopWatch.elapsedTime;
Expand Down Expand Up @@ -106,7 +108,7 @@ async function activateUnsafe(
context: IExtensionContext,
startupStopWatch: StopWatch,
startupDurations: IStartupDurations,
): Promise<[IExtensionApi, Promise<void>, IServiceContainer]> {
): Promise<[IExtensionApi & IProposedExtensionAPI, Promise<void>, IServiceContainer]> {
// Add anything that we got from initializing logs to dispose.
context.subscriptions.push(...logDispose);

Expand Down Expand Up @@ -158,7 +160,8 @@ async function activateUnsafe(
});

const api = buildApi(activationPromise, ext.legacyIOC.serviceManager, ext.legacyIOC.serviceContainer);
return [api, activationPromise, ext.legacyIOC.serviceContainer];
const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer);
return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer];
}

function displayProgress(promise: Promise<any>) {
Expand Down
5 changes: 5 additions & 0 deletions src/client/interpreter/interpreterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { PythonLocatorQuery } from '../pythonEnvironments/base/locator';
import { traceError } from '../logging';
import { PYTHON_LANGUAGE } from '../common/constants';
import { InterpreterStatusBarPosition } from '../common/experiments/groups';
import { reportActiveInterpreterChanged } from '../proposedApi';

type StoredPythonEnvironment = PythonEnvironment & { store?: boolean };

Expand Down Expand Up @@ -176,6 +177,10 @@ export class InterpreterService implements Disposable, IInterpreterService {
if (this._pythonPathSetting === '' || this._pythonPathSetting !== pySettings.pythonPath) {
this._pythonPathSetting = pySettings.pythonPath;
this.didChangeInterpreterEmitter.fire();
reportActiveInterpreterChanged({
interpreterPath: pySettings.pythonPath === '' ? undefined : pySettings.pythonPath,
resource,
});
const interpreterDisplay = this.serviceContainer.get<IInterpreterDisplay>(IInterpreterDisplay);
interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex));
}
Expand Down
Loading