Skip to content
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

Ensure Install Python button on the walkthrough opens and fills in the suggested command #19487

Merged
merged 2 commits into from
Jul 18, 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
231 changes: 72 additions & 159 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
{
"id": "python.installPythonMac",
"title": "Install Python",
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Open Terminal](command:workbench.action.terminal.new)\n",
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n",
"media": {
"markdown": "resources/walkthrough/install-python-macos.md"
},
Expand All @@ -136,7 +136,7 @@
{
"id": "python.installPythonLinux",
"title": "Install Python",
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Open Terminal](command:workbench.action.terminal.new)\n",
"description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n",
"media": {
"markdown": "resources/walkthrough/install-python-linux.md"
},
Expand Down Expand Up @@ -1804,7 +1804,8 @@
"vscode-nls": "^5.0.1",
"vscode-tas-client": "^0.1.22",
"winreg": "^1.2.4",
"xml2js": "^0.4.19"
"xml2js": "^0.4.19",
"which":"^2.0.2"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
Expand All @@ -1828,6 +1829,7 @@
"@types/uuid": "^8.3.4",
"@types/vscode": "~1.68.0",
"@types/winreg": "^1.2.30",
"@types/which":"^2.0.1",
"@types/xml2js": "^0.4.2",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
Expand Down
2 changes: 2 additions & 0 deletions src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping;
* @interface ICommandNameWithoutArgumentTypeMapping
*/
interface ICommandNameWithoutArgumentTypeMapping {
[Commands.InstallPythonOnMac]: [];
[Commands.InstallPythonOnLinux]: [];
[Commands.InstallPython]: [];
[Commands.ClearWorkspaceInterpreter]: [];
[Commands.Set_Interpreter]: [];
Expand Down
2 changes: 2 additions & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export namespace Commands {
export const RefreshTensorBoard = 'python.refreshTensorBoard';
export const ReportIssue = 'python.reportIssue';
export const InstallPython = 'python.installPython';
export const InstallPythonOnMac = 'python.installPythonOnMac';
export const InstallPythonOnLinux = 'python.installPythonOnLinux';
export const TriggerEnvironmentSelection = 'python.triggerEnvSelection';
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
'use strict';

import { inject, injectable } from 'inversify';
import { IExtensionSingleActivationService } from '../../../../activation/types';
import { ExtensionContextKey } from '../../../../common/application/contextKeys';
import { ICommandManager, IContextKeyManager } from '../../../../common/application/types';
import { PythonWelcome } from '../../../../common/application/walkThroughs';
import { Commands, PVSC_EXTENSION_ID } from '../../../../common/constants';
import { IBrowserService, IDisposableRegistry } from '../../../../common/types';
import { IPlatformService } from '../../../../common/platform/types';
import { IExtensionSingleActivationService } from '../../../../../activation/types';
import { ExtensionContextKey } from '../../../../../common/application/contextKeys';
import { ICommandManager, IContextKeyManager } from '../../../../../common/application/types';
import { PythonWelcome } from '../../../../../common/application/walkThroughs';
import { Commands, PVSC_EXTENSION_ID } from '../../../../../common/constants';
import { IBrowserService, IDisposableRegistry } from '../../../../../common/types';
import { IPlatformService } from '../../../../../common/platform/types';

@injectable()
export class InstallPythonCommand implements IExtensionSingleActivationService {
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false };

constructor(
@inject(ICommandManager) private readonly commandManager: ICommandManager,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable global-require */
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import type * as whichTypes from 'which';
import { inject, injectable } from 'inversify';
import { IExtensionSingleActivationService } from '../../../../../activation/types';
import { Commands } from '../../../../../common/constants';
import { IDisposableRegistry } from '../../../../../common/types';
import { ITerminalServiceFactory } from '../../../../../common/terminal/types';
import { ICommandManager } from '../../../../../common/application/types';
import { sleep } from '../../../../../common/utils/async';
import { OSType } from '../../../../../common/utils/platform';
import { traceVerbose } from '../../../../../logging';

/**
* Runs commands listed in walkthrough to install Python.
*/
@injectable()
export class InstallPythonViaTerminal implements IExtensionSingleActivationService {
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false };

constructor(
@inject(ICommandManager) private readonly commandManager: ICommandManager,
@inject(ITerminalServiceFactory) private readonly terminalServiceFactory: ITerminalServiceFactory,
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
) {}

public async activate(): Promise<void> {
this.disposables.push(
this.commandManager.registerCommand(Commands.InstallPythonOnMac, () =>
this._installPythonOnUnix(OSType.OSX),
),
);
this.disposables.push(
this.commandManager.registerCommand(Commands.InstallPythonOnLinux, () =>
this._installPythonOnUnix(OSType.Linux),
),
);
}

public async _installPythonOnUnix(os: OSType.Linux | OSType.OSX): Promise<void> {
const terminalService = this.terminalServiceFactory.getTerminalService({});
const commands = await getCommands(os);
for (const command of commands) {
await terminalService.sendText(command);
await waitForCommandToProcess();
}
}
}

async function getCommands(os: OSType.Linux | OSType.OSX) {
if (os === OSType.OSX) {
return ['brew install python3'];
}
return getCommandsForLinux();
}

async function getCommandsForLinux() {
let isDnfAvailable = false;
try {
const which = require('which') as typeof whichTypes;
const resolvedPath = await which('dnf');
traceVerbose('Resolved path to dnf module:', resolvedPath);
isDnfAvailable = resolvedPath.trim().length > 0;
} catch (ex) {
traceVerbose('Dnf not found', ex);
isDnfAvailable = false;
}
return isDnfAvailable
? ['sudo dnf install python3']
: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'];
}

async function waitForCommandToProcess() {
// Give the command some time to complete.
// Its been observed that sending commands too early will strip some text off in VS Code Terminal.
await sleep(500);
}
5 changes: 5 additions & 0 deletions src/client/interpreter/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy';
import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types';
import { EnvironmentTypeComparer } from './configuration/environmentTypeComparer';
import { InstallPythonCommand } from './configuration/interpreterSelector/commands/installPython';
import { InstallPythonViaTerminal } from './configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';
import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter';
import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter';
import { SetShebangInterpreterCommand } from './configuration/interpreterSelector/commands/setShebangInterpreter';
Expand Down Expand Up @@ -45,6 +46,10 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void
IExtensionSingleActivationService,
InstallPythonCommand,
);
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
InstallPythonViaTerminal,
);
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
SetInterpreterCommand,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import rewiremock from 'rewiremock';
import * as sinon from 'sinon';
import { anything, instance, mock, verify, when } from 'ts-mockito';
import * as TypeMoq from 'typemoq';
import { ICommandManager } from '../../../../client/common/application/types';
import { Commands } from '../../../../client/common/constants';
import { ITerminalService, ITerminalServiceFactory } from '../../../../client/common/terminal/types';
import { IDisposable } from '../../../../client/common/types';
import { InstallPythonViaTerminal } from '../../../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';

suite('Install Python via Terminal', () => {
let cmdManager: ICommandManager;
let terminalServiceFactory: ITerminalServiceFactory;
let installPythonCommand: InstallPythonViaTerminal;
let terminalService: ITerminalService;
setup(() => {
rewiremock.enable();
cmdManager = mock<ICommandManager>();
terminalServiceFactory = mock<ITerminalServiceFactory>();
terminalService = mock<ITerminalService>();
when(terminalServiceFactory.getTerminalService(anything())).thenReturn(instance(terminalService));
installPythonCommand = new InstallPythonViaTerminal(instance(cmdManager), instance(terminalServiceFactory), []);
});

teardown(() => {
rewiremock.disable();
sinon.restore();
});

test('Sends expected commands when InstallPythonOnLinux command is executed if no dnf is available', async () => {
let installCommandHandler: () => Promise<void>;
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
installCommandHandler = cb;
return TypeMoq.Mock.ofType<IDisposable>().object;
});
await installPythonCommand.activate();
when(terminalService.sendText('sudo apt-get update')).thenResolve();
when(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).thenResolve();

await installCommandHandler!();

verify(terminalService.sendText('sudo apt-get update')).once();
verify(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).once();
});

test('Sends expected commands when InstallPythonOnLinux command is executed if dnf is available', async () => {
let installCommandHandler: () => Promise<void>;
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
installCommandHandler = cb;
return TypeMoq.Mock.ofType<IDisposable>().object;
});
rewiremock('which').with((cmd: string) => {
if (cmd === 'dnf') {
return 'path/to/dnf';
}
throw new Error('Command not found');
});

await installPythonCommand.activate();
when(terminalService.sendText('sudo dnf install python3')).thenResolve();

await installCommandHandler!();

verify(terminalService.sendText('sudo dnf install python3')).once();
});

test('Sends expected commands on Mac when InstallPythonOnMac command is executed if no dnf is available', async () => {
let installCommandHandler: () => Promise<void>;
when(cmdManager.registerCommand(Commands.InstallPythonOnMac, anything())).thenCall((_, cb) => {
installCommandHandler = cb;
return TypeMoq.Mock.ofType<IDisposable>().object;
});
await installPythonCommand.activate();
when(terminalService.sendText('brew install python3')).thenResolve();

await installCommandHandler!();

verify(terminalService.sendText('brew install python3')).once();
});
});
2 changes: 2 additions & 0 deletions src/test/interpreters/serviceRegistry.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '../../client/interpreter/autoSelection/types';
import { EnvironmentTypeComparer } from '../../client/interpreter/configuration/environmentTypeComparer';
import { InstallPythonCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython';
import { InstallPythonViaTerminal } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';
import { ResetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter';
import { SetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter';
import { SetShebangInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter';
Expand Down Expand Up @@ -50,6 +51,7 @@ suite('Interpreters - Service Registry', () => {

[
[IExtensionSingleActivationService, InstallPythonCommand],
[IExtensionSingleActivationService, InstallPythonViaTerminal],
[IExtensionSingleActivationService, SetInterpreterCommand],
[IExtensionSingleActivationService, ResetInterpreterCommand],
[IExtensionSingleActivationService, SetShebangInterpreterCommand],
Expand Down