Skip to content

Commit 8f8f624

Browse files
author
Kartik Raj
authored
Direct users to the Jupyter extension when using Run in Interactive window (#21072)
Closes #20576
1 parent 48952a3 commit 8f8f624

13 files changed

+163
-3
lines changed

package.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,11 @@
374374
"light": "resources/light/repl.svg"
375375
},
376376
"title": "%python.command.python.viewOutput.title%"
377+
},
378+
{
379+
"category": "Python",
380+
"command": "python.installJupyter",
381+
"title": "%python.command.python.installJupyter.title%"
377382
}
378383
],
379384
"configuration": {
@@ -1705,13 +1710,25 @@
17051710
{
17061711
"submenu": "python.run",
17071712
"group": "Python",
1708-
"when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported"
1713+
"when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted"
17091714
},
17101715
{
17111716
"command": "python.sortImports",
17121717
"group": "Refactor",
17131718
"title": "%python.command.python.sortImports.title%",
17141719
"when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported"
1720+
},
1721+
{
1722+
"submenu": "python.runFileInteractive",
1723+
"group": "Jupyter2",
1724+
"when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && !isJupyterInstalled && isWorkspaceTrusted"
1725+
}
1726+
],
1727+
"python.runFileInteractive": [
1728+
{
1729+
"command": "python.installJupyter",
1730+
"group": "Jupyter2",
1731+
"when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported"
17151732
}
17161733
],
17171734
"python.run": [
@@ -1779,6 +1796,10 @@
17791796
"id": "python.run",
17801797
"label": "%python.editor.context.submenu.runPython%",
17811798
"icon": "$(play)"
1799+
},
1800+
{
1801+
"id": "python.runFileInteractive",
1802+
"label": "%python.editor.context.submenu.runPythonInteractive%"
17821803
}
17831804
],
17841805
"viewsWelcome": [

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"python.command.python.setInterpreter.title": "Select Interpreter",
1111
"python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting",
1212
"python.command.python.viewOutput.title": "Show Output",
13+
"python.command.python.installJupyter.title": "Install the Jupyter extension",
1314
"python.command.python.viewLanguageServerOutput.title": "Show Language Server Output",
1415
"python.command.python.configureTests.title": "Configure Tests",
1516
"python.command.testing.rerunFailedTests.title": "Rerun Failed Tests",
@@ -26,6 +27,7 @@
2627
"python.command.python.refreshTensorBoard.title": "Refresh TensorBoard",
2728
"python.menu.createNewFile.title": "Python File",
2829
"python.editor.context.submenu.runPython": "Run Python",
30+
"python.editor.context.submenu.runPythonInteractive": "Run in Interactive window",
2931
"python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).",
3032
"python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.",
3133
"python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).",

src/client/common/application/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping;
1717
*/
1818
interface ICommandNameWithoutArgumentTypeMapping {
1919
[Commands.InstallPythonOnMac]: [];
20+
[Commands.InstallJupyter]: [];
2021
[Commands.InstallPythonOnLinux]: [];
2122
[Commands.InstallPython]: [];
2223
[Commands.ClearWorkspaceInterpreter]: [];

src/client/common/application/contextKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export enum ExtensionContextKey {
55
showInstallPythonTile = 'showInstallPythonTile',
66
HasFailedTests = 'hasFailedTests',
77
RefreshingTests = 'refreshingTests',
8+
IsJupyterInstalled = 'isJupyterInstalled',
89
}

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export namespace Commands {
4646
export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell';
4747
export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal';
4848
export const GetSelectedInterpreterPath = 'python.interpreterPath';
49+
export const InstallJupyter = 'python.installJupyter';
4950
export const InstallPython = 'python.installPython';
5051
export const InstallPythonOnLinux = 'python.installPythonOnLinux';
5152
export const InstallPythonOnMac = 'python.installPythonOnMac';

src/client/common/serviceRegistry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStep
9090
import { Random } from './utils/random';
9191
import { ContextKeyManager } from './application/contextKeyManager';
9292
import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile';
93+
import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt';
9394

9495
export function registerTypes(serviceManager: IServiceManager): void {
9596
serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS);
@@ -110,6 +111,10 @@ export function registerTypes(serviceManager: IServiceManager): void {
110111
IJupyterExtensionDependencyManager,
111112
JupyterExtensionDependencyManager,
112113
);
114+
serviceManager.addSingleton<IExtensionSingleActivationService>(
115+
IExtensionSingleActivationService,
116+
RequireJupyterPrompt,
117+
);
113118
serviceManager.addSingleton<IExtensionSingleActivationService>(
114119
IExtensionSingleActivationService,
115120
CreatePythonFileCommandHandler,

src/client/common/utils/localize.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ export namespace LanguageService {
188188
);
189189
}
190190
export namespace Interpreters {
191+
export const requireJupyter = l10n.t(
192+
'Running in Interactive window requires Jupyter Extension. Would you like to install it? [Learn more](https://aka.ms/pythonJupyterSupport).',
193+
);
191194
export const installingPython = l10n.t('Installing Python into Environment...');
192195
export const discovering = l10n.t('Discovering Python Interpreters');
193196
export const refreshing = l10n.t('Refreshing Python Interpreters');

src/client/jupyter/jupyterIntegration.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { inject, injectable, named } from 'inversify';
88
import { dirname } from 'path';
99
import { CancellationToken, Event, Extension, Memento, Uri } from 'vscode';
1010
import type { SemVer } from 'semver';
11-
import { IWorkspaceService } from '../common/application/types';
11+
import { IContextKeyManager, IWorkspaceService } from '../common/application/types';
1212
import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants';
1313
import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types';
1414
import {
@@ -35,6 +35,7 @@ import {
3535
import { PythonEnvironment } from '../pythonEnvironments/info';
3636
import { IDataViewerDataProvider, IJupyterUriProvider } from './types';
3737
import { PylanceApi } from '../activation/node/pylanceApi';
38+
import { ExtensionContextKey } from '../common/application/contextKeys';
3839
/**
3940
* This allows Python extension to update Product enum without breaking Jupyter.
4041
* I.e. we have a strict contract, else using numbers (in enums) is bound to break across products.
@@ -201,9 +202,11 @@ export class JupyterExtensionIntegration {
201202
@inject(IComponentAdapter) private pyenvs: IComponentAdapter,
202203
@inject(IWorkspaceService) private workspaceService: IWorkspaceService,
203204
@inject(ICondaService) private readonly condaService: ICondaService,
205+
@inject(IContextKeyManager) private readonly contextManager: IContextKeyManager,
204206
) {}
205207

206208
public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined {
209+
this.contextManager.setContext(ExtensionContextKey.IsJupyterInstalled, true);
207210
if (!this.workspaceService.isTrusted) {
208211
this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(jupyterExtensionApi));
209212
return undefined;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { IExtensionSingleActivationService } from '../activation/types';
6+
import { IApplicationShell, ICommandManager } from '../common/application/types';
7+
import { Common, Interpreters } from '../common/utils/localize';
8+
import { Commands, JUPYTER_EXTENSION_ID } from '../common/constants';
9+
import { IDisposable, IDisposableRegistry } from '../common/types';
10+
import { sendTelemetryEvent } from '../telemetry';
11+
import { EventName } from '../telemetry/constants';
12+
13+
@injectable()
14+
export class RequireJupyterPrompt implements IExtensionSingleActivationService {
15+
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true };
16+
17+
constructor(
18+
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
19+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
20+
@inject(IDisposableRegistry) private readonly disposables: IDisposable[],
21+
) {}
22+
23+
public async activate(): Promise<void> {
24+
this.disposables.push(this.commandManager.registerCommand(Commands.InstallJupyter, () => this._showPrompt()));
25+
}
26+
27+
public async _showPrompt(): Promise<void> {
28+
const prompts = [Common.bannerLabelYes, Common.bannerLabelNo];
29+
const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No'];
30+
const selection = await this.appShell.showInformationMessage(Interpreters.requireJupyter, ...prompts);
31+
sendTelemetryEvent(EventName.REQUIRE_JUPYTER_PROMPT, undefined, {
32+
selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined,
33+
});
34+
if (!selection) {
35+
return;
36+
}
37+
if (selection === prompts[0]) {
38+
await this.commandManager.executeCommand(
39+
'workbench.extensions.installExtension',
40+
JUPYTER_EXTENSION_ID,
41+
undefined,
42+
);
43+
}
44+
}
45+
}

src/client/telemetry/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export enum EventName {
3030
PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT',
3131
PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT',
3232
CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT',
33+
REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT',
3334
ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH',
3435
ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION',
3536
ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE',

src/client/telemetry/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,22 @@ export interface IEventNamePropertyMapping {
13151315
*/
13161316
selection: 'Allow' | 'Close' | undefined;
13171317
};
1318+
/**
1319+
* Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed.
1320+
*/
1321+
/* __GDPR__
1322+
"conda_inherit_env_prompt" : {
1323+
"selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" }
1324+
}
1325+
*/
1326+
[EventName.REQUIRE_JUPYTER_PROMPT]: {
1327+
/**
1328+
* `Yes` When 'Yes' option is selected
1329+
* `No` When 'No' option is selected
1330+
* `undefined` When 'x' is selected
1331+
*/
1332+
selection: 'Yes' | 'No' | undefined;
1333+
};
13181334
/**
13191335
* Telemetry event sent with details when user clicks the prompt with the following message:
13201336
*

src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from '../../../client/interpreter/contracts';
2020
import { IInterpreterSelector } from '../../../client/interpreter/configuration/types';
2121
import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types';
22-
import { IWorkspaceService } from '../../../client/common/application/types';
22+
import { IContextKeyManager, IWorkspaceService } from '../../../client/common/application/types';
2323
import { MockMemento } from '../../mocks/mementos';
2424

2525
suite('Pylance Language Server - Interactive Window LSP Notebooks', () => {
@@ -41,6 +41,7 @@ suite('Pylance Language Server - Interactive Window LSP Notebooks', () => {
4141
mock<IComponentAdapter>(),
4242
mock<IWorkspaceService>(),
4343
mock<ICondaService>(),
44+
mock<IContextKeyManager>(),
4445
);
4546
jupyterApi.registerGetNotebookUriForTextDocumentUriFunction(getNotebookUriFunction);
4647
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { mock, instance, verify, anything, when } from 'ts-mockito';
5+
import { IApplicationShell, ICommandManager } from '../../client/common/application/types';
6+
import { Commands, JUPYTER_EXTENSION_ID } from '../../client/common/constants';
7+
import { IDisposableRegistry } from '../../client/common/types';
8+
import { Common, Interpreters } from '../../client/common/utils/localize';
9+
import { RequireJupyterPrompt } from '../../client/jupyter/requireJupyterPrompt';
10+
11+
suite('RequireJupyterPrompt Unit Tests', () => {
12+
let requireJupyterPrompt: RequireJupyterPrompt;
13+
let appShell: IApplicationShell;
14+
let commandManager: ICommandManager;
15+
let disposables: IDisposableRegistry;
16+
17+
setup(() => {
18+
appShell = mock<IApplicationShell>();
19+
commandManager = mock<ICommandManager>();
20+
disposables = mock<IDisposableRegistry>();
21+
22+
requireJupyterPrompt = new RequireJupyterPrompt(
23+
instance(appShell),
24+
instance(commandManager),
25+
instance(disposables),
26+
);
27+
});
28+
29+
test('Activation registers command', async () => {
30+
await requireJupyterPrompt.activate();
31+
32+
verify(commandManager.registerCommand(Commands.InstallJupyter, anything())).once();
33+
});
34+
35+
test('Show prompt with Yes selection installs Jupyter extension', async () => {
36+
when(
37+
appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo),
38+
).thenReturn(Promise.resolve(Common.bannerLabelYes));
39+
40+
await requireJupyterPrompt.activate();
41+
await requireJupyterPrompt._showPrompt();
42+
43+
verify(
44+
commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined),
45+
).once();
46+
});
47+
48+
test('Show prompt with No selection does not install Jupyter extension', async () => {
49+
when(
50+
appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo),
51+
).thenReturn(Promise.resolve(Common.bannerLabelNo));
52+
53+
await requireJupyterPrompt.activate();
54+
await requireJupyterPrompt._showPrompt();
55+
56+
verify(
57+
commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined),
58+
).never();
59+
});
60+
});

0 commit comments

Comments
 (0)