Skip to content

Commit

Permalink
Remove Homebrew installation (#537)
Browse files Browse the repository at this point in the history
* Basic tokenizer

* Fixed property names

* Tests, round I

* Tests, round II

* tokenizer test

* Remove temorary change

* Fix merge issue

* Merge conflict

* Merge conflict

* Completion test

* Fix last line

* Fix javascript math

* Make test await for results

* Add license headers

* Rename definitions to types

* License headers

* Fix typo in completion details (typo)

* Fix hover test

* Russian translations

* Update to better translation

* Fix typo

*  #70 How to get all parameter info when filling in a function param list

* Fix #70 How to get all parameter info when filling in a function param list

* Clean up

* Clean imports

* CR feedback

* Trim whitespace for test stability

* More tests

* Better handle no-parameters documentation

* Better handle ellipsis and Python3

* Basic services

* Install check

* Output installer messages

* Warn default Mac OS interpreter

* Remove test change

* Add tests

* PR feedback

* CR feedback

* Mock process instead

* Fix Brew detection

* Update test

* Elevated module install

* Fix path check

* Add check suppression option & suppress vor VE by default

* Fix most linter tests

* Merge conflict

* Per-user install

* Handle VE/Conda

* Fix tests

* Remove Homebrew

* Fix OS name
  • Loading branch information
Mikhail Arkhipov authored Jan 5, 2018
1 parent 321e204 commit d2a3b43
Show file tree
Hide file tree
Showing 2 changed files with 13 additions and 187 deletions.
98 changes: 10 additions & 88 deletions src/client/common/installer/pythonInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,116 +2,38 @@
// Licensed under the MIT License.
'use strict';

import { OutputChannel } from 'vscode';
import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { IApplicationShell } from '../application/types';
import { IPythonSettings } from '../configSettings';
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
import { IFileSystem, IPlatformService } from '../platform/types';
import { IProcessService } from '../process/types';
import { IOutputChannel } from '../types';
import { IPlatformService } from '../platform/types';

export class PythonInstaller {
private locator: IInterpreterLocatorService;
private process: IProcessService;
private fs: IFileSystem;
private outputChannel: OutputChannel;
private _platform: IPlatformService;
private _shell: IApplicationShell;
private shell: IApplicationShell;

constructor(private serviceContainer: IServiceContainer) {
this.locator = serviceContainer.get<IInterpreterLocatorService>(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE);
this.shell = serviceContainer.get<IApplicationShell>(IApplicationShell);
}

public async checkPythonInstallation(settings: IPythonSettings): Promise<boolean> {
if (settings.disableInstallationChecks === true) {
return true;
}
let interpreters = await this.locator.getInterpreters();
const interpreters = await this.locator.getInterpreters();
if (interpreters.length > 0) {
if (this.platform.isMac &&
const platform = this.serviceContainer.get<IPlatformService>(IPlatformService);
if (platform.isMac &&
settings.pythonPath === 'python' &&
interpreters[0].type === InterpreterType.Unknown) {
await this.shell.showWarningMessage('Selected interpreter is MacOS system Python which is not recommended. Please select different interpreter');
await this.shell.showWarningMessage('Selected interpreter is macOS system Python which is not recommended. Please select different interpreter');
}
return true;
}

if (!this.platform.isMac) {
// Windows or Linux
await this.shell.showErrorMessage('Python is not installed. Please download and install Python before using the extension.');
this.shell.openUrl('https://www.python.org/downloads');
return false;
}

this.process = this.serviceContainer.get<IProcessService>(IProcessService);
this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
this.outputChannel = this.serviceContainer.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);

if (this.platform.isMac) {
if (await this.shell.showErrorMessage('Python that comes with MacOS is not supported. Would you like to install regular Python now?', 'Yes', 'No') === 'Yes') {
const brewInstalled = await this.ensureBrew();
if (!brewInstalled) {
await this.shell.showErrorMessage('Unable to install Homebrew package manager. Try installing it manually.');
this.shell.openUrl('https://brew.sh');
return false;
}
await this.executeAndOutput('brew', ['install', 'python']);
}
}

interpreters = await this.locator.getInterpreters();
return interpreters.length > 0;
}

private isBrewInstalled(): Promise<boolean> {
return this.fs.fileExistsAsync('/usr/local/bin/brew');
}

private async ensureBrew(): Promise<boolean> {
if (await this.isBrewInstalled()) {
return true;
}
const result = await this.executeAndOutput(
'/usr/bin/ruby',
['-e', '"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"']);
return result && await this.isBrewInstalled();
}

private executeAndOutput(command: string, args: string[]): Promise<boolean> {
let failed = false;
this.outputChannel.show();

const result = this.process.execObservable(command, args, { mergeStdOutErr: true, throwOnStdErr: false });
result.out.subscribe(output => {
this.outputChannel.append(output.out);
}, error => {
failed = true;
this.shell.showErrorMessage(`Unable to execute '${command}', error: ${error}`);
});

return new Promise<boolean>((resolve, reject) => {
if (failed) {
resolve(false);
}
result.proc.on('exit', (code, signal) => {
resolve(!signal);
});
});
}

private get shell(): IApplicationShell {
if (!this._shell) {
this._shell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
}
return this._shell;
}

private get platform(): IPlatformService {
if (!this._platform) {
this._platform = this.serviceContainer.get<IPlatformService>(IPlatformService);
}
return this._platform;
await this.shell.showErrorMessage('Python is not installed. Please download and install Python before using the extension.');
this.shell.openUrl('https://www.python.org/downloads');
return false;
}
}
102 changes: 3 additions & 99 deletions src/test/install/pythonInstallation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import { IApplicationShell } from '../../client/common/application/types';
import { IPythonSettings } from '../../client/common/configSettings';
import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants';
import { PythonInstaller } from '../../client/common/installer/pythonInstallation';
import { IFileSystem, IPlatformService } from '../../client/common/platform/types';
import { IProcessService, ObservableExecutionResult, Output } from '../../client/common/process/types';
import { IOutputChannel } from '../../client/common/types';
import { IPlatformService } from '../../client/common/platform/types';
import { IInterpreterLocatorService } from '../../client/interpreter/contracts';
import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts';
import { ServiceContainer } from '../../client/ioc/container';
Expand All @@ -26,12 +24,9 @@ class TestContext {
public serviceManager: ServiceManager;
public serviceContainer: IServiceContainer;
public platform: TypeMoq.IMock<IPlatformService>;
public fileSystem: TypeMoq.IMock<IFileSystem>;
public appShell: TypeMoq.IMock<IApplicationShell>;
public locator: TypeMoq.IMock<IInterpreterLocatorService>;
public settings: TypeMoq.IMock<IPythonSettings>;
public process: TypeMoq.IMock<IProcessService>;
public output: TypeMoq.IMock<vscode.OutputChannel>;
public pythonInstaller: PythonInstaller;

constructor(isMac: boolean) {
Expand All @@ -40,19 +35,13 @@ class TestContext {
this.serviceContainer = new ServiceContainer(cont);

this.platform = TypeMoq.Mock.ofType<IPlatformService>();
this.fileSystem = TypeMoq.Mock.ofType<IFileSystem>();
this.appShell = TypeMoq.Mock.ofType<IApplicationShell>();
this.locator = TypeMoq.Mock.ofType<IInterpreterLocatorService>();
this.settings = TypeMoq.Mock.ofType<IPythonSettings>();
this.process = TypeMoq.Mock.ofType<IProcessService>();
this.output = TypeMoq.Mock.ofType<vscode.OutputChannel>();

this.serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, this.platform.object);
this.serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, this.fileSystem.object);
this.serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, this.appShell.object);
this.serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, this.locator.object);
this.serviceManager.addSingletonInstance<IProcessService>(IProcessService, this.process.object);
this.serviceManager.addSingletonInstance<vscode.OutputChannel>(IOutputChannel, this.output.object, STANDARD_OUTPUT_CHANNEL);
this.pythonInstaller = new PythonInstaller(this.serviceContainer);

this.platform.setup(x => x.isMac).returns(() => isMac);
Expand Down Expand Up @@ -80,7 +69,7 @@ suite('Installation', () => {
assert.equal(showErrorMessageCalled, false, 'Disabling checks has no effect');
});

test('Windows: Python missing', async () => {
test('Python missing', async () => {
const c = new TestContext(false);
let showErrorMessageCalled = false;
let openUrlCalled = false;
Expand All @@ -100,7 +89,7 @@ suite('Installation', () => {
assert.equal(url, 'https://www.python.org/downloads', 'Python download page is incorrect');
});

test('Mac: Python missing', async () => {
test('Mac: Default Python warning', async () => {
const c = new TestContext(true);
let called = false;
c.appShell.setup(x => x.showWarningMessage(TypeMoq.It.isAnyString())).callback(() => called = true);
Expand All @@ -115,89 +104,4 @@ suite('Installation', () => {
assert.equal(passed, true, 'Default MacOS Python not accepted');
assert.equal(called, true, 'Warning not shown');
});

test('Mac: Default Python, user refused install', async () => {
const c = new TestContext(true);
let errorMessage = '';

c.appShell
.setup(x => x.showErrorMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()))
.callback((m: string, a1: string, a2: string) => errorMessage = m)
.returns(() => Promise.resolve('No'));
c.locator.setup(x => x.getInterpreters()).returns(() => Promise.resolve([]));

const passed = await c.pythonInstaller.checkPythonInstallation(c.settings.object);
assert.equal(passed, false, 'Default MacOS Python accepted');
assert.equal(errorMessage.startsWith('Python that comes with MacOS is not supported'), true, 'Error message that MacOS Python not supported not shown');
});

test('Mac: Default Python, Brew installation', async () => {
const c = new TestContext(true);
let errorMessage = '';
let processName = '';
let args;
let brewPath;
let outputShown = false;

c.appShell
.setup(x => x.showErrorMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()))
.returns(() => Promise.resolve('Yes'));
c.appShell
.setup(x => x.showErrorMessage(TypeMoq.It.isAnyString()))
.callback((m: string) => errorMessage = m);
c.locator.setup(x => x.getInterpreters()).returns(() => Promise.resolve([]));
c.fileSystem
.setup(x => x.fileExistsAsync(TypeMoq.It.isAnyString()))
.returns((p: string) => {
brewPath = p;
return Promise.resolve(false);
});

const childProcess = TypeMoq.Mock.ofType<ChildProcess>();
childProcess
.setup(p => p.on('exit', TypeMoq.It.isAny()))
.callback((e: string, listener: (code, signal) => void) => {
listener.call(0, undefined);
});
const processOutput: Output<string> = {
source: 'stdout',
out: 'started'
};
const observable = new Rx.Observable<Output<string>>(subscriber => subscriber.next(processOutput));
const brewInstallProcess: ObservableExecutionResult<string> = {
proc: childProcess.object,
out: observable
};

c.output.setup(x => x.show()).callback(() => outputShown = true);
c.process
.setup(x => x.execObservable(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.callback((p: string, a: string[], o: SpawnOptions) => {
processName = p;
args = a;
})
.returns(() => brewInstallProcess);

await c.pythonInstaller.checkPythonInstallation(c.settings.object);

assert.notEqual(brewPath, undefined, 'Brew installer location not checked');
assert.equal(brewPath, '/usr/local/bin/brew', 'Brew installer location is incorrect');
assert.notEqual(processName, undefined, 'Brew installer not invoked');
assert.equal(processName, '/usr/bin/ruby', 'Brew installer name is incorrect');
assert.equal(args[0], '-e', 'Brew installer argument is incorrect');
assert.equal(args[1], '"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"', 'Homebrew installer argument is incorrect');
assert.equal(outputShown, true, 'Output panel not shown');
assert.equal(errorMessage.startsWith('Unable to install Homebrew'), true, 'Homebrew install failed message no shown');

c.fileSystem
.setup(x => x.fileExistsAsync(TypeMoq.It.isAnyString()))
.returns(() => Promise.resolve(true));
errorMessage = '';

await c.pythonInstaller.checkPythonInstallation(c.settings.object);
assert.equal(errorMessage, '', `Unexpected error message ${errorMessage}`);
assert.equal(processName, 'brew', 'Brew installer name is incorrect');
assert.equal(args[0], 'install', 'Brew "install" argument is incorrect');
assert.equal(args[1], 'python', 'Brew "python" argument is incorrect');
});
});

0 comments on commit d2a3b43

Please sign in to comment.