Skip to content

Commit 7b35110

Browse files
Kartik Rajrzhao271
authored andcommitted
Do not assume the Linux distro to be debian if dnf is not found while installing python (microsoft/vscode-python#19582)
* Implement * Code reviews Co-authored-by: Raymond Zhao <[email protected]> Co-authored-by: Raymond Zhao <[email protected]>
1 parent 5db6ce5 commit 7b35110

File tree

3 files changed

+91
-31
lines changed

3 files changed

+91
-31
lines changed

extensions/positron-python/src/client/common/utils/localize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ export namespace Interpreters {
277277
'Interpreters.selectInterpreterTip',
278278
'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar',
279279
);
280+
export const installPythonTerminalMessage = localize(
281+
'Interpreters.installPythonTerminalMessage',
282+
'💡 Please try installing the python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads',
283+
);
280284
}
281285

282286
export namespace InterpreterQuickPickList {

extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ import { inject, injectable } from 'inversify';
99
import { IExtensionSingleActivationService } from '../../../../../activation/types';
1010
import { Commands } from '../../../../../common/constants';
1111
import { IDisposableRegistry } from '../../../../../common/types';
12-
import { ITerminalServiceFactory } from '../../../../../common/terminal/types';
13-
import { ICommandManager } from '../../../../../common/application/types';
12+
import { ICommandManager, ITerminalManager } from '../../../../../common/application/types';
1413
import { sleep } from '../../../../../common/utils/async';
1514
import { OSType } from '../../../../../common/utils/platform';
1615
import { traceVerbose } from '../../../../../logging';
16+
import { Interpreters } from '../../../../../common/utils/localize';
17+
18+
enum PackageManagers {
19+
brew = 'brew',
20+
apt = 'apt',
21+
dnf = 'dnf',
22+
}
1723

1824
/**
1925
* Runs commands listed in walkthrough to install Python.
@@ -22,9 +28,15 @@ import { traceVerbose } from '../../../../../logging';
2228
export class InstallPythonViaTerminal implements IExtensionSingleActivationService {
2329
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false };
2430

31+
private readonly packageManagerCommands: Record<PackageManagers, string[]> = {
32+
brew: ['brew install python3'],
33+
dnf: ['sudo dnf install python3'],
34+
apt: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'],
35+
};
36+
2537
constructor(
2638
@inject(ICommandManager) private readonly commandManager: ICommandManager,
27-
@inject(ITerminalServiceFactory) private readonly terminalServiceFactory: ITerminalServiceFactory,
39+
@inject(ITerminalManager) private readonly terminalManager: ITerminalManager,
2840
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
2941
) {}
3042

@@ -42,36 +54,49 @@ export class InstallPythonViaTerminal implements IExtensionSingleActivationServi
4254
}
4355

4456
public async _installPythonOnUnix(os: OSType.Linux | OSType.OSX): Promise<void> {
45-
const terminalService = this.terminalServiceFactory.getTerminalService({});
46-
const commands = await getCommands(os);
57+
const commands = await this.getCommands(os);
58+
const terminal = this.terminalManager.createTerminal({
59+
name: 'Python',
60+
message: commands.length ? undefined : Interpreters.installPythonTerminalMessage,
61+
});
62+
terminal.show(true);
63+
await waitForTerminalToStartup();
4764
for (const command of commands) {
48-
await terminalService.sendText(command);
65+
terminal.sendText(command);
4966
await waitForCommandToProcess();
5067
}
5168
}
52-
}
5369

54-
async function getCommands(os: OSType.Linux | OSType.OSX) {
55-
if (os === OSType.OSX) {
56-
return ['brew install python3'];
70+
private async getCommands(os: OSType.Linux | OSType.OSX) {
71+
if (os === OSType.OSX) {
72+
return this.packageManagerCommands[PackageManagers.brew];
73+
}
74+
return this.getCommandsForLinux();
5775
}
58-
return getCommandsForLinux();
59-
}
6076

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;
77+
private async getCommandsForLinux() {
78+
for (const packageManager of [PackageManagers.apt, PackageManagers.dnf]) {
79+
let isPackageAvailable = false;
80+
try {
81+
const which = require('which') as typeof whichTypes;
82+
const resolvedPath = await which(packageManager);
83+
traceVerbose(`Resolved path to ${packageManager} module:`, resolvedPath);
84+
isPackageAvailable = resolvedPath.trim().length > 0;
85+
} catch (ex) {
86+
traceVerbose(`${packageManager} not found`, ex);
87+
isPackageAvailable = false;
88+
}
89+
if (isPackageAvailable) {
90+
return this.packageManagerCommands[packageManager];
91+
}
92+
}
93+
return [];
7194
}
72-
return isDnfAvailable
73-
? ['sudo dnf install python3']
74-
: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'];
95+
}
96+
97+
async function waitForTerminalToStartup() {
98+
// Sometimes the terminal takes some time to start up before it can start accepting input.
99+
await sleep(100);
75100
}
76101

77102
async function waitForCommandToProcess() {

extensions/positron-python/src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.unit.test.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,34 @@
33

44
'use strict';
55

6+
import { expect } from 'chai';
67
import rewiremock from 'rewiremock';
78
import * as sinon from 'sinon';
89
import { anything, instance, mock, verify, when } from 'ts-mockito';
910
import * as TypeMoq from 'typemoq';
10-
import { ICommandManager } from '../../../../client/common/application/types';
11+
import { ICommandManager, ITerminalManager } from '../../../../client/common/application/types';
1112
import { Commands } from '../../../../client/common/constants';
12-
import { ITerminalService, ITerminalServiceFactory } from '../../../../client/common/terminal/types';
13+
import { ITerminalService } from '../../../../client/common/terminal/types';
1314
import { IDisposable } from '../../../../client/common/types';
15+
import { Interpreters } from '../../../../client/common/utils/localize';
1416
import { InstallPythonViaTerminal } from '../../../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';
1517

1618
suite('Install Python via Terminal', () => {
1719
let cmdManager: ICommandManager;
18-
let terminalServiceFactory: ITerminalServiceFactory;
20+
let terminalServiceFactory: ITerminalManager;
1921
let installPythonCommand: InstallPythonViaTerminal;
2022
let terminalService: ITerminalService;
23+
let message: string | undefined;
2124
setup(() => {
2225
rewiremock.enable();
2326
cmdManager = mock<ICommandManager>();
24-
terminalServiceFactory = mock<ITerminalServiceFactory>();
27+
terminalServiceFactory = mock<ITerminalManager>();
2528
terminalService = mock<ITerminalService>();
26-
when(terminalServiceFactory.getTerminalService(anything())).thenReturn(instance(terminalService));
29+
message = undefined;
30+
when(terminalServiceFactory.createTerminal(anything())).thenCall((options) => {
31+
message = options.message;
32+
return instance(terminalService);
33+
});
2734
installPythonCommand = new InstallPythonViaTerminal(instance(cmdManager), instance(terminalServiceFactory), []);
2835
});
2936

@@ -32,12 +39,18 @@ suite('Install Python via Terminal', () => {
3239
sinon.restore();
3340
});
3441

35-
test('Sends expected commands when InstallPythonOnLinux command is executed if no dnf is available', async () => {
42+
test('Sends expected commands when InstallPythonOnLinux command is executed if apt is available', async () => {
3643
let installCommandHandler: () => Promise<void>;
3744
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
3845
installCommandHandler = cb;
3946
return TypeMoq.Mock.ofType<IDisposable>().object;
4047
});
48+
rewiremock('which').with((cmd: string) => {
49+
if (cmd === 'apt') {
50+
return 'path/to/apt';
51+
}
52+
throw new Error('Command not found');
53+
});
4154
await installPythonCommand.activate();
4255
when(terminalService.sendText('sudo apt-get update')).thenResolve();
4356
when(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).thenResolve();
@@ -67,6 +80,23 @@ suite('Install Python via Terminal', () => {
6780
await installCommandHandler!();
6881

6982
verify(terminalService.sendText('sudo dnf install python3')).once();
83+
expect(message).to.be.equal(undefined);
84+
});
85+
86+
test('Creates terminal with appropriate message when InstallPythonOnLinux command is executed if no known linux package managers are available', async () => {
87+
let installCommandHandler: () => Promise<void>;
88+
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
89+
installCommandHandler = cb;
90+
return TypeMoq.Mock.ofType<IDisposable>().object;
91+
});
92+
rewiremock('which').with((_cmd: string) => {
93+
throw new Error('Command not found');
94+
});
95+
96+
await installPythonCommand.activate();
97+
await installCommandHandler!();
98+
99+
expect(message).to.be.equal(Interpreters.installPythonTerminalMessage);
70100
});
71101

72102
test('Sends expected commands on Mac when InstallPythonOnMac command is executed if no dnf is available', async () => {
@@ -81,5 +111,6 @@ suite('Install Python via Terminal', () => {
81111
await installCommandHandler!();
82112

83113
verify(terminalService.sendText('brew install python3')).once();
114+
expect(message).to.be.equal(undefined);
84115
});
85116
});

0 commit comments

Comments
 (0)