Skip to content

Commit 5612a6a

Browse files
Kartik Rajwesm
Kartik Raj
authored andcommitted
Ensure Install Python button on the walkthrough opens and fills in the suggested command (microsoft/vscode-python#19487)
1 parent 296fac5 commit 5612a6a

File tree

9 files changed

+262
-170
lines changed

9 files changed

+262
-170
lines changed

extensions/positron-python/package-lock.json

Lines changed: 72 additions & 159 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/positron-python/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
{
127127
"id": "python.installPythonMac",
128128
"title": "Install Python",
129-
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Open Terminal](command:workbench.action.terminal.new)\n",
129+
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n",
130130
"media": {
131131
"markdown": "resources/walkthrough/install-python-macos.md"
132132
},
@@ -136,7 +136,7 @@
136136
{
137137
"id": "python.installPythonLinux",
138138
"title": "Install Python",
139-
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Open Terminal](command:workbench.action.terminal.new)\n",
139+
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n",
140140
"media": {
141141
"markdown": "resources/walkthrough/install-python-linux.md"
142142
},
@@ -1804,7 +1804,8 @@
18041804
"vscode-nls": "^5.0.1",
18051805
"vscode-tas-client": "^0.1.22",
18061806
"winreg": "^1.2.4",
1807-
"xml2js": "^0.4.19"
1807+
"xml2js": "^0.4.19",
1808+
"which":"^2.0.2"
18081809
},
18091810
"devDependencies": {
18101811
"@istanbuljs/nyc-config-typescript": "^1.0.2",
@@ -1828,6 +1829,7 @@
18281829
"@types/uuid": "^8.3.4",
18291830
"@types/vscode": "~1.68.0",
18301831
"@types/winreg": "^1.2.30",
1832+
"@types/which":"^2.0.1",
18311833
"@types/xml2js": "^0.4.2",
18321834
"@typescript-eslint/eslint-plugin": "^3.7.0",
18331835
"@typescript-eslint/parser": "^3.7.0",

extensions/positron-python/src/client/common/application/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping;
1616
* @interface ICommandNameWithoutArgumentTypeMapping
1717
*/
1818
interface ICommandNameWithoutArgumentTypeMapping {
19+
[Commands.InstallPythonOnMac]: [];
20+
[Commands.InstallPythonOnLinux]: [];
1921
[Commands.InstallPython]: [];
2022
[Commands.ClearWorkspaceInterpreter]: [];
2123
[Commands.Set_Interpreter]: [];

extensions/positron-python/src/client/common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export namespace Commands {
5757
export const RefreshTensorBoard = 'python.refreshTensorBoard';
5858
export const ReportIssue = 'python.reportIssue';
5959
export const InstallPython = 'python.installPython';
60+
export const InstallPythonOnMac = 'python.installPythonOnMac';
61+
export const InstallPythonOnLinux = 'python.installPythonOnLinux';
6062
export const TriggerEnvironmentSelection = 'python.triggerEnvSelection';
6163
}
6264

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
'use strict';
55

66
import { inject, injectable } from 'inversify';
7-
import { IExtensionSingleActivationService } from '../../../../activation/types';
8-
import { ExtensionContextKey } from '../../../../common/application/contextKeys';
9-
import { ICommandManager, IContextKeyManager } from '../../../../common/application/types';
10-
import { PythonWelcome } from '../../../../common/application/walkThroughs';
11-
import { Commands, PVSC_EXTENSION_ID } from '../../../../common/constants';
12-
import { IBrowserService, IDisposableRegistry } from '../../../../common/types';
13-
import { IPlatformService } from '../../../../common/platform/types';
7+
import { IExtensionSingleActivationService } from '../../../../../activation/types';
8+
import { ExtensionContextKey } from '../../../../../common/application/contextKeys';
9+
import { ICommandManager, IContextKeyManager } from '../../../../../common/application/types';
10+
import { PythonWelcome } from '../../../../../common/application/walkThroughs';
11+
import { Commands, PVSC_EXTENSION_ID } from '../../../../../common/constants';
12+
import { IBrowserService, IDisposableRegistry } from '../../../../../common/types';
13+
import { IPlatformService } from '../../../../../common/platform/types';
1414

1515
@injectable()
1616
export class InstallPythonCommand implements IExtensionSingleActivationService {
17-
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };
17+
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false };
1818

1919
constructor(
2020
@inject(ICommandManager) private readonly commandManager: ICommandManager,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* eslint-disable global-require */
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
5+
'use strict';
6+
7+
import type * as whichTypes from 'which';
8+
import { inject, injectable } from 'inversify';
9+
import { IExtensionSingleActivationService } from '../../../../../activation/types';
10+
import { Commands } from '../../../../../common/constants';
11+
import { IDisposableRegistry } from '../../../../../common/types';
12+
import { ITerminalServiceFactory } from '../../../../../common/terminal/types';
13+
import { ICommandManager } from '../../../../../common/application/types';
14+
import { sleep } from '../../../../../common/utils/async';
15+
import { OSType } from '../../../../../common/utils/platform';
16+
import { traceVerbose } from '../../../../../logging';
17+
18+
/**
19+
* Runs commands listed in walkthrough to install Python.
20+
*/
21+
@injectable()
22+
export class InstallPythonViaTerminal implements IExtensionSingleActivationService {
23+
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false };
24+
25+
constructor(
26+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
27+
@inject(ITerminalServiceFactory) private readonly terminalServiceFactory: ITerminalServiceFactory,
28+
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
29+
) {}
30+
31+
public async activate(): Promise<void> {
32+
this.disposables.push(
33+
this.commandManager.registerCommand(Commands.InstallPythonOnMac, () =>
34+
this._installPythonOnUnix(OSType.OSX),
35+
),
36+
);
37+
this.disposables.push(
38+
this.commandManager.registerCommand(Commands.InstallPythonOnLinux, () =>
39+
this._installPythonOnUnix(OSType.Linux),
40+
),
41+
);
42+
}
43+
44+
public async _installPythonOnUnix(os: OSType.Linux | OSType.OSX): Promise<void> {
45+
const terminalService = this.terminalServiceFactory.getTerminalService({});
46+
const commands = await getCommands(os);
47+
for (const command of commands) {
48+
await terminalService.sendText(command);
49+
await waitForCommandToProcess();
50+
}
51+
}
52+
}
53+
54+
async function getCommands(os: OSType.Linux | OSType.OSX) {
55+
if (os === OSType.OSX) {
56+
return ['brew install python3'];
57+
}
58+
return getCommandsForLinux();
59+
}
60+
61+
async function getCommandsForLinux() {
62+
let isDnfAvailable = false;
63+
try {
64+
const which = require('which') as typeof whichTypes;
65+
const resolvedPath = await which('dnf');
66+
traceVerbose('Resolved path to dnf module:', resolvedPath);
67+
isDnfAvailable = resolvedPath.trim().length > 0;
68+
} catch (ex) {
69+
traceVerbose('Dnf not found', ex);
70+
isDnfAvailable = false;
71+
}
72+
return isDnfAvailable
73+
? ['sudo dnf install python3']
74+
: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'];
75+
}
76+
77+
async function waitForCommandToProcess() {
78+
// Give the command some time to complete.
79+
// Its been observed that sending commands too early will strip some text off in VS Code Terminal.
80+
await sleep(500);
81+
}

extensions/positron-python/src/client/interpreter/serviceRegistry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy';
1212
import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types';
1313
import { EnvironmentTypeComparer } from './configuration/environmentTypeComparer';
1414
import { InstallPythonCommand } from './configuration/interpreterSelector/commands/installPython';
15+
import { InstallPythonViaTerminal } from './configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';
1516
import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter';
1617
import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter';
1718
import { SetShebangInterpreterCommand } from './configuration/interpreterSelector/commands/setShebangInterpreter';
@@ -45,6 +46,10 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void
4546
IExtensionSingleActivationService,
4647
InstallPythonCommand,
4748
);
49+
serviceManager.addSingleton<IExtensionSingleActivationService>(
50+
IExtensionSingleActivationService,
51+
InstallPythonViaTerminal,
52+
);
4853
serviceManager.addSingleton<IExtensionSingleActivationService>(
4954
IExtensionSingleActivationService,
5055
SetInterpreterCommand,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import rewiremock from 'rewiremock';
7+
import * as sinon from 'sinon';
8+
import { anything, instance, mock, verify, when } from 'ts-mockito';
9+
import * as TypeMoq from 'typemoq';
10+
import { ICommandManager } from '../../../../client/common/application/types';
11+
import { Commands } from '../../../../client/common/constants';
12+
import { ITerminalService, ITerminalServiceFactory } from '../../../../client/common/terminal/types';
13+
import { IDisposable } from '../../../../client/common/types';
14+
import { InstallPythonViaTerminal } from '../../../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';
15+
16+
suite('Install Python via Terminal', () => {
17+
let cmdManager: ICommandManager;
18+
let terminalServiceFactory: ITerminalServiceFactory;
19+
let installPythonCommand: InstallPythonViaTerminal;
20+
let terminalService: ITerminalService;
21+
setup(() => {
22+
rewiremock.enable();
23+
cmdManager = mock<ICommandManager>();
24+
terminalServiceFactory = mock<ITerminalServiceFactory>();
25+
terminalService = mock<ITerminalService>();
26+
when(terminalServiceFactory.getTerminalService(anything())).thenReturn(instance(terminalService));
27+
installPythonCommand = new InstallPythonViaTerminal(instance(cmdManager), instance(terminalServiceFactory), []);
28+
});
29+
30+
teardown(() => {
31+
rewiremock.disable();
32+
sinon.restore();
33+
});
34+
35+
test('Sends expected commands when InstallPythonOnLinux command is executed if no dnf is available', async () => {
36+
let installCommandHandler: () => Promise<void>;
37+
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
38+
installCommandHandler = cb;
39+
return TypeMoq.Mock.ofType<IDisposable>().object;
40+
});
41+
await installPythonCommand.activate();
42+
when(terminalService.sendText('sudo apt-get update')).thenResolve();
43+
when(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).thenResolve();
44+
45+
await installCommandHandler!();
46+
47+
verify(terminalService.sendText('sudo apt-get update')).once();
48+
verify(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).once();
49+
});
50+
51+
test('Sends expected commands when InstallPythonOnLinux command is executed if dnf is available', async () => {
52+
let installCommandHandler: () => Promise<void>;
53+
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
54+
installCommandHandler = cb;
55+
return TypeMoq.Mock.ofType<IDisposable>().object;
56+
});
57+
rewiremock('which').with((cmd: string) => {
58+
if (cmd === 'dnf') {
59+
return 'path/to/dnf';
60+
}
61+
throw new Error('Command not found');
62+
});
63+
64+
await installPythonCommand.activate();
65+
when(terminalService.sendText('sudo dnf install python3')).thenResolve();
66+
67+
await installCommandHandler!();
68+
69+
verify(terminalService.sendText('sudo dnf install python3')).once();
70+
});
71+
72+
test('Sends expected commands on Mac when InstallPythonOnMac command is executed if no dnf is available', async () => {
73+
let installCommandHandler: () => Promise<void>;
74+
when(cmdManager.registerCommand(Commands.InstallPythonOnMac, anything())).thenCall((_, cb) => {
75+
installCommandHandler = cb;
76+
return TypeMoq.Mock.ofType<IDisposable>().object;
77+
});
78+
await installPythonCommand.activate();
79+
when(terminalService.sendText('brew install python3')).thenResolve();
80+
81+
await installCommandHandler!();
82+
83+
verify(terminalService.sendText('brew install python3')).once();
84+
});
85+
});

extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '../../client/interpreter/autoSelection/types';
1616
import { EnvironmentTypeComparer } from '../../client/interpreter/configuration/environmentTypeComparer';
1717
import { InstallPythonCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython';
18+
import { InstallPythonViaTerminal } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';
1819
import { ResetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter';
1920
import { SetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter';
2021
import { SetShebangInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter';
@@ -50,6 +51,7 @@ suite('Interpreters - Service Registry', () => {
5051

5152
[
5253
[IExtensionSingleActivationService, InstallPythonCommand],
54+
[IExtensionSingleActivationService, InstallPythonViaTerminal],
5355
[IExtensionSingleActivationService, SetInterpreterCommand],
5456
[IExtensionSingleActivationService, ResetInterpreterCommand],
5557
[IExtensionSingleActivationService, SetShebangInterpreterCommand],

0 commit comments

Comments
 (0)