Skip to content

Commit c6c1e8b

Browse files
committed
Jupyter API to get Env associated with Notebooks
1 parent 9b182da commit c6c1e8b

File tree

6 files changed

+121
-10
lines changed

6 files changed

+121
-10
lines changed

pythonExtensionApi/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,9 @@ export type EnvironmentsChangeEvent = {
227227

228228
export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & {
229229
/**
230-
* Workspace folder the environment changed for.
230+
* Resource the environment changed for.
231231
*/
232-
readonly resource: WorkspaceFolder | undefined;
232+
readonly resource: Resource | undefined;
233233
};
234234

235235
/**

src/client/api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IConfigurationService, Resource } from './common/types';
1515
import { getDebugpyLauncherArgs } from './debugger/extension/adapter/remoteLaunchers';
1616
import { IInterpreterService } from './interpreter/contracts';
1717
import { IServiceContainer, IServiceManager } from './ioc/types';
18-
import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration';
18+
import { JupyterExtensionIntegration, JupyterExtensionPythonEnvironments, type JupyterPythonEnvironmentApi } from './jupyter/jupyterIntegration';
1919
import { traceError } from './logging';
2020
import { IDiscoveryAPI } from './pythonEnvironments/base/locator';
2121
import { buildEnvironmentApi } from './environmentApi';
@@ -33,11 +33,13 @@ export function buildApi(
3333
const configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService);
3434
const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService);
3535
serviceManager.addSingleton<JupyterExtensionIntegration>(JupyterExtensionIntegration, JupyterExtensionIntegration);
36+
serviceManager.addSingleton<JupyterExtensionPythonEnvironments>(JupyterExtensionPythonEnvironments, JupyterExtensionPythonEnvironments);
3637
serviceManager.addSingleton<TensorboardExtensionIntegration>(
3738
TensorboardExtensionIntegration,
3839
TensorboardExtensionIntegration,
3940
);
4041
const jupyterIntegration = serviceContainer.get<JupyterExtensionIntegration>(JupyterExtensionIntegration);
42+
const jupyterPythonEnvApi = serviceContainer.get<JupyterPythonEnvironmentApi>(JupyterExtensionPythonEnvironments);
4143
const tensorboardIntegration = serviceContainer.get<TensorboardExtensionIntegration>(
4244
TensorboardExtensionIntegration,
4345
);
@@ -146,7 +148,7 @@ export function buildApi(
146148
stop: (client: BaseLanguageClient): Promise<void> => client.stop(),
147149
getTelemetryReporter: () => getTelemetryReporter(),
148150
},
149-
environments: buildEnvironmentApi(discoveryApi, serviceContainer),
151+
environments: buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi),
150152
};
151153

152154
// In test environment return the DI Container.

src/client/api/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,9 @@ export type EnvironmentsChangeEvent = {
227227

228228
export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & {
229229
/**
230-
* Workspace folder the environment changed for.
230+
* Resource the environment changed for.
231231
*/
232-
readonly resource: WorkspaceFolder | undefined;
232+
readonly resource: Resource | undefined;
233233
};
234234

235235
/**

src/client/environmentApi.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from './api/types';
3434
import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi';
3535
import { EnvironmentKnownCache } from './environmentKnownCache';
36+
import type { JupyterPythonEnvironmentApi } from './jupyter/jupyterIntegration';
3637

3738
type ActiveEnvironmentChangeEvent = {
3839
resource: WorkspaceFolder | undefined;
@@ -115,6 +116,7 @@ function filterUsingVSCodeContext(e: PythonEnvInfo) {
115116
export function buildEnvironmentApi(
116117
discoveryApi: IDiscoveryAPI,
117118
serviceContainer: IServiceContainer,
119+
jupyterPythonEnvsApi: JupyterPythonEnvironmentApi,
118120
): PythonExtension['environments'] {
119121
const interpreterPathService = serviceContainer.get<IInterpreterPathService>(IInterpreterPathService);
120122
const configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
@@ -206,6 +208,14 @@ export function buildEnvironmentApi(
206208
}),
207209
onEnvironmentsChanged,
208210
onEnvironmentVariablesChanged,
211+
jupyterPythonEnvsApi.onDidChangePythonEnvironment((e) => {
212+
const jupyterEnv = environmentApi.getActiveEnvironmentPath(e);
213+
onDidActiveInterpreterChangedEvent.fire({
214+
id: jupyterEnv.id,
215+
path: jupyterEnv.path,
216+
resource: e,
217+
});
218+
}, undefined),
209219
);
210220
if (!knownCache!) {
211221
knownCache = initKnownCache();
@@ -224,6 +234,14 @@ export function buildEnvironmentApi(
224234
getActiveEnvironmentPath(resource?: Resource) {
225235
sendApiTelemetry('getActiveEnvironmentPath');
226236
resource = resource && 'uri' in resource ? resource.uri : resource;
237+
const jupyterEnv = resource ? jupyterPythonEnvsApi.getPythonEnvironment(resource) : undefined;
238+
if (jupyterEnv) {
239+
traceVerbose('Python Environment returned from Jupyter', resource?.fsPath, jupyterEnv.id);
240+
return {
241+
id: jupyterEnv.id,
242+
path: jupyterEnv.path,
243+
};
244+
}
227245
const path = configService.getSettings(resource).pythonPath;
228246
const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path);
229247
return {

src/client/jupyter/jupyterIntegration.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { inject, injectable, named } from 'inversify';
88
import { dirname } from 'path';
9-
import { Extension, Memento, Uri } from 'vscode';
9+
import { EventEmitter, Extension, Memento, Uri, workspace, type Event } from 'vscode';
1010
import type { SemVer } from 'semver';
1111
import { IContextKeyManager, IWorkspaceService } from '../common/application/types';
1212
import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants';
@@ -23,6 +23,7 @@ import { PylanceApi } from '../activation/node/pylanceApi';
2323
import { ExtensionContextKey } from '../common/application/contextKeys';
2424
import { getDebugpyPath } from '../debugger/pythonDebugger';
2525
import type { Environment } from '../api/types';
26+
import { DisposableBase } from '../common/utils/resourceLifecycle';
2627

2728
type PythonApiForJupyterExtension = {
2829
/**
@@ -170,3 +171,83 @@ export class JupyterExtensionIntegration {
170171
}
171172
}
172173
}
174+
175+
176+
export interface JupyterPythonEnvironmentApi {
177+
/**
178+
* This event is triggered when the environment associated with a Jupyter Notebook or Interactive Window changes.
179+
* The Uri in the event is the Uri of the Notebook/IW.
180+
*/
181+
onDidChangePythonEnvironment: Event<Uri>;
182+
/**
183+
* Returns the EnvironmentPath to the Python environment associated with a Jupyter Notebook or Interactive Window.
184+
* If the Uri is not associated with a Jupyter Notebook or Interactive Window, then this method returns undefined.
185+
* @param uri
186+
*/
187+
getPythonEnvironment(
188+
uri: Uri,
189+
):
190+
| undefined
191+
| {
192+
/**
193+
* The ID of the environment.
194+
*/
195+
readonly id: string;
196+
/**
197+
* Path to environment folder or path to python executable that uniquely identifies an environment. Environments
198+
* lacking a python executable are identified by environment folder paths, whereas other envs can be identified
199+
* using python executable path.
200+
*/
201+
readonly path: string;
202+
};
203+
}
204+
205+
// eslint-disable-next-line max-classes-per-file
206+
207+
@injectable()
208+
export class JupyterExtensionPythonEnvironments extends DisposableBase implements JupyterPythonEnvironmentApi {
209+
private jupyterExtension?: JupyterPythonEnvironmentApi;
210+
211+
private readonly _onDidChangePythonEnvironment = this._register(new EventEmitter<Uri>());
212+
213+
public readonly onDidChangePythonEnvironment = this._onDidChangePythonEnvironment.event;
214+
215+
constructor(
216+
@inject(IExtensions) private readonly extensions: IExtensions,
217+
) {
218+
super();
219+
}
220+
221+
public getPythonEnvironment(uri: Uri): undefined |
222+
{
223+
/**
224+
* The ID of the environment.
225+
*/
226+
readonly id: string;
227+
/**
228+
* Path to environment folder or path to python executable that uniquely identifies an environment. Environments
229+
* lacking a python executable are identified by environment folder paths, whereas other envs can be identified
230+
* using python executable path.
231+
*/
232+
readonly path: string;
233+
} {
234+
return isJupyterResource(uri) ? this.getJupyterApi()?.getPythonEnvironment(uri) : undefined;
235+
}
236+
237+
private getJupyterApi(){
238+
if (!this.jupyterExtension) {
239+
const api = this.extensions.getExtension<JupyterPythonEnvironmentApi>(JUPYTER_EXTENSION_ID)?.exports;
240+
if (!api) {
241+
return undefined;
242+
}
243+
this.jupyterExtension = api;
244+
this._register(api.onDidChangePythonEnvironment(this._onDidChangePythonEnvironment.fire, this._onDidChangePythonEnvironment));
245+
}
246+
return this.jupyterExtension;
247+
}
248+
}
249+
250+
function isJupyterResource(resource: Uri): boolean {
251+
// Jupyter extension only deals with Notebooks and Interactive Windows.
252+
return resource.fsPath.endsWith('.ipynb') || workspace.notebookDocuments.some((item) => item.uri.toString() === resource.toString());
253+
}

src/test/environmentApi.unit.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
EnvironmentsChangeEvent,
3939
PythonExtension,
4040
} from '../client/api/types';
41+
import { JupyterPythonEnvironmentApi } from '../client/jupyter/jupyterIntegration';
4142

4243
suite('Python Environment API', () => {
4344
const workspacePath = 'path/to/workspace';
@@ -80,7 +81,6 @@ suite('Python Environment API', () => {
8081
onDidChangeRefreshState = new EventEmitter();
8182
onDidChangeEnvironments = new EventEmitter();
8283
onDidChangeEnvironmentVariables = new EventEmitter();
83-
8484
serviceContainer.setup((s) => s.get(IExtensions)).returns(() => extensions.object);
8585
serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object);
8686
serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object);
@@ -94,8 +94,13 @@ suite('Python Environment API', () => {
9494
discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event);
9595
discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event);
9696
discoverAPI.setup((d) => d.getEnvs()).returns(() => []);
97+
const onDidChangePythonEnvironment = new EventEmitter<Uri>();
98+
const jupyterApi: JupyterPythonEnvironmentApi = {
99+
onDidChangePythonEnvironment: onDidChangePythonEnvironment.event,
100+
getPythonEnvironment: (_uri: Uri) => undefined,
101+
};
97102

98-
environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object);
103+
environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi);
99104
});
100105

101106
teardown(() => {
@@ -323,7 +328,12 @@ suite('Python Environment API', () => {
323328
},
324329
];
325330
discoverAPI.setup((d) => d.getEnvs()).returns(() => envs);
326-
environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object);
331+
const onDidChangePythonEnvironment = new EventEmitter<Uri>();
332+
const jupyterApi: JupyterPythonEnvironmentApi = {
333+
onDidChangePythonEnvironment: onDidChangePythonEnvironment.event,
334+
getPythonEnvironment: (_uri: Uri) => undefined,
335+
};
336+
environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi);
327337
const actual = environmentApi.known;
328338
const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal);
329339
assert.deepEqual(

0 commit comments

Comments
 (0)