Skip to content

Commit 262019b

Browse files
authored
Proposed API for discovered python environments (#18314)
* Proposed API initial commit * Add API description. * Adding tests initial * Added some tests * add getInterpreterDetails tests * Added tests for getInterpreterPaths * added tests for reportActiveInterpreterChanged * Tests for setActiveInterpreter * Add tests for reportInterpretersChanged * Tests for refreshInterpreters and getRefreshPromise * Add news item. * Update documentation and support for 'clear-all' event. * Fix tests and linting. * Fix logger tests.
1 parent e1773fc commit 262019b

File tree

16 files changed

+984
-133
lines changed

16 files changed

+984
-133
lines changed

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
blank_issues_enabled: false
22
contact_links:
3-
- name: "Bug 🐜"
3+
- name: 'Bug 🐜'
44
url: https://aka.ms/pvsc-bug
5-
about: "Use the `Python: Report Issue...` command (follow the link for instructions)"
6-
- name: "Pylance"
5+
about: 'Use the `Python: Report Issue...` command (follow the link for instructions)'
6+
- name: 'Pylance'
77
url: https://github.com/microsoft/pylance-release/issues
8-
about: "For issues relating to the Pylance language server extension"
9-
- name: "Jupyter"
8+
about: 'For issues relating to the Pylance language server extension'
9+
- name: 'Jupyter'
1010
url: https://github.com/microsoft/vscode-jupyter/issues
11-
about: "For issues relating to the Jupyter extension (including the interactive window)"
12-
- name: "Debugpy"
11+
about: 'For issues relating to the Jupyter extension (including the interactive window)'
12+
- name: 'Debugpy'
1313
url: https://github.com/microsoft/debugpy/issues
14-
about: "For issues relating to the debugpy debugger"
14+
about: 'For issues relating to the debugpy debugger'
1515
- name: Help/Support
1616
url: https://github.com/microsoft/vscode-python/discussions/categories/q-a
17-
about: "Having trouble with the extension? Need help getting something to work?"
18-
- name: "Chat"
17+
about: 'Having trouble with the extension? Need help getting something to work?'
18+
- name: 'Chat'
1919
url: https://aka.ms/python-discord
20-
about: "You can ask for help or chat in the `#vscode` channel of our microsoft-python Discord server"
20+
about: 'You can ask for help or chat in the `#vscode` channel of our microsoft-python Discord server'

news/1 Enhancements/17905.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Public API for environments (proposed).

src/client/api.ts

Lines changed: 1 addition & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,98 +4,15 @@
44
'use strict';
55

66
import { noop } from 'lodash';
7-
import { Event, Uri } from 'vscode';
7+
import { IExtensionApi } from './apiTypes';
88
import { isTestExecution } from './common/constants';
99
import { IConfigurationService, Resource } from './common/types';
1010
import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers';
1111
import { IInterpreterService } from './interpreter/contracts';
1212
import { IServiceContainer, IServiceManager } from './ioc/types';
1313
import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration';
14-
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
1514
import { traceError } from './logging';
1615

17-
/*
18-
* Do not introduce any breaking changes to this API.
19-
* This is the public API for other extensions to interact with this extension.
20-
*/
21-
22-
export interface IExtensionApi {
23-
/**
24-
* Promise indicating whether all parts of the extension have completed loading or not.
25-
* @type {Promise<void>}
26-
* @memberof IExtensionApi
27-
*/
28-
ready: Promise<void>;
29-
jupyter: {
30-
registerHooks(): void;
31-
};
32-
debug: {
33-
/**
34-
* Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging.
35-
* Users can append another array of strings of what they want to execute along with relevant arguments to Python.
36-
* E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']`
37-
* @param {string} host
38-
* @param {number} port
39-
* @param {boolean} [waitUntilDebuggerAttaches=true]
40-
* @returns {Promise<string[]>}
41-
*/
42-
getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise<string[]>;
43-
44-
/**
45-
* Gets the path to the debugger package used by the extension.
46-
* @returns {Promise<string>}
47-
*/
48-
getDebuggerPackagePath(): Promise<string | undefined>;
49-
};
50-
/**
51-
* Return internal settings within the extension which are stored in VSCode storage
52-
*/
53-
settings: {
54-
/**
55-
* An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes.
56-
*/
57-
readonly onDidChangeExecutionDetails: Event<Uri | undefined>;
58-
/**
59-
* Returns all the details the consumer needs to execute code within the selected environment,
60-
* corresponding to the specified resource taking into account any workspace-specific settings
61-
* for the workspace to which this resource belongs.
62-
* @param {Resource} [resource] A resource for which the setting is asked for.
63-
* * When no resource is provided, the setting scoped to the first workspace folder is returned.
64-
* * If no folder is present, it returns the global setting.
65-
* @returns {({ execCommand: string[] | undefined })}
66-
*/
67-
getExecutionDetails(
68-
resource?: Resource,
69-
): {
70-
/**
71-
* E.g of execution commands returned could be,
72-
* * `['<path to the interpreter set in settings>']`
73-
* * `['<path to the interpreter selected by the extension when setting is not set>']`
74-
* * `['conda', 'run', 'python']` which is used to run from within Conda environments.
75-
* or something similar for some other Python environments.
76-
*
77-
* @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set.
78-
* Otherwise, join the items returned using space to construct the full execution command.
79-
*/
80-
execCommand: string[] | undefined;
81-
};
82-
};
83-
84-
datascience: {
85-
/**
86-
* Launches Data Viewer component.
87-
* @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data.
88-
* @param {string} title Data Viewer title
89-
*/
90-
showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void>;
91-
/**
92-
* Registers a remote server provider component that's used to pick remote jupyter server URIs
93-
* @param serverProvider object called back when picking jupyter server URI
94-
*/
95-
registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void;
96-
};
97-
}
98-
9916
export function buildApi(
10017
ready: Promise<any>,
10118
serviceManager: IServiceManager,

src/client/apiTypes.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Event, Uri } from 'vscode';
5+
import { Resource } from './common/types';
6+
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
7+
8+
/*
9+
* Do not introduce any breaking changes to this API.
10+
* This is the public API for other extensions to interact with this extension.
11+
*/
12+
13+
export interface IExtensionApi {
14+
/**
15+
* Promise indicating whether all parts of the extension have completed loading or not.
16+
* @type {Promise<void>}
17+
* @memberof IExtensionApi
18+
*/
19+
ready: Promise<void>;
20+
jupyter: {
21+
registerHooks(): void;
22+
};
23+
debug: {
24+
/**
25+
* Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging.
26+
* Users can append another array of strings of what they want to execute along with relevant arguments to Python.
27+
* E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']`
28+
* @param {string} host
29+
* @param {number} port
30+
* @param {boolean} [waitUntilDebuggerAttaches=true]
31+
* @returns {Promise<string[]>}
32+
*/
33+
getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise<string[]>;
34+
35+
/**
36+
* Gets the path to the debugger package used by the extension.
37+
* @returns {Promise<string>}
38+
*/
39+
getDebuggerPackagePath(): Promise<string | undefined>;
40+
};
41+
/**
42+
* Return internal settings within the extension which are stored in VSCode storage
43+
*/
44+
settings: {
45+
/**
46+
* An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes.
47+
*/
48+
readonly onDidChangeExecutionDetails: Event<Uri | undefined>;
49+
/**
50+
* Returns all the details the consumer needs to execute code within the selected environment,
51+
* corresponding to the specified resource taking into account any workspace-specific settings
52+
* for the workspace to which this resource belongs.
53+
* @param {Resource} [resource] A resource for which the setting is asked for.
54+
* * When no resource is provided, the setting scoped to the first workspace folder is returned.
55+
* * If no folder is present, it returns the global setting.
56+
* @returns {({ execCommand: string[] | undefined })}
57+
*/
58+
getExecutionDetails(
59+
resource?: Resource,
60+
): {
61+
/**
62+
* E.g of execution commands returned could be,
63+
* * `['<path to the interpreter set in settings>']`
64+
* * `['<path to the interpreter selected by the extension when setting is not set>']`
65+
* * `['conda', 'run', 'python']` which is used to run from within Conda environments.
66+
* or something similar for some other Python environments.
67+
*
68+
* @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set.
69+
* Otherwise, join the items returned using space to construct the full execution command.
70+
*/
71+
execCommand: string[] | undefined;
72+
};
73+
};
74+
75+
datascience: {
76+
/**
77+
* Launches Data Viewer component.
78+
* @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data.
79+
* @param {string} title Data Viewer title
80+
*/
81+
showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void>;
82+
/**
83+
* Registers a remote server provider component that's used to pick remote jupyter server URIs
84+
* @param serverProvider object called back when picking jupyter server URI
85+
*/
86+
registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void;
87+
};
88+
}
89+
90+
export interface InterpreterDetailsOptions {
91+
useCache: boolean;
92+
}
93+
94+
export interface InterpreterDetails {
95+
path: string;
96+
version: string[];
97+
environmentType: string[];
98+
metadata: Record<string, unknown>;
99+
}
100+
101+
export interface InterpretersChangedParams {
102+
path?: string;
103+
type: 'add' | 'remove' | 'update' | 'clear-all';
104+
}
105+
106+
export interface ActiveInterpreterChangedParams {
107+
interpreterPath?: string;
108+
resource?: Uri;
109+
}
110+
111+
export interface IProposedExtensionAPI {
112+
environment: {
113+
/**
114+
* Returns the path to the python binary selected by the user or as in the settings.
115+
* This is just the path to the python binary, this does not provide activation or any
116+
* other activation command. The `resource` if provided will be used to determine the
117+
* python binary in a multi-root scenario. If resource is `undefined` then the API
118+
* returns what ever is set for the workspace.
119+
* @param resource : Uri of a file or workspace
120+
*/
121+
getActiveInterpreterPath(resource?: Resource): Promise<string | undefined>;
122+
/**
123+
* Returns details for the given interpreter. Details such as absolute interpreter path,
124+
* version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under
125+
* metadata field.
126+
* @param interpreterPath : Path of the interpreter whose details you need.
127+
* @param options : [optional]
128+
* * useCache : When true, cache is checked first for any data, returns even if there
129+
* is partial data.
130+
*/
131+
getInterpreterDetails(
132+
interpreterPath: string,
133+
options?: InterpreterDetailsOptions,
134+
): Promise<InterpreterDetails | undefined>;
135+
/**
136+
* Returns paths to interpreters found by the extension at the time of calling. This API
137+
* will *not* trigger a refresh. If a refresh is going on it will *not* wait for the refresh
138+
* to finish. This will return what is known so far. To get complete list `await` on promise
139+
* returned by `getRefreshPromise()`.
140+
*/
141+
getInterpreterPaths(): Promise<string[] | undefined>;
142+
/**
143+
* Sets the active interpreter path for the python extension. Configuration target will
144+
* always be the workspace.
145+
* @param interpreterPath : Interpreter path to set for a given workspace.
146+
* @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace
147+
* folder.
148+
*/
149+
setActiveInterpreter(interpreterPath: string, resource?: Resource): Promise<void>;
150+
/**
151+
* This API will re-trigger environment discovery. Extensions can wait on the returned
152+
* promise to get the updated interpreters list. If there is a refresh already going on
153+
* then it returns the promise for that refresh.
154+
*/
155+
refreshInterpreters(): Promise<string[] | undefined>;
156+
/**
157+
* Returns a promise for the ongoing refresh. Returns `undefined` if there are no active
158+
* refreshes going on.
159+
*/
160+
getRefreshPromise(): Promise<void> | undefined;
161+
/**
162+
* This event is triggered when the known interpreters list changes, like when a interpreter
163+
* is found, existing interpreter is removed, or some details changed on an interpreter.
164+
*/
165+
onDidInterpretersChanged: Event<InterpretersChangedParams[]>;
166+
/**
167+
* This event is triggered when the active interpreter changes.
168+
*/
169+
onDidActiveInterpreterChanged: Event<ActiveInterpreterChangedParams>;
170+
};
171+
}

src/client/extension.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ initializeFileLogging(logDispose);
3030

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

33-
import { buildApi, IExtensionApi } from './api';
33+
import { buildApi } from './api';
3434
import { IApplicationShell, IWorkspaceService } from './common/application/types';
3535
import { IAsyncDisposableRegistry, IDisposableRegistry, IExperimentService, IExtensionContext } from './common/types';
3636
import { createDeferred } from './common/utils/async';
@@ -42,6 +42,8 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry';
4242
import { IStartupDurations } from './types';
4343
import { runAfterActivation } from './common/utils/runAfterActivation';
4444
import { IInterpreterService } from './interpreter/contracts';
45+
import { IExtensionApi, IProposedExtensionAPI } from './apiTypes';
46+
import { buildProposedApi } from './proposedApi';
4547
import { WorkspaceService } from './common/application/workspace';
4648

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

@@ -158,7 +160,8 @@ async function activateUnsafe(
158160
});
159161

160162
const api = buildApi(activationPromise, ext.legacyIOC.serviceManager, ext.legacyIOC.serviceContainer);
161-
return [api, activationPromise, ext.legacyIOC.serviceContainer];
163+
const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer);
164+
return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer];
162165
}
163166

164167
function displayProgress(promise: Promise<any>) {

src/client/interpreter/interpreterService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { PythonLocatorQuery } from '../pythonEnvironments/base/locator';
2323
import { traceError } from '../logging';
2424
import { PYTHON_LANGUAGE } from '../common/constants';
2525
import { InterpreterStatusBarPosition } from '../common/experiments/groups';
26+
import { reportActiveInterpreterChanged } from '../proposedApi';
2627

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

@@ -176,6 +177,10 @@ export class InterpreterService implements Disposable, IInterpreterService {
176177
if (this._pythonPathSetting === '' || this._pythonPathSetting !== pySettings.pythonPath) {
177178
this._pythonPathSetting = pySettings.pythonPath;
178179
this.didChangeInterpreterEmitter.fire();
180+
reportActiveInterpreterChanged({
181+
interpreterPath: pySettings.pythonPath === '' ? undefined : pySettings.pythonPath,
182+
resource,
183+
});
179184
const interpreterDisplay = this.serviceContainer.get<IInterpreterDisplay>(IInterpreterDisplay);
180185
interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex));
181186
}

0 commit comments

Comments
 (0)